internal/server/health/health.go
Ref: Size: 2.6 KiB History
// Package health serves the eitri-server liveness and readiness probes.
//
// The split follows the Kubernetes convention: /livez answers "is the process
// alive" (restart me if not) and must never depend on an external service,
// while /readyz answers "should I receive traffic" (route around me until I say
// yes) and runs the dependency checks. Both are unauthenticated and mounted
// outside /api/ so a load balancer or the deploy script can probe them without
// a token.
//
// The package holds no eitri dependencies: callers pass dependency probes as
// Check closures, so this stays a leaf that main wires against the concrete
// store.
package health
import (
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"time"
)
// Check is one named readiness dependency probe. Probe returns nil when the
// dependency is reachable; any error marks it unavailable.
type Check struct {
Name string
Probe func(ctx context.Context) error
}
// Live handles GET /livez: 200 for as long as the process can serve HTTP. It
// runs no dependency checks by design — a liveness probe that failed because a
// dependency was briefly slow or down would trigger a needless restart, taking
// out a server that was merely waiting on someone else.
func Live(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, "ok\n")
}
// Ready returns a handler for GET /readyz. It runs every check under a context
// bounded by timeout and reports 200 {"status":"ready"} when all pass, or 503
// {"status":"unready"} when any fails. The JSON body names each check so an
// operator sees WHICH dependency is unready ("ok" | "unavailable"); the raw
// probe error is logged, never returned — this endpoint is unauthenticated and
// a probe error can carry internal detail (socket paths, driver messages).
func Ready(timeout time.Duration, checks ...Check) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
results := make(map[string]string, len(checks))
ready := true
for _, c := range checks {
if err := c.Probe(ctx); err != nil {
results[c.Name] = "unavailable"
ready = false
slog.Warn("readiness check failed", "check", c.Name, "err", err)
} else {
results[c.Name] = "ok"
}
}
status, label := http.StatusOK, "ready"
if !ready {
status, label = http.StatusServiceUnavailable, "unready"
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{"status": label, "checks": results})
}
}