package handlers import ( "encoding/json" "net/http" "slices" ) // HealthResponse is the healthcheck body. It is a typed struct rather // than a map so that the shape is part of the code and a change to it // shows up in a diff: something is always parsing this. type HealthResponse struct { OK bool `json:"ok"` App string `json:"app"` Version string `json:"version"` SchemaVersion int `json:"schema_version"` DatabaseOK bool `json:"database_ok"` SentryEnabled bool `json:"sentry_enabled"` MetricsProtected bool `json:"metrics_protected"` } // Healthcheck answers with the process's own view of whether it is // working. It touches the database on purpose: a health endpoint that // only proves the HTTP server is up will report healthy through the // entire outage that matters. // // A failure answers 503, not 200-with-ok-false. Everything that reads // this — a load balancer, a container runtime, a monitoring probe — // looks at the status code first, and several look at nothing else. func (h *Handlers) Healthcheck() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { resp := HealthResponse{ App: h.params.Globals.Appname, Version: h.params.Globals.Version, SentryEnabled: h.params.Sentry.Enabled(), MetricsProtected: h.params.Config.MetricsUser != "", } versions, err := h.params.Database.AppliedVersions(r.Context()) if err == nil { resp.DatabaseOK = true resp.OK = true if len(versions) > 0 { resp.SchemaVersion = slices.Max(versions) } } else { h.log.Error("healthcheck: database unreachable", "error", err) } status := http.StatusOK if !resp.OK { status = http.StatusServiceUnavailable } w.Header().Set("Content-Type", "application/json; charset=utf-8") // A cached healthcheck is a healthcheck that reports the past. w.Header().Set("Cache-Control", "no-store") w.WriteHeader(status) encodeErr := json.NewEncoder(w).Encode(resp) if encodeErr != nil { // The status and headers are already sent, so there is // nothing to answer with; the log is the only record left. h.log.Error("healthcheck: encoding response failed", "error", encodeErr) } } } // Panic is a route that panics, mounted only when DEBUG is on. It is // how the panic recoverer is exercised by hand in a running process; // the automated proof is in the middleware tests. func (h *Handlers) Panic() http.HandlerFunc { return func(_ http.ResponseWriter, _ *http.Request) { panic("deliberate panic from the debug route") } }