From c83ee1d24e3a6882be63c0b33aa4b0e9130221bd Mon Sep 17 00:00:00 2001 From: Jordan Krage Date: Wed, 11 Feb 2026 13:34:26 -0600 Subject: [PATCH] pkg/loop: replace PromServer with webServer, including pprof support --- pkg/loop/config.go | 2 +- pkg/loop/prom.go | 3 ++ pkg/loop/server.go | 8 ++-- pkg/loop/web.go | 101 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 pkg/loop/web.go diff --git a/pkg/loop/config.go b/pkg/loop/config.go index d59672e46a..19e1ab7f10 100644 --- a/pkg/loop/config.go +++ b/pkg/loop/config.go @@ -115,7 +115,7 @@ type EnvConfig struct { MercuryTransmitterReaperMaxAge time.Duration MercuryVerboseLogging bool - PrometheusPort int + PrometheusPort int //TODO more than just prom TracingEnabled bool TracingCollectorTarget string diff --git a/pkg/loop/prom.go b/pkg/loop/prom.go index b7fc7f4da5..789c2803cb 100644 --- a/pkg/loop/prom.go +++ b/pkg/loop/prom.go @@ -14,6 +14,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" ) +// Deprecated: Use WebServer which includes pprof routes type PromServer struct { port int srvrDone chan struct{} // closed when the http server is done @@ -24,10 +25,12 @@ type PromServer struct { handler http.Handler } +// Deprecated: Use WebServerOpts which includes pprof routes type PromServerOpts struct { Handler http.Handler } +// Deprecated: Use NewWebServer which includes pprof routes func NewPromServer(port int, lggr logger.Logger) *PromServer { return PromServerOpts{}.New(port, lggr) } diff --git a/pkg/loop/server.go b/pkg/loop/server.go index 1d1dfb6fa1..ae61c9d6c4 100644 --- a/pkg/loop/server.go +++ b/pkg/loop/server.go @@ -91,7 +91,7 @@ type Server struct { db *sqlx.DB // optional dbStatsReporter *pg.StatsReporter // optional DataSource sqlutil.DataSource // optional - promServer *PromServer + webServer *webServer checker *services.HealthChecker LimitsFactory limits.Factory } @@ -221,8 +221,8 @@ func (s *Server) start(opts ...ServerOpt) error { } } - s.promServer = NewPromServer(s.EnvConfig.PrometheusPort, s.Logger) - if err := s.promServer.Start(); err != nil { + s.webServer = WebServerOpts{}.New(s.Logger, s.EnvConfig.PrometheusPort) + if err := s.webServer.Start(ctx); err != nil { return fmt.Errorf("error starting prometheus server: %w", err) } @@ -290,7 +290,7 @@ func (s *Server) Stop() { s.Logger.ErrorIfFn(s.db.Close, "Failed to close database connection") } s.Logger.ErrorIfFn(s.checker.Close, "Failed to close health checker") - s.Logger.ErrorIfFn(s.promServer.Close, "Failed to close prometheus server") + s.Logger.ErrorIfFn(s.webServer.Close, "Failed to close web server") if err := s.Logger.Sync(); err != nil { fmt.Println("Failed to sync logger:", err) } diff --git a/pkg/loop/web.go b/pkg/loop/web.go new file mode 100644 index 0000000000..fca9824e46 --- /dev/null +++ b/pkg/loop/web.go @@ -0,0 +1,101 @@ +package loop + +import ( + "context" + "errors" + "net" + "net/http" + _ "net/http/pprof" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// webServer serves web routes including +// - /metrics prometheus metrics +// - /debug/pprof/ debug profiling +type webServer struct { + lggr logger.Logger + port int + handler http.Handler + + srvr *http.Server + tcpListener *net.TCPListener + + done chan struct{} +} + +type WebServerOpts struct { + Handler http.Handler +} + +func (o WebServerOpts) New(lggr logger.Logger, port int) *webServer { + s := &webServer{ + lggr: logger.Named(lggr, "WebServer"), + port: port, + handler: o.Handler, + srvr: &http.Server{ + // reasonable default based on typical prom poll interval of 15s. + ReadTimeout: 5 * time.Second, + }, + done: make(chan struct{}), + } + if s.handler == nil { + s.handler = promhttp.HandlerFor( + prometheus.DefaultGatherer, + promhttp.HandlerOpts{ + EnableOpenMetrics: true, + }, + ) + } + return s +} + +// setupListener creates an explicit listener so that we can resolve `:0` port, which is needed for testing +// if we didn't need the resolved addr, or could pick a static port we could use p.srvr.ListenAndServer +func (w *webServer) setupListener() error { + l, err := net.ListenTCP("tcp", &net.TCPAddr{ + Port: w.port, + }) + if err != nil { + return err + } + + w.tcpListener = l + return nil +} + +func (w *webServer) Start(ctx context.Context) error { + err := w.setupListener() + if err != nil { + return err + } + + http.Handle("/metrics", w.handler) + + // pprof handler registered via import side effects + + go func() { + defer close(w.done) + err := w.srvr.Serve(w.tcpListener) + if !errors.Is(err, http.ErrServerClosed) { + w.lggr.Errorw("Unexpected server error", "err", err) + } + }() + return nil +} + +func (w *webServer) Close() error { + err := w.srvr.Shutdown(context.Background()) + <-w.done + return err +} + +func (w *webServer) Ready() error { return nil } + +func (w *webServer) HealthReport() map[string]error { return map[string]error{w.Name(): nil} } + +func (w *webServer) Name() string { return w.lggr.Name() }