With KQL you answer questions when you look. At three in the morning, nobody looks. For that there are two things that ask on their own: health checks, with which the platform wants to know every minute whether the Kladde is alive and able to work, and alerts, with which Azure Monitor regularly runs a query or checks a metric and notifies you when something is wrong.

In this lesson, the Kladde gets separate endpoints for liveness and readiness, the container app uses them as probes, a test checks the Kladde from the outside, and two alerts report outages and a high error rate by email. At the end you trigger an alert on purpose, because an alert that has never fired is a guess.

Health checks in ASP.NET Core

A health check is a small check with three possible results: Healthy, Degraded or Unhealthy. You register checks with AddHealthChecks() and make them retrievable at an address with MapHealthChecks(...). The response is plain: the status as text and the status code 200 for Healthy and Degraded, 503 for Unhealthy.

This file app has two checks: one that is always healthy and only says “the process answers”, and one for the database, which here is a stand-in with a switch. It starts the app, queries both addresses, takes the database away and queries again:

C#
// health.cs
#:sdk Microsoft.NET.Sdk.Web

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var database = new DatabaseStandIn();
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Logging.ClearProviders();
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"])
    .AddCheck("database", () => database.Reachable ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy("no connection"));

var app = builder.Build();
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") });
app.MapHealthChecks("/health/ready");
await app.StartAsync();

using var http = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
await Show("Database reachable");
database.Reachable = false;
await Show("Database gone");
await app.StopAsync();

async Task Show(string situation)
{
    Console.WriteLine(situation);
    foreach (string path in new[] { "/health/live", "/health/ready" })
    {
        using HttpResponseMessage response = await http.GetAsync(path);
        Console.WriteLine($"  {path,-14} {(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
    }
}

sealed class DatabaseStandIn
{
    public bool Reachable { get; set; } = true;
}
Bash
dotnet run health.cs
Database reachable
  /health/live   200 Healthy
  /health/ready  200 Healthy
Database gone
  /health/live   200 Healthy
  /health/ready  503 Unhealthy

The first line #:sdk Microsoft.NET.Sdk.Web turns the file app into a web app. UseUrls("http://127.0.0.1:0") lets the system pick a free port, and app.Urls then says which one. The difference between the two addresses is the core of this lesson: Without the database, the app still lives, but it is not ready.