// Package health implements simple liveness and readiness probes. // // huntsman has no real backing services, so readiness == liveness // today, but the Health struct keeps the door open for plugging in real // checkers later (e.g. an upstream API ping) without touching the router. package health import ( "context" "encoding/json" "net/http" "sync" "time" ) // Checker reports the current health of one subsystem. type Checker interface { Check(ctx context.Context) error } // CheckerFunc adapts an ordinary function to the Checker interface. type CheckerFunc func(ctx context.Context) error // Check satisfies the Checker interface. func (f CheckerFunc) Check(ctx context.Context) error { return f(ctx) } // Health aggregates named checkers and exposes them as HTTP handlers. type Health struct { mu sync.RWMutex checkers map[string]Checker } // New constructs an empty Health registry. func New() *Health { return &Health{checkers: make(map[string]Checker)} } // Register adds (or replaces) a named checker. func (h *Health) Register(name string, c Checker) { h.mu.Lock() defer h.mu.Unlock() h.checkers[name] = c } // Healthz is a liveness probe — does the process exist and respond? func (h *Health) Healthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) } // Readyz runs all registered checkers with a 5s timeout. Any failure // flips the response to 503 and lists per-checker status in the body. func (h *Health) Readyz(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() h.mu.RLock() defer h.mu.RUnlock() results := make(map[string]string, len(h.checkers)) allOK := true for name, checker := range h.checkers { if err := checker.Check(ctx); err != nil { results[name] = err.Error() allOK = false } else { results[name] = "ok" } } w.Header().Set("Content-Type", "application/json") if allOK { w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusServiceUnavailable) } _ = json.NewEncoder(w).Encode(results) }