Monitoring von ASP.NET Core Applikationen

Integritätsprüfungen (Health Checks) werden verwendet, um den Status einer App in Echtzeit zu überwachen. Durch die Integration von Webhooks werden fehlerhafte Integritätsprüfungen sofort gemeldet (z.B. MS Teams, Slack, Email, etc…). Dies ermöglicht eine schnellere und präzisere Handlungsweise und Untersuchung des Problems. Health Checks werden von der Anwendung als HTTP-Endpunkt(e) verfügbar gemacht, über den die Komponente der App-Infrastruktur und deren Wechselwirkung überprüft werden. Übliche Integritätsprüfungen überwachen u.A. :

  • Die Verbindung zu externen Abhängigkeiten (Services, Datenbanken)
  • Die Nutzung der physischen Ressourcen (Arbeitsspeicher, CPU Leistung, etc…)

Health Checks Framework für ASP.NET Core

ASP.NET Core bietet die built-in Health Checks Middleware und Bibliothek, die wir benutzen werden, um Integritätsprüfungen für unsere Anwendung zu erstellen. Im Folgenden Abschnitt zeigen wir den benötigten Setup und Konfiguration der Middleware.

Setup

Für eine primitive Nutzung reicht es, den Health Check Service in Startup.cs hinzuzufügen. 

public class Startup
    {

        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            services.AddHealthChecks();

            // ...
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ...

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHealthChecks("/health", new HealthCheckOptions()
                {
                    Predicate = _ => true,
                    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
                });
            });
        }
    }

Über den von uns gesetzten Endpunkt „/health“ können wir nun den Status der Anwendung abfragen. Im nächsten Schritt definieren wir ein paar Health Checks.

Registrierung eines Health Checks

Health Checks Pakete enthalten Integritätsprüfungen für mehrere Szenarien/Abhängigkeiten, die in der Industrie häufig vorkommen oder verwendet werden. Eine Liste dieser Abhängigkeiten finden wir in der offiziellen Dokumentation. Um ein Health Check zu verwenden, brauchen wir nur das entsprechende Paket zu installieren und den Service hinzuzufügen. Zum Beispiel, unsere Anwendung benutzt, die CosmosDb. Die Registrierung des Health Checks erfolgt in zwei Schritte.

Install-Package AspNetCore.HealthChecks.CosmosDb
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddHealthChecks()
        .AddCosmosDb();
}

Custom Health Check

Jeder Health-Check muss die IHealthCheck Schnittstelle implementieren. Die Methode CheckHealthAsync gibt ein HealthCheckResult zurück. Ein Health Check Ergebnis kann einen der folgenden Werte betragen:

  • Healthy
  • Degraded
  • Unhealthy
Folgend ist die Implementierung eines Health-Checks, der unsere Webseite anpingt und anhand des Ergebnisses und der Dauer der Rundreise einen Status zurück gibt.
 public class ExampleHealthCheck : IHealthCheck
    {
        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
        {
            try
            {
                var ping = new Ping();
                var reply = ping.Send("www.inoteq.com");

                if (reply.Status != IPStatus.Success)
                    return HealthCheckResult.Unhealthy();

                if (reply.RoundtripTime > 100)
                    return HealthCheckResult.Degraded();

                return HealthCheckResult.Healthy();
            }
            catch
            {
                return HealthCheckResult.Unhealthy();
            }
        }
    }

Um diesen Health-Check anwenden zu können, müssen wir den in Startup.cs registrieren.

   services
                .AddHealthChecks()
                .AddCheck<ExampleHealthCheck>("Ping Check");

Es kann für kompliziertere Anwendungen mit umfangreicheren Integritätsprüfungen von Vorteil sein, die verschiedene Checks nach Kategorie zu trennen. Die Health-Checks Bibliothek ermöglicht uns, bei der Registrierung der Checks Tags zu definieren. Darauf basierend können wir dann die Health Endpunkte zuordnen.

services.AddHealthChecks()
    .AddCheck("CosmosDb Check", () =>
        HealthCheckResult.Healthy("CosmosDb is healthy"), tags: new[] { "azure" })
    .AddCheck("Blob Storage Check", () =>
        HealthCheckResult.Unhealthy("Blob Storage is unhealthy"), tags: new[] { "azure" })
    .AddCheck("Memory Check", () =>
        HealthCheckResult.Healthy(""), tags: new[] { "system" });
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health/azure", new HealthCheckOptions()
    {
        Predicate = (check) => check.Tags.Contains("azure")
    });
});

