// Package healthcheck implementiert INT-07: den Health-Check-Endpunkt // für das Mail-Modul (Erreichbarkeit von Datenbank, Objektspeicher, // Suchindex und Jobqueue, getrennt gemeldet). // // Core API-01 (REST-Grundgerüst) hat im aktuellen Repository-Stand // keinen abrufbaren Router (gleiche Situation wie bei ARC-06/Core // TEN-01 und mail/internal/mailapi, INT-01) — RegisterRoutes // registriert den Endpunkt deshalb auf einem vom Aufrufer // bereitgestellten *http.ServeMux mit demselben Pfadschema // "/api/v1/mail/..." wie mailapi. package healthcheck import ( "context" "encoding/json" "net/http" "sync" ) // Status-Werte (Akzeptanzkriterium 3: "degraded" statt generischem // Fehler). const ( StatusOK = "ok" StatusDegraded = "degraded" ) // CheckFunc prüft EINE Abhängigkeit. Ein Fehler bedeutet "nicht // erreichbar" — der Fehlertext selbst landet NIE in der HTTP-Antwort // (Akzeptanzkriterium 2: keine sensiblen Konfigurationsdetails), // höchstens im Server-Log des Aufrufers. type CheckFunc func(ctx context.Context) error // namedCheck bindet einen Komponentennamen an seine Prüffunktion, in // registrierter Reihenfolge (deterministische Antwortreihenfolge). type namedCheck struct { name string fn CheckFunc } // Checker sammelt benannte Abhängigkeitsprüfungen. type Checker struct { mu sync.Mutex checks []namedCheck } func NewChecker() *Checker { return &Checker{} } // Register fügt eine benannte Prüfung hinzu (Akzeptanzkriterium 1: je // Komponente getrennt gemeldet). func (c *Checker) Register(name string, fn CheckFunc) { c.mu.Lock() defer c.mu.Unlock() c.checks = append(c.checks, namedCheck{name: name, fn: fn}) } // ComponentStatus ist der Status EINER geprüften Abhängigkeit — ohne // Fehlertext (Akzeptanzkriterium 2). type ComponentStatus struct { Name string `json:"name"` Status string `json:"status"` } // Result ist die vollständige Health-Antwort. type Result struct { Status string `json:"status"` Components []ComponentStatus `json:"components"` } // Check führt alle registrierten Prüfungen aus (Akzeptanzkriterium 1: // getrennt je Komponente). Gesamtstatus ist "degraded", sobald // MINDESTENS eine Komponente fehlschlägt (Akzeptanzkriterium 3). func (c *Checker) Check(ctx context.Context) Result { c.mu.Lock() checks := make([]namedCheck, len(c.checks)) copy(checks, c.checks) c.mu.Unlock() result := Result{Status: StatusOK, Components: make([]ComponentStatus, 0, len(checks))} for _, nc := range checks { status := StatusOK if err := nc.fn(ctx); err != nil { status = StatusDegraded result.Status = StatusDegraded } result.Components = append(result.Components, ComponentStatus{Name: nc.name, Status: status}) } return result } // ServeHTTP liefert den Health-Status als JSON. Ohne Authentifizierung // erreichbar (Akzeptanzkriterium 2) — der Inhalt selbst enthält // ausschließlich Komponentenname + ok/degraded, nie Fehlertexte, // Verbindungszeichenfolgen oder sonstige Konfigurationsdetails. func (c *Checker) ServeHTTP(w http.ResponseWriter, r *http.Request) { result := c.Check(r.Context()) status := http.StatusOK if result.Status == StatusDegraded { status = http.StatusServiceUnavailable } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(result) } // RegisterRoutes registriert den Health-Endpunkt auf mux. func (c *Checker) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/v1/mail/health", c.ServeHTTP) }