// Package metricsserver provides a standalone HTTP server for exposing Prometheus metrics. // // This package implements a dedicated metrics server that exposes application metrics // via HTTP. It uses a custom Prometheus registry to avoid conflicts with other metric // collectors and provides graceful shutdown capabilities. // // # Usage // // Create a new metrics server and register your metrics with its Registry(): // // m := metricsserver.New() // myCounter := prometheus.NewCounter(...) // m.Registry().MustRegister(myCounter) // // When you need a Gatherer (for example, to pass to other libraries), use the Gatherer() method // instead of prometheus.DefaultGatherer to ensure your custom metrics are collected: // // gatherer := m.Gatherer() // Returns the custom registry as a Gatherer // // To use the default Prometheus gatherer alongside your custom registry: // // m := metricsserver.NewWithDefaultGatherer() // m.Gatherer() // Returns prometheus.DefaultGatherer package metricsserver import ( "context" "net/http" "strconv" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/sync/errgroup" "go.ntppool.org/common/logger" ) // Metrics provides a custom Prometheus registry and HTTP handlers for metrics exposure. // It isolates application metrics from the default global registry. type Metrics struct { r *prometheus.Registry useDefaultGatherer bool } // New creates a new Metrics instance with a custom Prometheus registry. // Use this when you want isolated metrics that don't interfere with the default registry. func New() *Metrics { r := prometheus.NewRegistry() m := &Metrics{ r: r, useDefaultGatherer: false, } return m } // NewWithDefaultGatherer creates a new Metrics instance that uses the default Prometheus gatherer. // This is useful when you want to expose metrics from the default registry alongside your custom metrics. // The custom registry will still be available via Registry() but Gatherer() will return prometheus.DefaultGatherer. func NewWithDefaultGatherer() *Metrics { r := prometheus.NewRegistry() m := &Metrics{ r: r, useDefaultGatherer: true, } return m } // Registry returns the custom Prometheus registry. // Use this to register your application's metrics collectors. func (m *Metrics) Registry() *prometheus.Registry { return m.r } // Gatherer returns the Prometheus gatherer to use for collecting metrics. // This returns the custom registry's Gatherer by default, ensuring your registered // metrics are collected. If the instance was created with NewWithDefaultGatherer(), // this returns prometheus.DefaultGatherer instead. // // Use this method when you need a prometheus.Gatherer interface, for example when // configuring other libraries that need to collect metrics. // // IMPORTANT: Do not use prometheus.DefaultGatherer directly if you want to collect // metrics registered with this instance's Registry(). Always use this Gatherer() method. func (m *Metrics) Gatherer() prometheus.Gatherer { if m.useDefaultGatherer { return prometheus.DefaultGatherer } return m.r } // Handler returns an HTTP handler for the /metrics endpoint with OpenMetrics support. func (m *Metrics) Handler() http.Handler { log := logger.NewStdLog("prom http", false, nil) return promhttp.HandlerFor(m.r, promhttp.HandlerOpts{ ErrorLog: log, Registry: m.r, EnableOpenMetrics: true, }) } // ListenAndServe starts a metrics server on the specified port and blocks until ctx is done. // The server exposes the metrics handler and shuts down gracefully when the context is cancelled. func (m *Metrics) ListenAndServe(ctx context.Context, port int) error { log := logger.Setup() srv := &http.Server{ Addr: ":" + strconv.Itoa(port), ReadTimeout: 10 * time.Second, WriteTimeout: 20 * time.Second, IdleTimeout: 120 * time.Second, Handler: m.Handler(), } g, ctx := errgroup.WithContext(ctx) g.Go(func() error { err := srv.ListenAndServe() if err != nil && err != http.ErrServerClosed { log.Warn("metrics server done listening", "err", err) return err } return nil }) <-ctx.Done() if err := srv.Shutdown(ctx); err != nil { log.Error("metrics server shutdown failed", "err", err) return err } return g.Wait() }