// Package metricsapi hosts the optional Prometheus scrape listener. // // The listener is enabled only when BACKEND_OTEL_METRICS_EXPORTER=prometheus // and the configured listen address is non-empty. main.go wires this server // into the application lifecycle only when Enabled returns true. package metricsapi import ( "context" "errors" "fmt" "net" "net/http" "sync" "go.uber.org/zap" ) // Server owns the optional Prometheus HTTP listener. type Server struct { addr string handler http.Handler logger *zap.Logger stateMu sync.RWMutex server *http.Server listener net.Listener } // NewServer constructs a Prometheus scrape server bound to addr. A handler of // nil is replaced with http.NotFoundHandler so the server can still serve // 404s in unconfigured deployments. func NewServer(addr string, handler http.Handler, logger *zap.Logger) *Server { if handler == nil { handler = http.NotFoundHandler() } if logger == nil { logger = zap.NewNop() } return &Server{ addr: addr, handler: handler, logger: logger.Named("metricsapi"), } } // Enabled reports whether the metrics listener should run. func (s *Server) Enabled() bool { return s != nil && s.addr != "" } // Run binds the listener and serves the scrape surface. A disabled server // blocks until ctx is cancelled so the App lifecycle can still treat it as a // regular Component. func (s *Server) Run(ctx context.Context) error { if ctx == nil { return errors.New("run backend metrics server: nil context") } if err := ctx.Err(); err != nil { return err } if !s.Enabled() { <-ctx.Done() return nil } listener, err := net.Listen("tcp", s.addr) if err != nil { return fmt.Errorf("run backend metrics server: listen on %q: %w", s.addr, err) } server := &http.Server{ Handler: s.handler, } s.stateMu.Lock() s.server = server s.listener = listener s.stateMu.Unlock() s.logger.Info("backend metrics server started", zap.String("addr", listener.Addr().String())) defer func() { s.stateMu.Lock() s.server = nil s.listener = nil s.stateMu.Unlock() }() err = server.Serve(listener) switch { case err == nil: return nil case errors.Is(err, http.ErrServerClosed): s.logger.Info("backend metrics server stopped") return nil default: return fmt.Errorf("run backend metrics server: serve on %q: %w", s.addr, err) } } // Shutdown gracefully stops the metrics listener within ctx. func (s *Server) Shutdown(ctx context.Context) error { if ctx == nil { return errors.New("shutdown backend metrics server: nil context") } s.stateMu.RLock() server := s.server s.stateMu.RUnlock() if server == nil { return nil } if err := server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("shutdown backend metrics server: %w", err) } return nil }