Files
sendico/api/fx/ingestor/internal/metrics/server.go
Stephan D 62a6631b9a
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
service backend
2025-11-07 18:35:26 +01:00

135 lines
3.1 KiB
Go

package metrics
import (
"context"
"errors"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/fx/ingestor/internal/config"
"github.com/tech/sendico/fx/ingestor/internal/fmerrors"
"github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/api/routers/health"
"github.com/tech/sendico/pkg/mlogger"
"github.com/prometheus/client_golang/prometheus/promhttp"
"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, fmerrors.New("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) {}