Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

ASP.NET Core Health Checks Quick Start

DZone 's Guide to

ASP.NET Core Health Checks Quick Start

Schedule a quick check-up for your ASP.NET Core system.

· Web Dev Zone ·
Free Resource

ASP.NET Core comes with built-in support for health checks that allow us to monitor system health. It's not about logging or advanced monitoring — it's about giving a quick assessment on whether the system is okay or not. This blog post shows how ASP.NET Core health checks work.

Health Checks at a Glance

A health check is a quick check for system health. It can be a simple yes-no style check, but it can also be a check of multiple components. A health check is an indicator that provides brief information assessing health. It's like visiting a doctor for regular health check-ups. The doctor makes multiple checks, quick ones, and tells if everything is okay or not. If something needs more attention, then the doctor agrees with the patient to make a special appointment and get a better idea of what's going on.

It's similar to the health checks for systems. Health checks don't provide the following:

  • Extensive logs of the health status of the system,
  • Analytic and telemetric data to debug system,
  • Deeper monitoring data.

Health checks are just about the current health status of the system, and maybe it's components.

From practice, I can tell that most useful and painless health checks are small and fast ones that don't put much load on the system. Health checks that take 30 seconds are a clear indication that something is terribly wrong with the implementation.

But what if we need to save health checks history? Well, I consider it as a task for monitoring systems that request health status with a given interval and log it to their own data storage. Health checks we write must provide this information in a fast and reliable way.

Health Checks in ASP.NET Core

ASP.NET Core has health checks support available out-of-box. The most primitive way to make it work is just to enable it in Startup class.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddHealthChecks();

        // ...
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...

        app.UseHealthChecks("/hc");

        // ...
    }
}


It does nothing very intelligent. It creates a route for health checks and tells us whether that system is healthy. Currently, we have no checks defined and by default our system is considered as healthy.

What is system health? Well, it's up to us to define. ASP.NET Core just provides us with a minimalistic base to do it.

Checking Connection to External System

One of the most popular health checks is checking if the connection to some external system works. It's a good example because it's a little bit tricky and it's perfect to demonstrate all health check statuses.

Let's think for a moment what could possibly go wrong if we try to ping some other machine in the network. My list of the most important scenarios are below:

  1. Other machines are not available or refuse connection (possible case for exception).
  2. Other machines are available but the connection or machine itself is very slow (our health check for this external dependency may take more time than expected).

From here, I get three possible statuses that ASP.NET Core supports:

  • Healthy — ping succeeded with no errors and timeouts
  • Degraded — ping succeeded but it took too long
  • Unhealthy — ping failed or exception was thrown.

First, I demonstrate how to write an inline health check directly to the Startup class. Yes, I know, it's too much code for simple lambda in the Startup class but we will change it later.

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddHealthChecks()
            .AddCheck("ping", () =>
            {
                try
                {
                    using (var ping = new Ping())
                    {
                        var reply = ping.Send("www.google.com");
                        if (reply.Status != IPStatus.Success)
                        {
                            return HealthCheckResult.Unhealthy();
                        }

                        if (reply.RoundtripTime > 100)
                        {
                            return HealthCheckResult.Degraded();
                        }

                        return HealthCheckResult.Healthy();
                    }
                }
                catch
                {
                    return HealthCheckResult.Unhealthy();
                }
            });

    // ...
}


If I change the address of the external system to something that doesn't exist in my local network, then health check fails when I refresh health check page in browse.

The same way, we can use very small ping timeout to try the Degraded status.

Using Health Check Classes

Let's move our health check to a separate class. It's not a hack but a supported scenario in ASP.NET Core. There's the IHealthCheck interface we can use for this.

public class PingHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            using (var ping = new Ping())
            {
                var reply = await ping.SendPingAsync("www.google.com");
                if (reply.Status != IPStatus.Success)
                {
                    return HealthCheckResult.Unhealthy();
                }

                if (reply.RoundtripTime > 100)
                {
                    return HealthCheckResult.Degraded();
                }

                return HealthCheckResult.Healthy();
            }
        }
        catch
        {
            return HealthCheckResult.Unhealthy();
        }
    }
}


In the Startup class, we must register our custom health check type so that ASP.NET Core knows it.

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddHealthChecks()
            .AddCheck<PingHealthCheck>("ping");

    // ...
}


We can keep the code of our health checks in separate classes and the Startup class is clean again.

Making Ping Check Configurable

I'm sorry, but now, software architecture and design are taking over my sober senses and I will need to make a quick jump away from health checks for a moment. In practice, we hardly see systems with no external dependencies these days. Ping test can be primitive but it's easy to implement and use. Putting these two things together, we can build a general class for ping checks.

public class PingHealthCheck : IHealthCheck
{
    private string _host;
    private int _timeout;

    public PingHealthCheck(string host, int timeout)
    {
        _host = host;
        _timeout = timeout;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            using (var ping = new Ping())
            {
                var reply = await ping.SendPingAsync(_host, _timeout);
                if (reply.Status != IPStatus.Success)
                {
                    return HealthCheckResult.Unhealthy();
                }

                if (reply.RoundtripTime >= _timeout)
                {
                    return HealthCheckResult.Degraded();
                }

                return HealthCheckResult.Healthy();
            }
        }
        catch
        {
            return HealthCheckResult.Unhealthy();
        }
    }
}


Now, we can add ping check for multiple external end-points, as shown here.

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddHealthChecks()
            .AddCheck("ping1", new PingHealthCheck("www.google.com", 100))
            .AddCheck("ping2", new PingHealthCheck("www.bing.com", 100));

    // ...
}


But the output of ping check will remain the same. There's only one answer and this is all we get back right now.

Displaying Status of Multiple Health Checks

If we want to show more data about health check of our system, we can do it by building a custom output writer and using health check options to inject it into the health checks feature. Credits go to Dejan Stojanovic.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...

    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("/hc", options);

    // ...
}


This writer combines all health check results together and shows the status of each of these as JSON.

{
  "status": "Healthy",
  "errors": [
    {
      "key": "ping1",
      "value": "Healthy"
    },
    {
      "key": "ping2",
      "value": "Healthy"
    }
  ]
}


We can read this JSON from monitoring tools and scripts to save or display health checks history.

Wrapping Up

Although health checks in ASP.NET Core seem a bit basic and minimalistic, it is good base for implementing system-specific health checks. It's easy, logical, and not too tricky to get the most important concepts. I especially like how flexible it is. When writing health checks, we have to follow the best practices and keep all checks small and fast so they don't put an unexpected load onto system components and external services.

Topics:
web dev ,asp.net ,health checks ,health ,system health ,json

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}