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. Tím můžeme monitorovat, zda v aplikaci nedošlo k nějaké chybě, a ideálně na ni zareagovat 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 určité akci (eventu). Lze se setkat i s termíny web callback, push API nebo reverse API. Výhodou tohoto přístupu je, že není nutné neustále dotazovat API na aktuální stav. Místo toho dostaneme informaci o akci, která nás zajímá, přesně ve chvíli, kdy nastane.

Obsah

Úvod

Můžeme kontrolovat dostupnost služeb, na kterých je naše aplikace závislá, například databázový server, Azure cloud, různá API a podobně. Kromě toho lze monitorovat i minimální hardwarové prostředky, jako jsou 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í kontrolu. Výstup z health checku je možné přizpůsobit dle potřeb. Například webové rozhraní Health Check UI má vlastní implementaci ResponseWriteru, což si ukážeme.

Health check je také užitečný například pro účely load balanceru, aby neposílal požadavky na nefunkční služby. V kontejnerizaci může sloužit k resetování služby nebo při použití mikroslužeb umožňuje detekci nefunkčních služeb, které se deaktivují, zatímco zbytek aplikace zůstane v provozu.

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 několik 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 vše nastaveno, a pokud zadáme adresu https://localhost:44306/healthchecks-ui spustí se monitoring. V mém případě aplikace běží na localhostu v IIS Express. V monitoringu vidíme, že aplikace webapp (http://localhost:5000/fullhealth) běží a připojuje se k externí službě webservice.cz. Nicméně nemá přístup k zápisu do složky pro logy, což aplikaci nebrání v provozu (stav je proto označen jako Degraded). Naopak se ale nepřipojuje k databázi (kvůli chybějícímu connection stringu v konfiguračním souboru), což je vážný problém. Z tohoto důvodu je status aplikace označen jako Unhealthy, a tím se stává celý status aplikace Unhealthy. Pomocí webhooku si můžeme o problému nechat zaslat notifikaci.

Odkazy