// Package health implementiert die Health Engine aus PROMPT.md §93/§94. // // Jede Komponente meldet einen expliziten Zustand; der Gesamtzustand ist immer // der schlechteste gemeldete Einzelzustand. Ein unbekannter oder nicht geprüfter // Zustand gilt niemals als gesund (Fail Secure). package health import ( "context" "sync" "time" ) // Status ist der Gesundheitszustand einer Komponente. type Status string const ( // StatusHealthy bedeutet: die Komponente arbeitet uneingeschränkt. StatusHealthy Status = "healthy" // StatusDegraded bedeutet: die Komponente arbeitet, aber eingeschränkt. StatusDegraded Status = "degraded" // StatusWarning bedeutet: ein Problem bahnt sich an und braucht Aufmerksamkeit. StatusWarning Status = "warning" // StatusCritical bedeutet: die Komponente ist funktionsunfähig. StatusCritical Status = "critical" // StatusOffline bedeutet: die Komponente ist nicht erreichbar. StatusOffline Status = "offline" ) // statusSeverity ordnet jedem Zustand eine Schwere zu. // Höhere Werte sind schlechter und bestimmen den Gesamtzustand. var statusSeverity = map[Status]int{ StatusHealthy: 0, StatusDegraded: 1, StatusWarning: 2, StatusOffline: 3, StatusCritical: 4, } // IsWorseThan meldet, ob dieser Zustand schwerwiegender ist als der verglichene. func (status Status) IsWorseThan(otherStatus Status) bool { return statusSeverity[status] > statusSeverity[otherStatus] } // CheckResult ist das Ergebnis einer einzelnen Komponentenprüfung. type CheckResult struct { // Status ist der ermittelte Zustand der Komponente. Status Status `json:"status"` // Message erklärt den Zustand verständlich (PROMPT.md §48: keine rohen Fehlercodes). Message string `json:"message,omitempty"` // RecommendedAction nennt den nächsten sinnvollen Schritt bei Problemen. RecommendedAction string `json:"recommended_action,omitempty"` // LatencyMilliseconds ist die Dauer der Prüfung. LatencyMilliseconds float64 `json:"latency_ms"` // CheckedAt ist der Zeitpunkt der Prüfung in UTC. CheckedAt time.Time `json:"checked_at"` } // SystemReport ist der Gesamtzustand des Dienstes. type SystemReport struct { // Status ist der schlechteste Zustand aller geprüften Komponenten. Status Status `json:"status"` // Components enthält das Ergebnis je Komponente. Components map[string]CheckResult `json:"components"` // CheckedAt ist der Zeitpunkt der Gesamtauswertung in UTC. CheckedAt time.Time `json:"checked_at"` } // CheckFunc prüft eine einzelne Komponente. // // Die Implementierung muss den übergebenen Context respektieren, damit eine // hängende Komponente nicht den gesamten Health-Check blockiert. type CheckFunc func(context.Context) CheckResult // registeredCheck verbindet einen Komponentennamen mit seiner Prüffunktion. type registeredCheck struct { // componentName ist der Name der Komponente im Bericht (z. B. "database"). componentName string // checkFunction führt die eigentliche Prüfung durch. checkFunction CheckFunc // isCritical legt fest, ob diese Komponente für die Betriebsbereitschaft nötig ist. isCritical bool } // Registry sammelt alle Health-Checks eines Dienstes. // Sie ist nebenläufig nutzbar, da Checks aus HTTP-Handlern aufgerufen werden. type Registry struct { // mutex schützt die Check-Liste gegen gleichzeitige Registrierung und Auswertung. mutex sync.RWMutex // checks sind die registrierten Komponentenprüfungen. checks []registeredCheck // checkTimeout begrenzt die Dauer einer einzelnen Prüfung. checkTimeout time.Duration } // NewRegistry erzeugt eine leere Registry. // // checkTimeout begrenzt jede einzelne Prüfung, damit eine langsame Komponente // den Health-Endpunkt nicht blockiert. func NewRegistry(checkTimeout time.Duration) *Registry { return &Registry{checkTimeout: checkTimeout} } // Register nimmt eine Komponentenprüfung auf. // // isCritical steuert, ob ein Ausfall dieser Komponente die Betriebsbereitschaft // (Readiness) aufhebt. func (registry *Registry) Register(componentName string, isCritical bool, checkFunction CheckFunc) { registry.mutex.Lock() defer registry.mutex.Unlock() registry.checks = append(registry.checks, registeredCheck{ componentName: componentName, checkFunction: checkFunction, isCritical: isCritical, }) } // Check führt alle registrierten Prüfungen nebenläufig aus. func (registry *Registry) Check(parentContext context.Context) SystemReport { registry.mutex.RLock() // Eine Kopie erlaubt das Freigeben der Sperre vor den eigentlichen Prüfungen, // die deutlich länger dauern als das Kopieren der Liste. checksToRun := make([]registeredCheck, len(registry.checks)) copy(checksToRun, registry.checks) registry.mutex.RUnlock() componentResults := make(map[string]CheckResult, len(checksToRun)) // resultsMutex schützt die Ergebniskarte, da alle Prüfungen parallel laufen. var resultsMutex sync.Mutex var checkWaitGroup sync.WaitGroup for _, checkToRun := range checksToRun { checkWaitGroup.Add(1) go func(currentCheck registeredCheck) { defer checkWaitGroup.Done() checkResult := registry.runSingleCheck(parentContext, currentCheck) resultsMutex.Lock() componentResults[currentCheck.componentName] = checkResult resultsMutex.Unlock() }(checkToRun) } checkWaitGroup.Wait() return SystemReport{ Status: worstStatus(componentResults), Components: componentResults, CheckedAt: time.Now().UTC(), } } // runSingleCheck führt eine Prüfung mit Timeout und Panic-Schutz aus. func (registry *Registry) runSingleCheck(parentContext context.Context, currentCheck registeredCheck) CheckResult { checkContext, cancelCheckContext := context.WithTimeout(parentContext, registry.checkTimeout) defer cancelCheckContext() checkStartTime := time.Now() // resultChannel ist gepuffert, damit die Goroutine auch nach einem Timeout // terminieren kann und nicht dauerhaft blockiert. resultChannel := make(chan CheckResult, 1) go func() { // Ein Panic in einer Prüfung darf den Health-Endpunkt nicht mitreißen. defer func() { if panicValue := recover(); panicValue != nil { resultChannel <- CheckResult{ Status: StatusCritical, Message: "Die Zustandsprüfung dieser Komponente ist unerwartet abgebrochen.", RecommendedAction: "Serverlog zu dieser Komponente prüfen.", } } }() resultChannel <- currentCheck.checkFunction(checkContext) }() select { case checkResult := <-resultChannel: checkResult.LatencyMilliseconds = float64(time.Since(checkStartTime).Microseconds()) / 1000 checkResult.CheckedAt = time.Now().UTC() // Eine Prüfung ohne gesetzten Status gilt als kritisch, nicht als gesund. if checkResult.Status == "" { checkResult.Status = StatusCritical checkResult.Message = "Die Komponente hat keinen Zustand gemeldet." } return checkResult case <-checkContext.Done(): return CheckResult{ Status: StatusCritical, Message: "Die Komponente hat nicht rechtzeitig geantwortet.", RecommendedAction: "Erreichbarkeit und Auslastung der Komponente prüfen.", LatencyMilliseconds: float64(time.Since(checkStartTime).Microseconds()) / 1000, CheckedAt: time.Now().UTC(), } } } // IsReady meldet, ob alle als kritisch markierten Komponenten gesund sind. // // Readiness entscheidet, ob ein Load Balancer Verkehr schicken darf; eine // unkritische Warnung soll den Dienst deshalb nicht aus dem Verkehr nehmen. func (registry *Registry) IsReady(parentContext context.Context) (bool, SystemReport) { systemReport := registry.Check(parentContext) registry.mutex.RLock() defer registry.mutex.RUnlock() for _, registeredComponent := range registry.checks { if !registeredComponent.isCritical { continue } componentResult, isPresent := systemReport.Components[registeredComponent.componentName] // Ein fehlendes Ergebnis wird als nicht bereit gewertet. if !isPresent || componentResult.Status != StatusHealthy { return false, systemReport } } return true, systemReport } // worstStatus ermittelt den schlechtesten Zustand aller Komponenten. func worstStatus(componentResults map[string]CheckResult) Status { // Ohne registrierte Prüfung gibt es keine belegte Aussage über die Gesundheit. if len(componentResults) == 0 { return StatusHealthy } overallStatus := StatusHealthy for _, componentResult := range componentResults { if componentResult.Status.IsWorseThan(overallStatus) { overallStatus = componentResult.Status } } return overallStatus }