// Package httpserver is the shared HTTP scaffolding used by every // service. It wires /health and /metrics and applies a graceful // shutdown to whatever handlers the caller passes in. package httpserver import ( "context" "errors" "log/slog" "net/http" "time" "git3.techno-world.net/lrosales/broad-announce/internal/observability" ) // Server bundles the http.Server with its config and the prom registry // the caller wants to expose on /metrics. type Server struct { cfg Config srv *http.Server logger *slog.Logger registry http.Handler } // Config is the bits the caller can change. type Config struct { Addr string ServiceName string ShutdownGrace time.Duration ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration MaxHeaderBytes int } // New constructs a Server with the given ServiceName and a custom mux // (the caller is expected to add /v1/* handlers; /health and /metrics // are added automatically). func New(cfg Config, logger *slog.Logger, reg http.Handler) *Server { if cfg.ReadTimeout == 0 { cfg.ReadTimeout = 10 * time.Second } if cfg.WriteTimeout == 0 { cfg.WriteTimeout = 30 * time.Second } if cfg.IdleTimeout == 0 { cfg.IdleTimeout = 60 * time.Second } if cfg.MaxHeaderBytes == 0 { cfg.MaxHeaderBytes = 1 << 20 // 1 MB } mux := http.NewServeMux() mux.Handle("/health", observability.HealthHandler(cfg.ServiceName)) mux.Handle("/metrics", reg) return &Server{ cfg: cfg, logger: logger, registry: reg, srv: &http.Server{ Addr: cfg.Addr, Handler: mux, ReadTimeout: cfg.ReadTimeout, WriteTimeout: cfg.WriteTimeout, IdleTimeout: cfg.IdleTimeout, MaxHeaderBytes: cfg.MaxHeaderBytes, }, } } // Mux returns the underlying mux so the caller can add /v1/* routes. func (s *Server) Mux() *http.ServeMux { return s.srv.Handler.(*http.ServeMux) } // Start runs ListenAndServe. Returns when the server stops. func (s *Server) Start() error { s.logger.Info("http listening", "addr", s.cfg.Addr) if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil } // Shutdown gracefully stops the server. func (s *Server) Shutdown(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, s.cfg.ShutdownGrace) defer cancel() return s.srv.Shutdown(ctx) }