Es ist auch möglich die Berichte der HealthChecks an anderen Entitäten mittels Publishers weiterzuleiten. Dafür muss die IHealthCheckPublisher Schnittstelle implementiert werden.  Der Publisher führt die Health-Checks aus und ruft die PublishAsync Methode auf. Detaillierte Dokumentation der Schnittstelle und der Optionen eines HealthCheckPublisher können Sie hier finden. Die Bibliothek unterstützt übliche Monitoring Systeme wie z.B.

  • Prometheus
  • DataDog
  • Application Insights (Azure)
 
Um die Berichte an z.B. Application Insights weiterzuleiten, kann der folgender Code verwendet werden
services.AddHealthChecks()
        .AddCheck<ExampleHealthCheck>("Ping Check")
        .AddApplicationInsightsPublisher()

Health Checks UI

Health Checks UI ist eine minimale Benutzeroberfläche zum Speichern und Darstellen von Berichten konfigurierter Health-Checks Endpunkten. Um den Service zu integrieren, müssen wir den in gleicher Weise wie die Health-Checks Middleware zu der Service Collection hinzufügen. Der Endpunkt zu der UI ist per Default „/healthchecks-ui“.

Setup

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
        .AddHealthChecksUI()
        .AddInMemoryStorage();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app
          .UseEndpoints(config =>
           {
             config.MapHealthChecksUI();
          });
    }
}

Health-Checks Timeline

Mittels AddInMemoryStorage() ermöglichen wir das Speichern der Health-Checks Ergebnisse im Arbeitsspeicher. Für Production ist dennoch die Nutzung einer Datenbank zu empfehlen. Aktuell sind folgende Datenbanken unterstützt:

  • MySQL
  • PostgreSQL
  • SQLite
  • SqlServer

Dies sorgt u.A. dafür eine Historie der Health-Checks erfassen zu können.

Webhooks & Alerting

Logischerweise möchten wir benachrichtigt werden, wenn eine Komponente unserer Anwendung nicht richtig funktioniert (i.e. der Health-Check gibt ein Unhealthy/Degraded Status zurück). Dafür müssen wir AddWebhookNotification Methode benutzen. Den Payload der Benachrichtigung können wir an der Anforderungen des Empfängers (Teams, Slack, email, etc…) anpassen. Wenn die Anwendung oder die fehlerhafte Komponente zum normalen Stand wiedergebracht wird, wird ein RestorePayload gesendet werden. HealthChecksUI unterstützt aktuell die folgenden Bookmarks

  • [[ LIVENESS ]] : Der Name der fehlerhaften Komponente
  • [[ FAILURE ]]: Eine auf der auftretenden Fehlern bezogene Nachricht (Anzahl fehlerhafter Checks)
  • [[ DESCRIPTIONS ]] : Eine Beschreibung des Fehlers

Webhooks können in appsettings.json konfiguriert werden oder in Startup.cs wie folgend.

s.AddWebhookNotification("Teams",
        uri: teamsWebhookUri,
        payload: teamsPayload.FailurePayload(),
        restorePayload: teamsPayload.RestorePayload(),
        customMessageFunc: (report) =>
             {
                  var failing = report.Entries.Where(e => e.Value.Status == UIHealthStatus.Unhealthy);
                   return $"{failing.Count()} healthchecks are failing";
             },
         customDescriptionFunc: report =>
             {
                   var failing = report.Entries.Where(e => e.Value.Status == UIHealthStatus.Unhealthy);
                   return $"HealthChecks with names {string.Join("/", failing.Select(f => f.Key))} are failing";
             });

Im obigen Code-Schnipsel ändern wir den Default [[ FAILURE ]] und [[ DESCRIPTIONS ]] mit jeweils customMessageFunc und customDescriptionFunc. Der Payload und RestorePayload werden vom teamsPayload Objekt generiert. Die TeamsPayload Klasse haben wir selber implementiert, um den Code sauber zu halten. 

Zusammenfassung

Dieser Artikel thematisierte die Wichtigkeit und Nützlichkeit der Integration von Health Checks in einer Anwendung. Für die Demo haben wir eine ASP.NET Core Anwendung als Use-Case genommen, für welche wir einen Health Check implementiert haben, der einen Server anpingt und anhand der Antwort einen Status zurückgibt. Dazu haben wir die HealthChecksUI SPA integriert, um die Liste der Health Checks  darzustellen. Außerdem haben wir ein paar Features der HealthChecks Bibliothek benutzt, indem wir das Alerting bei fehlerhaften Checks eingebaut haben. Die Bibliothek unterstützt eine Vielzahl Konfigurationsalternativen für verschiedene Szenarien und App-Infrastrukturen, die wir in der Demo nicht gebraucht haben. (Authentifizierung, Styling/Branding der UI, Health Check Ergebnisse mit Key/Value Data zusätzlich zur Beschreibung des Ergebnisses). Weitere Infos zu der Bibliothek können Sie in der Git Repository und der Dokumentation von Microsoft finden.