nidus-sync/main.go
Eli Ribble d4cbfb960e
Some checks failed
/ golint (push) Failing after 12s
Move to setting version info explicitly in linker flags
We don't have go built-in VCS information in a nix build because the git
repository isn't present. After struggling to build an overlay that
would provide it, I decided this path is easier of just injecting in the
data that we need.

Issue: #5
2026-05-19 19:46:05 +00:00

353 lines
10 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"runtime/debug"
//"strconv"
"syscall"
"time"
"github.com/coreos/go-systemd/activation"
"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/getsentry/sentry-go/zerolog"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"source.gleipnir.technology/Gleipnir/nidus-sync/api"
"source.gleipnir.technology/Gleipnir/nidus-sync/auth"
"source.gleipnir.technology/Gleipnir/nidus-sync/config"
"source.gleipnir.technology/Gleipnir/nidus-sync/db"
"source.gleipnir.technology/Gleipnir/nidus-sync/html"
"source.gleipnir.technology/Gleipnir/nidus-sync/lint"
"source.gleipnir.technology/Gleipnir/nidus-sync/llm"
"source.gleipnir.technology/Gleipnir/nidus-sync/middleware"
"source.gleipnir.technology/Gleipnir/nidus-sync/platform"
"source.gleipnir.technology/Gleipnir/nidus-sync/rmo"
nidussync "source.gleipnir.technology/Gleipnir/nidus-sync/sync"
"source.gleipnir.technology/Gleipnir/nidus-sync/version"
)
// These will be set by ldflags at build time by Nix
var (
BuildTime = "unknown"
Revision = "unknown"
Version = "dev"
)
func main() {
err := config.Parse()
if err != nil {
log.Error().Err(err).Msg("Failed to parse config")
os.Exit(1)
}
var prod = flag.Bool("prod", false, "Force into production mode")
flag.Parse()
if prod != nil && *prod {
log.Warn().Msg("Forcing production mode for testing templates")
config.Environment = "PRODUCTION"
}
v := GetVersionInfo()
log.Info().
Str("environment", config.Environment).
Bool("is-prod", config.IsProductionEnvironment()).
Str("revision", v.Revision).
Str("build_time", v.BuildTime.String()).
Bool("is modified", v.IsModified).
Str("Version", Version).
Str("Revision", Revision).
Str("BuildDate", BuildTime).
Msg("Starting")
api.SetVersionInfo(v)
err = sentry.Init(sentry.ClientOptions{
Debug: false, //!config.IsProductionEnvironment(),
Dsn: config.SentryDSN,
EnableTracing: true,
SendDefaultPII: true,
TracesSampleRate: 1.0,
})
if err != nil {
log.Error().Err(err).Msg("Failed to start sentry connection")
os.Exit(2)
}
sentryWriter, err := sentryzerolog.New(sentryzerolog.Config{
ClientOptions: sentry.ClientOptions{
Dsn: config.SentryDSN,
},
Options: sentryzerolog.Options{
Levels: []zerolog.Level{zerolog.ErrorLevel, zerolog.FatalLevel, zerolog.PanicLevel},
WithBreadcrumbs: true,
FlushTimeout: 3 * time.Second,
},
})
if err != nil {
log.Fatal().Err(err).Msg("Failed to create sentry writer")
os.Exit(2)
}
defer lint.LogOnErr(sentryWriter.Close, "close sentry writer")
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.MultiLevelWriter(zerolog.ConsoleWriter{Out: os.Stderr}, sentryWriter))
if os.Getenv("VERBOSE") != "" {
log.Logger = log.Logger.Level(zerolog.DebugLevel)
} else {
log.Logger = log.Logger.Level(zerolog.InfoLevel)
}
// Defer cleanup in reverse order - these will execute LAST (LIFO)
defer func() {
log.Info().Msg("Final cleanup")
err = os.Stderr.Sync()
if err != nil {
log.Error().Err(err).Msg("sync stderr")
}
err = sentryWriter.Close()
if err != nil {
log.Error().Err(err).Msg("close sentrywriter")
}
sentry.Flush(2 * time.Second)
}()
err = db.InitializeDatabase(context.TODO(), config.PGDSN)
if err != nil {
log.Error().Err(err).Msg("Failed to connect to database")
os.Exit(3)
}
err = html.LoadTemplates()
if err != nil {
log.Error().Err(err).Msg("Failed to load html templates")
os.Exit(4)
}
// Start up background processes
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = platform.StartAll(ctx)
if err != nil {
log.Error().Err(err).Msg("Failed at platform.StartAll")
os.Exit(5)
}
router_logger := log.With().Logger()
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
//r := chi.NewRouter()
r := mux.NewRouter()
r.Use(LoggerMiddleware(&router_logger))
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
//r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(sentryMiddleware.Handle)
r.Use(auth.NewSessionManager().LoadAndSave)
// Set up routing by hostname
sync_router := r.Host(config.DomainNidus).Subrouter()
sync_api_router := sync_router.PathPrefix("/api").Subrouter()
api.AddRoutesSync(sync_api_router)
nidussync.Router(sync_router)
rmo_router := r.Host(config.DomainRMO).Subrouter()
rmo_api_router := rmo_router.PathPrefix("/api").Subrouter()
api.AddRoutesRMO(rmo_api_router)
rmo.Router(rmo_router)
//hr.Map("", sr) // default
//hr.Map("*", sr) // default
log.Debug().Str("report url", config.DomainRMO).Str("sync url", config.DomainNidus).Msg("Serving at URLs")
// Start pprof debug server on a separate port (localhost-only for security).
// Tunnels and Staging builds can set PPROF_BIND to override.
// Access via: curl http://localhost:6060/debug/pprof/goroutine?debug=1
pprofBind := os.Getenv("PPROF_BIND")
if pprofBind == "" {
pprofBind = "localhost:6060"
}
go func() {
log.Info().Str("address", pprofBind).Msg("Starting pprof debug server")
if err := http.ListenAndServe(pprofBind, nil); err != nil {
log.Error().Err(err).Msg("pprof server stopped")
}
}()
openai_logger := log.With().Logger()
err = llm.CreateOpenAIClient(ctx, &openai_logger)
if err != nil {
log.Error().Err(err).Msg("Failed to start openAI client")
os.Exit(8)
}
server := &http.Server{
Addr: config.Bind,
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
}
if config.IsProductionEnvironment() {
listeners, _ := activation.Listeners()
if len(listeners) != 1 {
log.Error().Int("len", len(listeners)).Msg("Unexpected number of socket activation FDs")
os.Exit(1)
}
go func() {
log.Info().Str("address", config.Bind).Msg("Serving HTTP requests")
if err := server.Serve(listeners[0]); err != nil && err != http.ErrServerClosed {
log.Error().Str("err", err.Error()).Msg("HTTP Server Error")
}
log.Debug().Msg("Exiting listen-and-serve goroutine")
}()
} else {
go func() {
log.Info().Str("address", config.Bind).Msg("Serving HTTP requests")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error().Str("err", err.Error()).Msg("HTTP Server Error")
}
log.Debug().Msg("Exiting listen-and-serve goroutine")
}()
}
chan_envelope := make(chan platform.Envelope, 10)
platform.SetEventChannel(chan_envelope)
api.SetEventChannel(chan_envelope)
// Wait for the interrupt signal to gracefully shut down
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
<-signalCh
log.Info().Msg("Received shutdown signal, shutting down...")
// Ensure logs are flushed
err = os.Stderr.Sync()
if err != nil {
log.Error().Err(err).Msg("stderr sync")
}
platform.EventShutdown()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Error().Err(err).Msg("Server didn't shutdown cleanly")
}
cancel()
close(chan_envelope)
platform.WaitForExit()
log.Info().Msg("Shutdown complete")
err = os.Stderr.Sync()
if err != nil {
panic("can't sync stderr")
}
}
func LoggerMiddleware(logger *zerolog.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
log := logger.With().Logger()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
t2 := time.Now()
// Recover and record stack traces in case of a panic
if rec := recover(); rec != nil {
log.Error().
Str("type", "error").
Timestamp().
Interface("recover_info", rec).
Bytes("debug_stack", debug.Stack()).
Msg("log system error")
fmt.Println("Stack:", string(debug.Stack()))
http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
remote_addr := r.RemoteAddr
forwarded_for := r.Header.Get("X-Forwarded-For")
if forwarded_for != "" {
remote_addr = forwarded_for
}
// log end request
if os.Getenv("VERBOSE") != "" {
log.Info().
//Str("type", "access").
Timestamp().
Fields(map[string]interface{}{
"remote_ip": remote_addr,
"url": r.URL.Path,
//"proto": r.Proto,
"method": r.Method,
//"user_agent": r.Header.Get("User-Agent"),
"status": ww.Status(),
"latency_ms": float64(t2.Sub(t1).Nanoseconds()) / 1000000.0,
"bytes_in": r.Header.Get("Content-Length"),
"bytes_out": ww.BytesWritten(),
}).
Msg("incoming_request")
}
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
}
func GetVersionInfo() (version.VersionInfo, error) {
// Try ldflags first (set by Nix build)
if Revision != "" && Revision != "unknown" {
build_time, err := parseNixTime(BuildTime)
if err != nil {
return version.VersionInfo{}, fmt.Errorf("parse nix time: %w", err)
}
return version.VersionInfo{
BuildTime: build_time,
IsModified: Revision == "dirty",
Revision: Revision,
}, nil
}
// Fallback to debug.ReadBuildInfo() for development
info, ok := debug.ReadBuildInfo()
if !ok {
return version.VersionInfo{}, fmt.Errorf("read build info not ok")
}
var v version.VersionInfo
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.modified":
v.IsModified = setting.Value == "true"
case "vcs.revision":
if len(setting.Value) > 7 {
v.Revision = setting.Value[:7]
} else {
v.Revision = setting.Value
}
case "vcs.time":
if t, err := time.Parse(time.RFC3339, setting.Value); err == nil {
v.BuildTime = t
}
}
}
return v, nil
}
func parseNixTime(timestamp string) (time.Time, error) {
layout := "20060102150405"
t, err := time.Parse(layout, timestamp)
if err != nil {
return time.Time{}, err
}
return t, nil
}