// Package health provides a standalone HTTP server for health checks. // // This package implements a simple health check server that can be used // to expose health status endpoints for monitoring and load balancing. // It supports custom health check handlers and provides structured logging // with graceful shutdown capabilities. package health import ( "context" "log/slog" "net/http" "strconv" "time" "go.ntppool.org/common/logger" "golang.org/x/sync/errgroup" ) // Server is a standalone HTTP server dedicated to health checks. // It runs separately from the main application server to ensure health // checks remain available even if the main server is experiencing issues. // // The server includes built-in timeouts, graceful shutdown, and structured // logging for monitoring and debugging health check behavior. type Server struct { log *slog.Logger healthFn http.HandlerFunc } // NewServer creates a new health check server with the specified health handler. // If healthFn is nil, a default handler that returns HTTP 200 "ok" is used. func NewServer(healthFn http.HandlerFunc) *Server { if healthFn == nil { healthFn = basicHealth } srv := &Server{ log: logger.Setup(), healthFn: healthFn, } return srv } // SetLogger replaces the default logger with a custom one. func (srv *Server) SetLogger(log *slog.Logger) { srv.log = log } // Listen starts the health server on the specified port and blocks until ctx is cancelled. // The server exposes the health handler at "/__health" with graceful shutdown support. func (srv *Server) Listen(ctx context.Context, port int) error { srv.log.Info("starting health listener", "port", port) serveMux := http.NewServeMux() serveMux.HandleFunc("/__health", srv.healthFn) hsrv := &http.Server{ Addr: ":" + strconv.Itoa(port), ReadTimeout: 10 * time.Second, WriteTimeout: 20 * time.Second, IdleTimeout: 120 * time.Second, Handler: serveMux, } g, ctx := errgroup.WithContext(ctx) g.Go(func() error { err := hsrv.ListenAndServe() if err != http.ErrServerClosed { srv.log.Warn("health check server done listening", "err", err) return err } return nil }) <-ctx.Done() g.Go(func() error { shCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := hsrv.Shutdown(shCtx); err != nil { srv.log.Error("health check server shutdown failed", "err", err) return err } return nil }) return g.Wait() } // HealthCheckListener runs a simple HTTP server on the specified port for health check probes. func HealthCheckListener(ctx context.Context, port int, log *slog.Logger) error { srv := NewServer(nil) srv.SetLogger(log) return srv.Listen(ctx, port) } func basicHealth(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Write([]byte("ok")) }