package http import ( "context" "net/http" "strings" "sync" "time" ) const readinessCheckTimeout = 2 * time.Second type readinessCheck struct { name string check func(context.Context) error } type Readiness struct { mu sync.Mutex checks []readinessCheck started bool stopping bool } func NewReadiness() *Readiness { return &Readiness{} } func (r *Readiness) Register(name string, check func(context.Context) error) { r.mu.Lock() defer r.mu.Unlock() r.checks = append(r.checks, readinessCheck{name: name, check: check}) } func (r *Readiness) MarkStarted() { r.mu.Lock() defer r.mu.Unlock() r.started = true } func (r *Readiness) MarkStopping() { r.mu.Lock() defer r.mu.Unlock() r.stopping = true } func (r *Readiness) Report(ctx context.Context) (bool, string) { r.mu.Lock() started, stopping := r.started, r.stopping checks := append([]readinessCheck(nil), r.checks...) r.mu.Unlock() switch { case stopping: return false, "shutting down\n" case !started: return false, "not started\n" } ctx, cancel := context.WithTimeout(ctx, readinessCheckTimeout) defer cancel() var failing strings.Builder for _, c := range checks { if err := c.check(ctx); err != nil { failing.WriteString(c.name + "\n") } } if failing.Len() > 0 { return false, failing.String() } return true, "ok\n" } func OpsHandler(rd *Readiness) http.Handler { return OpsHandlerWithMetrics(rd, nil) } func OpsHandlerWithMetrics(rd *Readiness, metrics http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Cache-Control", "no-store") if r.Method != http.MethodGet && r.Method != http.MethodHead { http.NotFound(w, r) return } switch r.URL.Path { case "/healthz": w.Write([]byte("ok\n")) case "/readyz": ready, body := rd.Report(r.Context()) if !ready { w.WriteHeader(http.StatusServiceUnavailable) } w.Write([]byte(body)) case "/metrics": if metrics == nil { http.NotFound(w, r) return } metrics.ServeHTTP(w, r) default: http.NotFound(w, r) } }) }