// 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)
}