Health monitoring webové aplikace
16.02.2021Pokud 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
- Základní nastavení
- Přidání dalších kontrol
- Monitorovací služba
- Přizpůsobení výstupu
- Závěr
- Odkazy
Ú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.