package health import ( "context" "encoding/json" "errors" "net/http" "strings" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/metrics" ) type Check struct { Name string Check func(context.Context) error } type Handler struct { service string checks []Check } type Logger interface { Info(string, ...any) Error(string, ...any) } func NewHandler(service string, checks []Check) *Handler { service = strings.TrimSpace(service) if service == "" { service = "vehicle-service" } return &Handler{service: service, checks: checks} } func NewMux(service string, checks []Check, registry *metrics.Registry) *http.ServeMux { mux := http.NewServeMux() handler := NewHandler(service, checks) mux.Handle("/healthz", handler) mux.Handle("/readyz", handler) if registry != nil { mux.Handle("/metrics", metrics.NewHandler(registry)) } return mux } func NewServer(addr string, service string, checks []Check, registry *metrics.Registry) *http.Server { addr = strings.TrimSpace(addr) if addr == "" { return nil } return &http.Server{ Addr: addr, Handler: NewMux(service, checks, registry), ReadHeaderTimeout: 5 * time.Second, } } func Start(ctx context.Context, logger Logger, server *http.Server) { if server == nil { return } go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = server.Shutdown(shutdownCtx) }() go func() { if logger != nil { logger.Info("health server started", "addr", server.Addr) } if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) && logger != nil { logger.Error("health server failed", "error", err) } }() } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeHealth(w, http.StatusMethodNotAllowed, map[string]any{"error": "method not allowed"}) return } path := strings.Trim(r.URL.Path, "/") if path != "healthz" && path != "readyz" { writeHealth(w, http.StatusNotFound, map[string]any{"error": "route not found"}) return } if path == "healthz" { writeHealth(w, http.StatusOK, map[string]any{ "service": h.service, "status": "ok", "time": time.Now().UTC().Format(time.RFC3339), }) return } h.writeReadiness(w, r) } func (h *Handler) writeReadiness(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() status := "ok" code := http.StatusOK checks := make(map[string]any, len(h.checks)) for _, check := range h.checks { name := strings.TrimSpace(check.Name) if name == "" { continue } if check.Check == nil { checks[name] = map[string]any{"status": "ok"} continue } if err := check.Check(ctx); err != nil { status = "degraded" code = http.StatusServiceUnavailable checks[name] = map[string]any{"status": "error", "error": err.Error()} continue } checks[name] = map[string]any{"status": "ok"} } writeHealth(w, code, map[string]any{ "service": h.service, "status": status, "time": time.Now().UTC().Format(time.RFC3339), "checks": checks, }) } func writeHealth(w http.ResponseWriter, status int, payload map[string]any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) }