Neues Paket mail/internal/healthcheck: Checker sammelt benannte CheckFunc-Prüfungen, liefert Gesamt- und Einzelstatus je Komponente (ok/degraded, nie ein generischer Fehler). Fehlertexte einzelner Prüfungen fließen nie in die HTTP-Antwort — nur Name+Status je Komponente. Vier konkrete Prüfungen (checks.go) gegen die real vorhandenen Ticket-Abhängigkeiten: DatabaseCheck (pgxpool.Ping), ObjectStorageCheck (HeadBucket gegen den ARC-06-Bucket), SearchIndexCheck (reale Manticore-Anfrage), JobQueueCheck (COUNT gegen mail_index_jobs, SRC-02/indexworker — COUNT statt Zeilenzugriff, damit eine leere aber erreichbare Queue nicht fälschlich als Ausfall gilt). RegisterRoutes registriert GET /api/v1/mail/health ohne Authentifizierung auf einem vom Aufrufer bereitgestellten *http.ServeMux, gleiches Pfadschema wie mailapi (INT-01) — Core API-01 hat weiterhin keinen abrufbaren Router. Alle drei Pflichtprüfungen mit echten Nachweisen: simulierter Ausfall einer von vier Abhängigkeiten korrekt als degraded abgebildet, eine Prüfung mit absichtlich eingebetteter Verbindungszeichenfolge inkl. Passwort im Fehlertext taucht nirgends in der HTTP-Antwort auf, echter httptest-HTTP-Server-Integrationstest gegen den Endpunkt. Alle vier konkreten Prüfungen zusätzlich real gegen laufende Postgres-, MinIO- und Manticore-Instanzen verifiziert (inkl. echter ARC-06-Provisionierung). go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
111 lines
3.5 KiB
Go
111 lines
3.5 KiB
Go
// 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)
|
|
}
|