How to Implement Health Checks ASP.NET Core

Finding out the status of a service or other services that our service depends on is made easier by health checks. If your services are being loaded across multiple service nodes by a load balancer, for instance, Health checks can be useful. The load balancer can use the node’s health status to direct requests to the healthy node if there are multiple nodes. To find out how the dependent services are doing, we could also conduct health checks.

For instance, we could use the health status of the API or database server that our service uses to determine the health of our service. In this scenario, we could downgrade the health status of our service to degraded or unhealthy if the dependent service is in a degraded or unhealthy status. Health checks are endpoints in ASP.Net Core APIs that reveal the service health to other services.

We must first register health check services with AddHealthChecks in the ConfigureServices method of the Startup class in order to add a fundamental health check to an ASP.Net Core application. Then, using the MapHealthChecks extension method, we must add the EndpointMiddleware to the IApplicationBuilder and add a health check endpoint. We can give the endpoint any name we like.

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

    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHealthChecks("/health");
        });
    }
}

ASP.NET Core 6

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();

We must first implement the IHealthCheck interface before we can create a custom health check. We must specifically implement the IHealthCheck interface’s CheckHealthAsync method. To determine the health status of the HealthCheckResult, we can perform any tasks inside the method.

Using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace WeatherApi;

public class MemoryHealthCheck : IHealthCheck
{
    private const long Threshold = 1024L * 1024L * 1024L;

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var allocatedBytesInManagedMemory = GC.GetTotalMemory(forceFullCollection: false);
        var data = new Dictionary<string, object>()
        {
            { "AllocatedBytesInManagedMemory", allocatedBytesInManagedMemory },
            { "NumberOfTimesGarbageCollectionOccurredForGen0Collection", GC.CollectionCount(0) },
            { "NumberOfTimesGarbageCollectionOccurredForGen1Collection", GC.CollectionCount(1) },
            { "NumberOfTimesGarbageCollectionOccurredForGen2Collection", GC.CollectionCount(2) }
        };

        if (allocatedBytesInManagedMemory > Threshold)
        {
            var result = new HealthCheckResult(
                status: context.Registration.FailureStatus,
                description: $"Allocated bytes in managed memory is > {Threshold}",
                data: data);

            return Task.FromResult(result);
        }

        var healthyResult = new HealthCheckResult(
            status: HealthStatus.Healthy,
            description: $"Allocated bytes in managed memory is <= {Threshold}",
            data: data);

        return Task.FromResult(healthyResult);
    }
}

To determine if the service is healthy, I only consider a condition from the health check mentioned above. MemoryHealthCheck returns the default failure status (Unhealthy by default) if it is not healthy. When we add the health check to the IHealthChecksBuilder, we have the option to alter the default failure status for the health check.

builder.Services
    .AddHealthChecks()
    .AddCheck<MemoryHealthCheck>(name: "memory", failureStatus: HealthStatus.Degraded);

Customize the HTTP status codes and response for the health check

Regardless of the health check status, a health check endpoint will by default return a 200 OK status code when we call it. Depending on the results of the health check, it may be useful to generate a specific status code at times. This can be done by setting the HealthCheckOptionsResultStatusCodes property. In the example below, the endpoint will return a 500 InternalServerError if the health status has deteriorated. The endpoint will respond with a 503 ServiceUnavailable response when the health status is unhealthy.

app.MapHealthChecks(pattern: "/ready", options: new HealthCheckOptions
{
    ResponseWriter = WriteHealthCheckResponseAsync,
    ResultStatusCodes =
    {
        [HealthStatus.Degraded] = StatusCodes.Status500InternalServerError,
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable,
    },
    Predicate = _ => true
});

For health checks, the default responses (report) are Healthy, Unhealthy, or Degraded. By giving a delegate to the ResponseWriter of HealthCheckOptions, we can alter the response (report).

static Task WriteHealthCheckResponseAsync(
    HttpContext httpContext, HealthReport healthReport)
{
    httpContext.Response.ContentType = "application/json";

    var dependencyHealthChecks = healthReport.Entries.Select(entry => new
    {
        Name = entry.Key,
        Discription = entry.Value.Description,
        Status = entry.Value.Status.ToString(),
        DurationInSeconds = entry.Value.Duration.TotalSeconds.ToString("0:0.00"),
        Data = entry.Value.Data,
        Exception = entry.Value.Exception?.Message
    });

    var healthCheckResponse = new
    {
        Status = healthReport.Status.ToString(),
        TotalCheckExecutionTimeInSeconds = healthReport.TotalDuration.TotalSeconds.ToString("0:0.00"),
        DependencyHealthChecks = dependencyHealthChecks
    };

    var serializerOptions = new JsonSerializerOptions
    {
        WriteIndented = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    var responseString = JsonSerializer.Serialize(healthCheckResponse, serializerOptions);

    return httpContext.Response.WriteAsync(responseString);
}

The above delegate creates the following report.

{
  "status": "Healthy",
  "totalCheckExecutionDurationInSeconds": "0:0.12",
  "dependencyHealthChecks": [
    {
      "name": "memory",
      "discription": "Reports memory status of the application.",
      "status": "Healthy",
      "durationInSeconds": "0:0.02",
      "data": {
        "AllocatedBytesInManagedMemory": 1907368,
        "NumberOfTimesGarbageCollectionOccurredForGen0Collection": 2,
        "NumberOfTimesGarbageCollectionOccurredForGen1Collection": 2,
        "NumberOfTimesGarbageCollectionOccurredForGen2Collection": 0
      }
    }
  ]
}

Your application may subject you to a number of health checks. You can use tags and the Predicate property of HealthCheckOptions to filter out the health checks you need if you do not need to return the status of every health check. A HealthCheckRegistration type parameter is taken by the predicate property. The HealthCheckRegistration‘s Tags property can be used to filter the health checks. When adding the health checks, we can set the tag(s) using the AddCheck extension’s tags parameter.

builder.Services
    .AddHealthChecks()
    .AddCheck<MemoryHealthCheck>(
        name: "memory", 
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "memory" });

COPY

COPY
app.MapHealthChecks(pattern: "/ready", options: new HealthCheckOptions
{
    Predicate = registration => registration.Tags.Contains("memory")
}); ;

Thanks for reading.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *