Health monitoring webové aplikace

16.02.2021

Pokud spravujeme jednu nebo více aplikací, může se nám hodit tzv. health check. Health check je námi zvolená kontrola na straně aplikace, kterou můžeme pravidelně spouštět a tím monitorovat jestli v aplikaci nedošlo k nějaké chybě a tím na ní zaregovat ideálně dříve, než se o ní dozví uživatel :-)

Slovníček

ASP.NET Core Open-source webový framework od Microsoftu (první release 2016).
JSON Formát pro výměnu dat, který je dobře čitelný i pro uživatele.
Webhook HTTP požadavek, který se zavolá při nějaké akci (eventu). Lze se setkat i s termínem web callback, push API nebo reverse API. Výhodou je, že se nepotřebujeme neustále dotazovat API na aktuální stav, ale dostaneme informaci o akci, která nás zajímá, v momentě kdy nastane.

Obsah

Úvod

Kontrolovat můžeme dostupnost služeb, na kterých je naše aplikace závislá, například databázový server, Azure cloud, ruzná API a podobně, ale také můžeme kontrolovat minimální hw prostředky jako RAM nebo CPU. Spousta health checků je již připravená ve formě Nuget balíčků, ale v případě potřeby si můžeme napsat i vlastní check. Výstup z health checku je také možné upravit dle potřeby, například webový interface Health Check UI má vlastní implementaci ResponseWriteru, což si ukážeme.

Health check je také užitečný pro účely load balanceru, aby neposílal požadavky na neběžící služby, v kontejnerizaci kdy resetujeme chybnou službu nebo při použítí mikroslužeb, kdy nefungujicí službu uživateli skryjeme a zbytek aplikace funguje dál.

Základní nastavení

Do souboru Startup.cs v metodě ConfigureServices přidáme health check middleware

services.AddHealthChecks()
        .AddCheck(
          name: "Health", // Jméno kontroly, které můžeme zobrazit ve výpisu kontrol
          check: () => { return new HealthCheckResult(HealthStatus.Healthy); }, // Vracíme Status Code 200
          tags: new[] { "health" }); // Pokud si označíme kontrolu tagem, tak můžeme pro různé endpointy spouštět jiné kontroly 
 

do Configure metody přidáme konfiguraci endpointu, na které má health check "běžet"

app.UseEndpoints(endpoints =>
{
    // Výchozí routování
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");

    // Nastavíme na url /health spouštění všech kontrol s tagem "health"
    endpoints.MapHealthChecks("/health", new HealthCheckOptions
    {
        Predicate = (check) => check.Tags.Contains("health")
    });
});

Pokud aplikaci spustíme, můžeme na adrese /health ověřit že nám běží. Zároveň nestahujeme větší než nutně potřebné množství dat a aplikaci zbytečně nezatěžujeme.

Přidání dalších kontrol

Zatím máme nastavenou pouze jednoduchou kontrolu, která nám říká, jestli aplikace odpovídá nebo ne. Pokud bychom chtěli kontrolovat něco specifického tak můžeme použít nějaký dostupný nuget balíček nebo si napsat vlastní check. Zajímavý je například balíček pro kontrolu nastavení Entity Frameworku.

SQL Server

// Nuget: Install-Package AspNetCore.HealthChecks.SqlServer
.AddSqlServer(
    connectionString: Configuration.GetConnectionString("SomeDatabase"),
    failureStatus: HealthStatus.Unhealthy,
    tags: new[] { "sqlserver" });

URL check

// Nuget: Install-Package AspNetCore.HealthChecks.Uris
.AddUrlGroup(new Uri(@"http://webservice.cz"), "webservice.cz check", HealthStatus.Degraded)

Vlastní check (directory access)

Pokud potřebujeme zkontrolovat jestli má aplikace přístup na síťovou složku, například v podnikové síti, můžeme implementovat interface IHealthCheck, například
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace WebApp.HealthChecks
{
    public class DirectoryWriteAccessHealthCheck : IHealthCheck
    {
        private readonly string _path;
        private readonly string _fileName;

        public DirectoryWriteAccessHealthCheck(string path, string fileName = null)
        {
            _path = path;
            _fileName = fileName ?? "healthCheck.txt";
        }

        public Task<HealthCheckResult> CheckHealthAsync(
            HealthCheckContext context, CancellationToken cancellationToken = default)
        {
            try
            {
                string fullPath = Path.Combine(_path, _fileName);
                File.WriteAllText(fullPath, "Application health check.");
                File.Delete(fullPath);
                return Task.FromResult(HealthCheckResult.Healthy("Healthy"));
            } 
            catch
            {
                // If HealthCheckResult is not defined, by default it is Unhealthy and
                // it can be overriden with services.AddHealthChecks().AddCheck(failureStatus:) parameter.
                // If HealthCheckResult is defined, the failureStatus parameter do not override the returned status.
                // return Task.FromResult(HealthCheckResult.Unhealthy("Unhealthy", e));
                throw;
            }
        }
    }
}

třídu pak inicializujeme v instance parametru

