package app import ( "context" "errors" "fmt" "log/slog" "net/http" "time" "go.bigb.es/auxilia/culpa" "go.bigb.es/auxilia/scribe" "sourcecraft.dev/bigbes/huntsman/internal/config" "sourcecraft.dev/bigbes/huntsman/internal/domain/search" "sourcecraft.dev/bigbes/huntsman/internal/platform/health" "sourcecraft.dev/bigbes/huntsman/internal/platform/observability" "sourcecraft.dev/bigbes/huntsman/internal/server" ) // loggerWire builds the application's structured logger from config and // installs it as the slog default so package-level callers pick it up. type loggerWire struct { Config config.Config `config:""` Log *slog.Logger } func (w *loggerWire) Init(_ context.Context) error { w.Log = observability.NewLogger(w.Config.Log.Level, w.Config.Log.Format) slog.SetDefault(w.Log) return nil } // searchWire constructs the search service and HTTP handler. Both are // kept on the wire so HTTP can inject the handler and other services // can call into the service if needed later. type searchWire struct { Config config.Config `config:""` Service *search.Service Handler *search.Handler } func (w *searchWire) Init(_ context.Context) error { svc, err := search.NewService(w.Config.Search.DefaultProvider, w.Config.Server.PublicURL) if err != nil { return culpa.Wrap(err, "build search service") } w.Service = svc w.Handler = search.NewHandler(svc) return nil } // healthWire wraps the health registry so steward owns its lifetime. // Add upstream probes by calling Health.Register from another service's // Init via inject:"". type healthWire struct { Health *health.Health } func (w *healthWire) Init(_ context.Context) error { w.Health = health.New() return nil } // shutdownTimeout is how long http.Server.Shutdown is allowed to drain // in-flight requests before returning. const shutdownTimeout = 30 * time.Second // httpServer is the root steward service. It depends on logger, search, // and health wires (resolved by type). Init builds the chi router; Start // runs the listener in a goroutine; Stop drains via Shutdown. type httpServer struct { Config config.Config `config:""` Logger *loggerWire `inject:""` Search *searchWire `inject:""` Health *healthWire `inject:""` Info BuildInfo srv *http.Server } func (s *httpServer) Init(_ context.Context) error { router := server.Routes(s.Logger.Log, s.Health.Health, s.Search.Handler) addr := fmt.Sprintf("%s:%d", s.Config.Server.Host, s.Config.Server.Port) s.srv = &http.Server{ Addr: addr, Handler: router, ReadTimeout: s.Config.Server.ReadTimeout, WriteTimeout: s.Config.Server.WriteTimeout, IdleTimeout: 60 * time.Second, } return nil } func (s *httpServer) Start(_ context.Context) error { go func() { s.Logger.Log.Info("server starting", "addr", s.srv.Addr, "version", s.Info.Version, ) if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { s.Logger.Log.Error("server stopped unexpectedly", scribe.Err(culpa.Wrap(err, "ListenAndServe")), ) } }() return nil } func (s *httpServer) Stop(_ context.Context) error { s.Logger.Log.Info("shutting down gracefully") shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() if err := s.srv.Shutdown(shutdownCtx); err != nil { return culpa.Wrap(err, "http shutdown") } return nil }