Health Checks in ASP.NET Core

Health checks are a set of checks (duh) that you perform in order to tell whether an application/service is up, running & healthy or not. It's usually one or more endpoints that reports the status, the response differs from language/framework to an other.

Health checks are very useful especially when your application depends on other things like a database or even other services. Usually, you'll find the generated endpoint(s) used by an Orchestrator to figure out whether the app is still up or not (liveness).

Today we'll see how we can add health checks to an ASP.NET Core API.
All the code is available in this GitHub repository.

Built-in health checks

In ASP.NET Core, the package Microsoft.AspNetCore.Diagnostics.HealthChecks is used to add health checks to your application. This means that in every project, you have the ability to add health checks out of the box.

The package is referenced implicitly in ASP.NET Core 3+

Adding health checks is straightforward:

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

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  app.UseEndpoints(endpoints =>
  {
    endpoints.MapControllers();
    endpoints.MapHealthChecks("/health");
  });
}

This will inject a middleware that will, for now since we didn't configure any checks, report that the app is healthy.

Healthy

Custom checks

The package doesn't come with any built-in checks, it only provides the base. In order to add your own checks, there are two ways:

services.AddHealthChecks()
  .AddCheck("AlwaysHealthy", () => HealthCheckResult.Healthy())
  .AddCheck<MyCustomCheck>("My Custom Check");
  • AddCheck(string name, Func<HealthCheckResult> check), which takes no arguments and returns a status.
  • AddCheck<IHealthCheck>(string name), which takes a class that implements the interface IHealthCheck, where you can put your logic.
Your IHealthCheck classes are registered in the DI container, so you can resolve dependencies in the constructor!

This lets you perform all possible checks, but you'll have to write them manually.
Another downside is that the endpoint's response doesn't provide more information by default, maybe one day you'll want to know which checks fail and you'll have to write your own ResponseWriter.

Filtering checks

Imagine you have multiple custom checks:

services.AddHealthChecks()
  .AddCheck("AlwaysHealthy", () => HealthCheckResult.Healthy(), tags: new[] { "Tag1" })
  .AddCheck("AlwaysHealthyToo", () => HealthCheckResult.Healthy(), tags: new[] { "Tag1" })
  .AddCheck("AlwaysUnhealthy", () => HealthCheckResult.Unhealthy(), tags: new[] { "Tag2" });

We can create two endpoints, each one handling a tag:

app.UseEndpoints(endpoints =>
{
  endpoints.MapControllers();

  endpoints.MapHealthChecks("/health1", new HealthCheckOptions()
  {
      Predicate = (check) => check.Tags.Contains("Tag1")
  });

  endpoints.MapHealthChecks("/health2", new HealthCheckOptions()
  {
      Predicate = (check) => check.Tags.Contains("Tag2")
  });
});

If you visit /health1, it'll return Healthy and if you visit /health2, it'll return Unhealthy.
This is very useful if you want different health endpoints to check different things.

Advanced health checks

Instead of writing every check ourselves, the package AspNetCore.Diagnostics.HealthChecks comes to the rescue! It's an ASP.NET Core package that plugs into the existing health checks base and adds many custom checks, including:

  • SQL Server
  • MySQL
  • SQLite
  • RabbitMQ
  • Elasticsearch
  • Redis
  • System: Disk Storage, Private Memory, Virtual Memory, Process, Windows Service
  • Azure Storage: Blob, Queue and Table
  • Amazon S3
  • Network: Ftp, SFtp, DNS, TCP port, Smtp, Imap
  • MongoDB
  • Kafka
  • Kubernetes

The full list, which is a lot bigger, can be found in the repositories’ README.
What's even better is that it contains a UI too!

Database checks

AspNetCore.HealthChecks contains checks for the most popular database providers. Today we'll try the SQL Server one by installing the package AspNetCore.HealthChecks.SqlServer.

To keep the example short, I will not include the DbContext creation but you should have one, as well as the ConnectionString in appsettings.json
services.AddHealthChecks()
  .AddSqlServer(Configuration["ConnectionString"]); // Your database connection string

It's as simple as this. If you configured your DbContext and your SQL Server is running, the /health endpoint should return the status Healthy.

System checks

AspNetCore.HealthChecks.System contains many system related checks, let's look at a few of them:

