ASP.NET Core 2.2 Health Checks Explained

This is part 7 in our series about ASP.NET Core:

ASP.NET Core 2.2 introduces a range of new features. One of the more interesting (IMO) is Health Checks. You may use tools like Pingdom or elmah.io Uptime Monitoring to ping your website in a specified interval. Pinging a single HTML page may or may not reveal if your application is healthy or not. Health Checks to the rescue! Before trying out the code yourself, make sure to install the recent version of ASP.NET Core 2.2 and Visual Studio 2017. The behavior of health checks has changed from preview to preview.

ASP.NET Core Health Checks Explained

When developing a new service, I typically define a custom route (like /working). This endpoint is requested by Uptime Monitoring, and a notification is sent if things stop working. How /working is implemented, is up to each API. Some APIs may require a database connection, while others need something completely else. Until now, one of my custom health endpoints could have looked something like this:

[Route("working")]
public ActionResult Working()
{
    using (var connection = new SqlConnection(_connectionString))
    {
        try
        {
            connection.Open();
        }
        catch (SqlException)
        {
            return new HttpStatusCodeResult(503, "Generic error");
        }
    }

    return new EmptyResult();
}

In this example, the /working endpoint returns 503 if we couldn't open a connection to a SQL Server, but could be another database or any other resource.

With the new Health Checks feature in ASP.NET Core 2.2, health checks no longer require you to create new controllers like in the example above. Health checks are enabled and configured through Startup.cs:

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

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...
    app.UseHealthChecks("/working");
    ...
}

With the sample above, a new endpoint named /working can be requested by your preferred uptime monitoring system or simply through the browser:

Healthy

In this example, requesting the endpoint will return a status code 200 together with the body Healthy, indicating that the API is reachable. If we want to replicate the controller action in the first example, we have a couple of options. Health checks that you only need in a single API can be specified directly as a new check:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services
        .AddHealthChecks()
        .AddCheck("sql", () =>
        {
            using (var connection = new SqlConnection(_connectionString))
            {
                try
                {
                    connection.Open();
                }
                catch (SqlException)
                {
                    return HealthCheckResult.Failed();
                }
            }

            return HealthCheckResult.Passed();
        });
    ...
}

Requesting the /working endpoint automatically executes all lambdas provided to invocations of the AddCheck-method. In the case of a SqlException thrown inside the health check, a status code of 503 and body Unhealthy is returned:

Unhealthy

A better approach is to write a re-usable health check, contained in its class:

public class SqlServerHealthCheck : IHealthCheck
{
    SqlConnection _connection;

    public string Name => "sql";

    public SqlServerHealthCheck(SqlConnection connection)
    {
        _connection = connection;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        try
        {
            connection.Open();
        }
        catch (SqlException)
        {
            return HealthCheckResult.Failed();
        }
        
        return HealthCheckResult.Passed();
    }
}

Configuring the new health check only requires a single line of code:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services
        .AddHealthChecks()
        .AddCheck<SqlServerHealthCheck>("sql");
    ...
}

Both lambda-based and IHealthCheck-based checks can be mixed and matched. When adding multiple checks, ASP.NET Core executes each check in the order they were added. All checks needs to return HealthCheckResult.Passed() in order for the request to be successful. This means that a single failing health check, will cause an Unhealthy response.

Health checks are a great addition to an already excellent platform. In time, health checks will (hopefully) exist for most possible dependencies. In fact, the BeatPulse project, has started to migrate their checks to ASP.NET Core Health Checks. Checks for popular databases like SQL Server, MySQL and Oracle already exists. I hope for .NET compatible software as well as the community to pick up the task of writing reusable health checks for all sorts of resources.

Dependency injection

As shown in the code for SqlServerHealthCheck, health checks added using the AddCheck<T>-method can use dependency injection, just as you know if from other areas of ASP.NET Core. Health checks are created as transient objects, meaning that a new instance is created every time the /working endpoint is requested. If you want another lifecycle, health checks can be added manually like this:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services
        .AddHealthChecks()
        .AddCheck<SqlServerHealthCheck>("sql");
    services.AddSingleton<SqlServerHealthCheck>();
    ...
}

Here, the SqlServerHealthCheck is added as a singleton.

Custom status codes

Health checks comes with a range of status codes out of the box. Like 503 for unhealthy responses. In some cases you may want to change the codes, in order to make the health endpoint work with a client expecting a certain status code.

To change a status code, you can provide custom options when calling the UseHealthChecks-method:

var options = new HealthCheckOptions();
options.ResultStatusCodes[HealthStatus.Unhealthy] = 418;
app.UseHealthChecks("/working", options);

In this example, I replace the status code for the Unhealthy state with 418 (I'm a teapot).

Custom response

Much like the returned status codes, the body of the response can be customized as well:

var options = new HealthCheckOptions();
options.ResponseWriter = async (c, r) =>
{
    c.Response.ContentType = "application/json";

    var result = JsonConvert.SerializeObject(new
    {
        status = r.Status.ToString(),
        errors = r.Entries.Select(e => new { key = e.Key, value = e.Value.Status.ToString() })
    });
    await c.Response.WriteAsync(result);
};
app.UseHealthChecks("/working", options);

In the example, a JSON object containing the overall status and status of each health check is returned.

Custom response

I expect Microsoft and/or the community to come up with multiple health check response writers. A writer able to serialize the result to draft-inadarei-api-health-check-02 or rfc7807 would make a lot of sense.

Visualizing health checks

Currently, the best option for visualizing health check results, is through an open source project named HealthChecksUI. There's an integration for Application Insights (AI), but it simply logs the result as a metric and there is no good visualization option for it on AI.

To enable HealthChecksUI, install the AspNetCore.HealthChecks.UI package:

Install-Package AspNetCore.HealthChecks.UI

Enable the UI in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecksUI();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecksUI();
}

HealthChecksUI can be accessed on /health-ui:

HealthChecksUI

Credit: Xabaril


Features steps
We monitor your websites

We monitor your websites

We monitor your websites for crashes and availability. This helps you get an overview of the quality of your applications and to spot trends in your releases.

We notify you

We notify you

We notify you when errors starts happening using Slack, HipChat, mail or other forms of communication to help you react to errors before your users do.

We help you fix bugs

We help you fix bugs

We help you fix bugs quickly by combining error diagnostic information with innovative quick fixes and answers from Stack Overflow and social media.

See how we can help you monitor your website for crashes Monitor your website