Files
galaxy-game/backend/internal/metricsapi/server.go
T
2026-05-06 10:14:55 +03:00

122 lines
2.8 KiB
Go

// 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
}