package metrics import ( "context" "errors" "net/http" "strings" "time" "github.com/go-chi/chi/v5" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/tech/sendico/fx/ingestor/internal/config" "github.com/tech/sendico/pkg/api/routers" "github.com/tech/sendico/pkg/api/routers/health" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "go.uber.org/zap" ) const ( defaultAddress = ":9102" readHeaderTimeout = 5 * time.Second defaultShutdownWindow = 5 * time.Second ) type Server interface { SetStatus(health.ServiceStatus) Close(context.Context) } func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error) { if logger == nil { return nil, merrors.InvalidArgument("metrics: logger is nil") } if cfg == nil || !cfg.Enabled { logger.Debug("Metrics disabled; using noop server") return noopServer{}, nil } address := strings.TrimSpace(cfg.Address) if address == "" { address = defaultAddress } metricsLogger := logger.Named("metrics") router := chi.NewRouter() router.Handle("/metrics", promhttp.Handler()) var healthRouter routers.Health if hr, err := routers.NewHealthRouter(metricsLogger, router, ""); err != nil { metricsLogger.Warn("Failed to initialise health router", zap.Error(err)) } else { hr.SetStatus(health.SSStarting) healthRouter = hr } httpServer := &http.Server{ Addr: address, Handler: router, ReadHeaderTimeout: readHeaderTimeout, } ms := &httpServerWrapper{ logger: metricsLogger, server: httpServer, health: healthRouter, timeout: defaultShutdownWindow, } go func() { metricsLogger.Info("Prometheus endpoint listening", zap.String("address", address)) if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { metricsLogger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err)) if healthRouter != nil { healthRouter.SetStatus(health.SSTerminating) } } }() return ms, nil } type httpServerWrapper struct { logger mlogger.Logger server *http.Server health routers.Health timeout time.Duration } func (s *httpServerWrapper) SetStatus(status health.ServiceStatus) { if s == nil || s.health == nil { return } s.logger.Debug("Updating metrics health status", zap.String("status", string(status))) s.health.SetStatus(status) } func (s *httpServerWrapper) Close(ctx context.Context) { if s == nil { return } if s.health != nil { s.health.SetStatus(health.SSTerminating) s.health.Finish() s.health = nil } if s.server == nil { return } shutdownCtx := ctx if shutdownCtx == nil { shutdownCtx = context.Background() } if s.timeout > 0 { var cancel context.CancelFunc shutdownCtx, cancel = context.WithTimeout(shutdownCtx, s.timeout) defer cancel() } if err := s.server.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) { s.logger.Warn("Failed to stop metrics server", zap.Error(err)) } else { s.logger.Info("Metrics server stopped") } } type noopServer struct{} func (noopServer) SetStatus(health.ServiceStatus) {} func (noopServer) Close(context.Context) {}