// Package adminapi exposes the optional private admin HTTP listener used for // operational endpoints such as Prometheus metrics. package adminapi import ( "context" "errors" "fmt" "log/slog" "net" "net/http" "sync" "galaxy/user/internal/config" ) // Server owns the optional admin HTTP listener exposed by the user service. type Server struct { cfg config.AdminHTTPConfig handler http.Handler logger *slog.Logger stateMu sync.RWMutex server *http.Server listener net.Listener } // NewServer constructs an admin HTTP server for cfg and handler. func NewServer(cfg config.AdminHTTPConfig, handler http.Handler, logger *slog.Logger) *Server { if handler == nil { handler = http.NotFoundHandler() } if logger == nil { logger = slog.Default() } mux := http.NewServeMux() mux.Handle("GET /metrics", handler) return &Server{ cfg: cfg, handler: mux, logger: logger.With("component", "admin_http"), } } // Enabled reports whether the admin listener should run. func (server *Server) Enabled() bool { return server != nil && server.cfg.Addr != "" } // Run binds the configured listener and serves the admin HTTP surface until // Shutdown closes the server. A disabled admin server returns when ctx is // canceled. func (server *Server) Run(ctx context.Context) error { if ctx == nil { return errors.New("run admin HTTP server: nil context") } if err := ctx.Err(); err != nil { return err } if !server.Enabled() { <-ctx.Done() return nil } listener, err := net.Listen("tcp", server.cfg.Addr) if err != nil { return fmt.Errorf("run admin HTTP server: listen on %q: %w", server.cfg.Addr, err) } httpServer := &http.Server{ Handler: server.handler, ReadHeaderTimeout: server.cfg.ReadHeaderTimeout, ReadTimeout: server.cfg.ReadTimeout, IdleTimeout: server.cfg.IdleTimeout, } server.stateMu.Lock() server.server = httpServer server.listener = listener server.stateMu.Unlock() server.logger.Info("admin HTTP server started", "addr", listener.Addr().String()) shutdownDone := make(chan struct{}) go func() { defer close(shutdownDone) <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), server.cfg.ReadTimeout) defer cancel() _ = server.Shutdown(shutdownCtx) }() defer func() { server.stateMu.Lock() server.server = nil server.listener = nil server.stateMu.Unlock() <-shutdownDone }() err = httpServer.Serve(listener) switch { case err == nil: return nil case errors.Is(err, http.ErrServerClosed): server.logger.Info("admin HTTP server stopped") return nil default: return fmt.Errorf("run admin HTTP server: serve on %q: %w", server.cfg.Addr, err) } } // Shutdown gracefully stops the admin HTTP server within ctx. func (server *Server) Shutdown(ctx context.Context) error { if ctx == nil { return errors.New("shutdown admin HTTP server: nil context") } server.stateMu.RLock() httpServer := server.server server.stateMu.RUnlock() if httpServer == nil { return nil } if err := httpServer.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("shutdown admin HTTP server: %w", err) } return nil }