.AddCheck(
    name: "Write access to logs directory",
    instance: new DirectoryWriteAccessHealthCheck(@"F:\logs\webapp\"),
    failureStatus: HealthStatus.Degraded,
    tags: new[] { "Write access to logs directory" });

Nastavení endpointu

Protože jsme přidali hodně služeb, vytvoříme si pro ně nový endpoint. V predikátu nebudeme nic filtrovat, takže ve výpisu budeme mít všechny kontroly.

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

Nastavení vlastní JSON odpovědi

Pokud nám nestačí výchozí status, tedy textový souhrnný status za všechny kontroly ("Healthy", "Unhealthy", "Degraded"), můžeme implementovat vlastní ResponseWriter.

endpoints.MapHealthChecks("/healthfull", new HealthCheckOptions()
  {
      Predicate = _ => true
      // Nadefinování že chceme použít vlastní ResponseWriter funkci
      ResponseWriter = CustomHealthCheckWriter
  });
private Task CustomHealthCheckWriter(HttpContext httpContext, HealthReport healthReport)
{
    var overallResult = new OverallCustomHealthCheckResult()
    {
        OverallStatus = healthReport.Status.ToString(),
        OverallDuration = healthReport.TotalDuration.TotalSeconds.ToString(),
        Environment = Environment.EnvironmentName
    };

    foreach (var entry in healthReport.Entries)
    {
        overallResult.HealthChecks.Add(new CustomHealtCheckResult()
        {
            Name = entry.Key,
            Status = entry.Value.Status.ToString(),
            Duration = entry.Value.Duration.TotalSeconds.ToString(),
            ExceptionMessage = entry.Value.Exception?.Message
        });
    }

    httpContext.Response.ContentType = "application/json";

    string json = System.Text.Json.JsonSerializer.Serialize(overallResult, new JsonSerializerOptions()
    {
        WriteIndented = true
    });

    return httpContext.Response.WriteAsync(json);

}

class OverallCustomHealthCheckResult
{
    public string OverallStatus { get; set; }
    public string OverallDuration { get; set; }
    public string Environment { get; set; }
    public List<CustomHealtCheckResult> HealthChecks { get; set; }
        = new List<CustomHealtCheckResult>();

};

class CustomHealtCheckResult
{
    public string Name { get; set; }
    public string Status { get; set; }
    public string ExceptionMessage { get; set; }
    public string Duration { get; set; }
}

Výstup pak může vypadat nějak obdobně, JSON je čitelný a lze ho zpracovat i programově

Monitorovací služba

Základní nastavení

Zaregistrujeme middleware a ukládat checky budeme do in-memory paměti, je ale možné nastavit i ukládání do databáze.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    // Install-Package AspNetCore.HealthChecks.UI
    // Install-Package AspNetCore.HealthChecks.UI.InMemory.Storage
    services.AddHealthChecksUI().AddInMemoryStorage();
}

Nastavíme UI endpoint

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");

    // default route is /healthchecks-ui but we can change it
    endpoints.MapHealthChecksUI(options => options.UIPath = "/healthchecks-ui");
});

Propojení s existující aplikací

Nyní můžeme aplikaci spustit, ale nemáme nastavené žádné aplikace, které chceme monitorovat. V první řadě musíme takové aplikace upravit, aby nám vraceli JSON response, kterému bude naše aplikace rozumět a k tomu využijeme metodu UIResponseWriter.WriteHealthCheckUIResponse.

endpoints.MapHealthChecks("/fullhealth", new HealthCheckOptions
{
    Predicate = _ => true,
    // Install-Package AspNetCore.Diagnostics.HealthChecks.UI.Client
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

v naší monitorovací aplikaci pak v appsettings.json nastavíme monitoring endpointu /fullhealth, kterému jsme nastavili UIResponseWriter.WriteHealthCheckUIResponse, včetně základního nastavení

  • EvaluationTimeOnSeconds nám říká jak často se kontrola bude provádět
  • MinimumSecondsBetweenFailureNotifications nám říká jak často budeme informováni o nedostupnosti služby

"HealthChecks-UI": {
  "HealthChecks": [
    {
      "Name": "WebApp",
      "Uri": "http://localhost:5000/fullhealth"
    }
  ],
  "EvaluationTimeOnSeconds": 10,
  "MinimumSecondsBetweenFailureNotifications": 1
}

Nastavení upozornění (webhook)

Pokud nám nějaká aplikace přestane odpovídat, můžeme si nastavit tzv. webhook. Webhook nám zavolá nějakou službu, například Azure Function, která nám může poslat email nebo sms. Detaily k nastavení jsou v dokumentaci.

Závěr

Nyní máme již vše nastaveno a pokud zadáme adresu https://localhost:44306/healthchecks-ui (v mém případě mi běží aplikace na localhostu v IIS express) tak se nám spustí monitoring a vidíme, že aplikace webapp (http://localhost:5000/fullhealth) běží, také se připojí na externí službu webservice.cz, ale nefunguje jí přístup na zápis do log složky, což aplikaci nebrání ve fungování (proto je nastavený stav Degraded), ale také se nepřipojí k databázi (z důvodu, že v konfiguračním souboru není nastavený connection string) a to je již vážný problém, proto je status Unhealthy a tím se i celý status aplikace stává Unhealthy. Pomocí webhooku si pak můžeme nechat zaslat notifikaci, že u této aplikace došlo k problému.

Odkazy