services.AddHealthChecks()
  .AddSqlServer(Configuration["ConnectionString"]) // Your database connection string
  .AddDiskStorageHealthCheck(s => s.AddDrive("C:\\", 1024)) // 1024 MB (1 GB) free minimum
  .AddProcessAllocatedMemoryHealthCheck(512) // 512 MB max allocated memory
  .AddProcessHealthCheck("ProcessName", p => p.Length > 0) // check if process is running
  .AddWindowsServiceHealthCheck("someservice", s => s.Status == ServiceControllerStatus.Running); // check if a windows service is running

As you can see, there is a lot of useful checks! It would've been better if the Storage check checked the storage used instead of free, it can be very useful when working with containers.

URL checks

Imagine your app's health depends on an external API, you're using one of its endpoints in one of your functionalities. There is a health check for that scenario too!
By installing the package AspNetCore.HealthChecks.Uris, you can use:

services.AddHealthChecks()
  .AddSqlServer(Configuration["ConnectionString"]) // Your database connection string
  .AddDiskStorageHealthCheck(s => s.AddDrive("C:\\", 1024)) // 1024 MB (1 GB) free minimum
  .AddProcessAllocatedMemoryHealthCheck(512) // 512 MB max allocated memory
  .AddProcessHealthCheck("ProcessName", p => p.Length > 0) // check if process is running
  .AddWindowsServiceHealthCheck("someservice", s => s.Status == ServiceControllerStatus.Running) // check if a windows service is running
  .AddUrlGroup(new Uri("https://localhost:44318/weatherforecast"), "Example endpoint"); // should return status code 200

You can configure much more using the provided UriHealthCheckOptions, for example what status codes are considered good or bad, more than one URLs, etc…

UI

There is also a package that adds a monitoring UI that shows you the status of all the checks you added, as well as their history.

First, let's install the packages:

Then let's register the UI:

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

  services
    .AddHealthChecksUI(s =>
    {
        s.AddHealthCheckEndpoint("endpoint1", "https://localhost:44318/health");
    })
    .AddInMemoryStorage();

  // ...
}

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

  pp.UseEndpoints(endpoints =>
  {
      endpoints.MapControllers();
      endpoints.MapHealthChecksUI();

      endpoints.MapHealthChecks("/health", new HealthCheckOptions()
      {
          Predicate = _ => true,
          ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
      });
  });

  // ...
}

Here's what you'll find when you visit the /health endpoint:

{
  "status": "Unhealthy",
  "totalDuration": "00:00:00.0531378",
  "entries": {
    "diskstorage": {
      "data": {},
      "duration": "00:00:00.0003344",
      "status": "Healthy"
    },
    "process_allocated_memory": {
      "data": {},
      "description": "Allocated megabytes in memory: 11 mb",
      "duration": "00:00:00.0002289",
      "status": "Healthy"
    },
    "process": {
      "data": {},
      "duration": "00:00:00.0162908",
      "status": "Unhealthy"
    },
    "windowsservice": {
      "data": {},
      "duration": "00:00:00.0001123",
      "status": "Healthy"
    },
    "Example endpoint": {
      "data": {},
      "duration": "00:00:00.0514370",
      "status": "Healthy"
    },
    "sqlserver": {
      "data": {},
      "duration": "00:00:00.0319841",
      "status": "Healthy"
    }
  }
}

And if you visit /healthchecks-ui, which is the default URL for the UI:

Health Checks UI

Health Checks UI

We can also tell the UI to save the results in an actual database, let's try the SQLite one available in the package AspNetCore.HealthChecks.UI.SQLite.Storage.

All we have to do is replace the in memory call:

services
  .AddHealthChecksUI(s =>
  {
      s.AddHealthCheckEndpoint("endpoint1", "https://localhost:44318/health");
  })
  .AddSqliteStorage("Data Source = healthchecks.db");

Now every time the UI fetches the health checks at /health, it will also use the SQLite database to save a bunch of information.

Health Checks DB

Health Checks DB

Conclusion

In this post, we saw what ASP.NET Core provides by default regarding health checks. Microsoft made a very extensible system where other developers can create all sorts of checks while keeping the same infrastructure.

We also saw some of the AspNetCore.HealthChecks packages that plug on top of ASP.NET Core's health checks to add the most popular checks anyone would ever need. Not only that, but also a monitoring UI that is customizable when you provide it your CSS file.

What we didn't see in this post is the ability to have hooks (e.g. Slack) that listen on the application's health, which AspNetCore.HealthChecks also provides.

As a reminder, all the code is available in this GitHub repository. I hope this post was useful, see you around!

Zanid Haytam Written by:

Zanid Haytam is an enthusiastic programmer that enjoys coding, reading code, hunting bugs and writing blog posts.