Datenbanken und externe Dienste
Health Checks in ASP.NET richtig nutzen, Teil 2
Der erste Teil der Serie „Health Checks in ASP.NET richtig nutzen“ hat einen einfachen Health-Endpunkt vorgestellt, der entweder drei unterschiedliche Werte für den Status oder weitere Informationen, etwa im JSON-Format, ausgibt. Der in der Praxis spannende Teil beginnt dort, wo Health Checks Abhängigkeiten wie Datenbanken, Message Broker oder externe HTTP-APIs überwachen. Ziel ist, nicht nur den eigenen Prozess, sondern das gesamte System aus Sicht des Clients zu betrachten: Wenn die Datenbank nicht erreichbar ist, ist der Service aus Nutzersicht faktisch down.
ASP.NET Core bringt mit IHealthCheck eine einfache Schnittstelle mit, über die sich eigene Prüfungen implementieren lassen. Zur Erinnerung: Teil 1 dieser Serie hat ein eine einfache Klasse SampleHealthCheck (siehe Listing 1) gezeigt, die dieses Interface implementiert.
Listing 1: Implementierung des IHealthCheck Interface
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"));
}
}
Die Prüflogik ist hier allerdings noch auf den Standardwert true festgelegt, übernimmt aber noch keine wirkliche Prüfung. Dieses Beispiel lässt sich nun auf andere Bereiche erweitern. Ein typischer Datenbank-Check etwa könnte aussehen wie in Listing 2 gezeigt.
Listing 2: Implementierung eines beispielhaften Datenbank-Checks
public class DatabaseHealthCheck : IHealthCheck
{
private readonly IConfiguration _config;
public DatabaseHealthCheck(IConfiguration config) => _config = config;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var connectionString = _config.GetConnectionString("DefaultConnection");
try
{
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
// Optional: einfache Abfrage, um Latenz / Timeouts zu erkennen
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT 1";
await cmd.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy("Database reachable");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Database unreachable", ex);
}
}
}
Registriert wird dieser Check bei AddHealthChecks, idealerweise mit einem sprechenden Namen zur besseren Identifikation:
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("sql_db");
Für viele Standard-Abhängigkeiten existieren zudem fertige Checks, sowohl in Microsoft-Paketen als auch in Bibliotheken wie AspNetCore.Diagnostics.HealthChecks. Dazu zählen Pakete für beispielsweise SQL Server, PostgreSQL, Redis, MongoDB wie AspNetCore.HealthChecks.SqlServer, AspNetCore.HealthChecks.Npgsql (PostgreSQL) und AspNetCore.HealthChecks.Redis. Damit lässt sich eine typische Infrastruktur sehr schnell abbilden:
var sqlServerConnection = builder.Configuration.GetConnectionString("SqlServerConnection") ?? "SqlServerDefaultConnection";
var redisConnection = builder.Configuration.GetConnectionString("RedisConnection") ?? "RedisDefaultConnection";
builder.Services.AddHealthChecks()
.AddSqlServer(sqlServerConnection)
.AddRedis(redisConnection)
.AddCheck<ExternalApiHealthCheck>("external_api");
Wer mehr Einfluss beim Hinzufügen dieser Health Checks haben möchte, kann das über weitere Parameter bei AddSqlServer und AddRedis erledigen, wie Listing 3 zeigt.
Listing 3: Konfiguration von Health Checks mit Parametern
builder.Services.AddHealthChecks()
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
name: "sqlserver",
failureStatus: HealthStatus.Unhealthy,
tags: new[] { "database", "sql" })
.AddRedis(
redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
failureStatus: HealthStatus.Degraded,
tags: new[] { "cache", "redis" });
Ein externer API-Check sollte dabei immer mit Time-outs und gegebenenfalls Cancellation-Tokens arbeiten, damit der Health-Endpunkt selbst nicht blockiert. Listing 4 zeigt die Implementierung einer eigenen Health-Check-Klasse für externe APIs.
Listing 4: Health-Check-Implementierung für einen externen Dienst
public class ExternalApiHealthCheck : IHealthCheck
{
private readonly HttpClient _client;
public ExternalApiHealthCheck(HttpClient client) => _client = client;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(2));
try
{
var response = await _client.GetAsync("/ping", cts.Token);
if (response.IsSuccessStatusCode)
return HealthCheckResult.Healthy("External API reachable");
return HealthCheckResult.Unhealthy($"External API returned {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("External API unreachable", ex);
}
}
}
Wichtig ist die Bewertung: Eine nicht erreichbare optionale Integration kann Degraded sein, während das Wegbrechen der primären Datenbank Unhealthy bedeuten sollte. Durch die unterschiedlichen Statuswerte können Load-Balancer oder Orchestrierungsplattformen differenziert reagieren, indem sie beispielsweise Anfragen drosseln, statt den Pod sofort neu zu starten.
Startup-Health-Check
Bei Anwendungen mit langwierigen Initialisierungsschritten lässt sich eine separate Startup-Probe implementieren. Im Beispiel von Microsoft setzt ein Background-Service nach 15 Sekunden eine Variable StartupCompleted. Die zugehörige StartupHealthCheck-Klasse liefert Healthy, sobald die Initialisierung abgeschlossen ist. Diese Probe wird über Tags nur an den Readiness-Endpunkt gebunden. In Kubernetes wird die Startup-Probe nur während des initialen Hochfahrens ausgeführt.
Zwischenfazit und Ausblick
Dieser Beitrag hat gezeigt, wie sich Datenbanken, Caches und der Speicher im Rahmen von Health Checks überwachen lassen und wie Liveness-, Readiness- und Startup-Proben mithilfe von Tags getrennt werden. Im nächsten Teil dieser Serie wird es um externe APIs, Dateisystem- und Speicherplatzprüfungen sowie um eigene JSON-Antworten für Monitoring-Tools gehen.