Grundlagen von Health Checks
Health Checks in ASP.NET richtig nutzen, Teil 1
In vielen Projekten existiert ein /health-Endpunkt, um anzugeben, ob ein Dienst beziehungsweise Endpunkt noch erreichbar ist. Dieser Health Check gibt aber allzu häufig lediglich einen HTTP-Status-Code 200 als OK zurück. Das ist zwar ein technisches Merkmal, dass alles in Ordnung ist, allerdings ohne jeden fachlichen Mehrwert.
Moderne Health Checks sollten dagegen darauf abzielen, die tatsächliche Betriebsfähigkeit der Anwendung und ihrer Abhängigkeiten zu überprüfen und maschinenlesbar zu berichten. Hier ist ASP.NET Core keine Ausnahme. Seit .NET Core 2.2 ist das Health-Check-Framework direkt in ASP.NET Core integriert. Es stellt ein standardisiertes API über die Schnittstelle IHealthCheck, eine zentrale Ausführungskomponente über HealthCheckService und eine Middleware für HTTP-Endpunkte zur Verfügung. Ein Health Check liefert eine HealthStatus-Enumeration mit den Werten Healthy, Degraded sowie Unhealthy plus optionale Daten und Fehlermeldungen, die Monitoring-Systeme wie Prometheus, Azure Application Insights oder Kubernetes-Probes auswerten können.
Die Basiskonfiguration ist minimal: In Program.cs sind die Health Checks zu registrieren und auf einen Endpunkt zu mappen. Im Minimal-Hosting-Modell kann das in etwa so aussehen wie in Listing 1.
Listing 1: Beispiel für die Basis-Registrierung eines Health Checks
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddHealthChecks(); // Basis-Registrierung
var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();
Ausgabe des Health-Endpunkts im Browser (Bild 1)
Autor
Ausgabe des Health-Endpunkts in Postman (Bild 2)
AutorDieser Endpunkt gibt standardmäßig nur den aggregierten Status zurück (HTTP-Status-Code 200 für Healthy oder Degraded, 503 für Unhealthy). Bild 1 zeigt die Ausgabe im Browser und Bild 2 zeigt die Antwort in Postman.
Für produktive Szenarien empfiehlt sich jedoch eine ausführlichere Ausgabe, etwa JSON, die pro Check einen Status und die entsprechende Fehlermeldung enthält, wie Listing 2 zeigt.
Listing 2: Health-Check-Implementierung mit Status-Informationen
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using System.Text.Json;
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var payload = new
{
status = report.Status.ToString(),
results = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalMilliseconds,
})
};
await context.Response.WriteAsync(JsonSerializer.Serialize(payload));
}
});
Die Rückgabe ist jetzt die nachfolgende JSON-Ausgabe, die in Bild 3 als Resultat in Postman gezeigt ist.
{"status":"Healthy","results":[]}
Damit wird /health zum echten Diagnose-Endpunkt, der nicht nur „rot“ signalisiert, sondern konkret sagt, welcher Check versagt hat und wie lange die Ausführung gedauert hat.
Eigene Checks mit IHealthCheck
Um prüfbare Abhängigkeiten wie Datenbank, Dateisystem oder externe APIs abzudecken, lässt sich die Schnittstelle IHealthCheck implementieren. Die Methode CheckHealthAsync liefert ein HealthCheckResult, das Status, optionale Schlüssel-Werte-Daten und eine Beschreibung enthält. Der Code in Listing 3 demonstriert den Aufbau eines einfachen Checks.
Ausgabe des Health-Endpunkts im JSON-Format in Postman (Bild 3)
AutorListing 3: Implementierung für einen Health Check in einer eigenen Klasse
public class SampleHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var isHealthy = true; // Prüflogik...
if (isHealthy)
{
return Task.FromResult(
HealthCheckResult.Healthy("Alles in Ordnung"));
}
return Task.FromResult(
new HealthCheckResult(
context.Registration.FailureStatus,
"Ein Fehler ist aufgetreten"));
}
}
In der Konfiguration muss der Check über AddCheck registriert sein. Dabei lassen sich Fehlerstatus und Tags festgelegen. Tags helfen später bei der Unterscheidung zwischen Liveness- und Readiness-Proben. Listing 4 zeigt ein Beispiel für diese Konfiguration.
Dieser Code gibt jetzt den JSON-String aus Listing 5 als Antwort zurück.
In Bild 4 ist die Antwort in Postman zu sehen. Der Health-Endpunkt gibt nun eine deutlich umfangreichere Antwort zurück. Soll die Implementierung schlanker ausfallen, lässt sich über AddCheck auch eine Lambda-Funktion angeben, die eine sehr schlanke Prüfung ohne eigene Klasse erlaubt.
Listing 4: Beispiel-Konfiguration für die Health-Check-Implementierung in einer Klasse
builder.Services.AddHealthChecks()
.AddCheck<SampleHealthCheck>(
"Sample",
failureStatus: HealthStatus.Degraded,
tags: ["sample"]);
Listing 5: JSON-Ausgabe des Health Checks
{
"status":"Healthy",
"results":[
{
"name":"Sample",
"status":"Healthy",
"description":"Alles in Ordnung",
"duration":1.455
}
]
}
Erweiterte JSON-Ausgabe des Health-Endpunkts (Bild 4)
AutorAusblick und Zwischenfazit
Dieser erste Beitrag der Serie „Health Checks in ASP.NET richtig nutzen“ hat die Grundlagen vermittelt: Warum Zustandsprüfungen wichtig sind, wie der /health-Endpunkt konfiguriert wird und wie sich eigene Prüfungen implementieren lassen.
Für Health Checks gilt es darüber hinaus zu definieren, welche Art von „Gesundheit“ überhaupt zu prüfen ist: Läuft nur der Prozess? Ist die Datenbank erreichbar? Sind alle kritischen Integrationen funktionsfähig? Diese Abgrenzung ist in produktiven Anwendungen sehr relevant und deshalb Gegenstand weiterer Teile dieser Serie, wenn es von reinen internen Checks hin zu Abhängigkeiten, Ressourcen und Kubernetes-Probes geht.