HTTP API Health Checks (Readiness)

API Health Checks

Motivation

The examples in this post are for a HTTP health check see Health Checks for detailed motivation and alternatives like TCP. Health checks can include any downstream dependencies for your app like Database, Queue Service ect, in this post Im just looking at downstream HTTP dependencies.

We can leverage and extend the existing Microsoft.Extensions.Diagnostics.HealthChecks to check the APIs downstream dependencies are operational. This can be done with a simple New Relic Synthetic Monitor that polls http://myapi/healthcheck which would then in turn poll http://dependency-1/ping ect

The healthcheck endpoint should at very least require authorization in the form of a scope check.

Code Example

  1. Install the following packages
  • Microsoft.AspNetCore.Diagnostics.HealthChecks
  • Microsoft.Extensions.Diagnostics.HealthChecks
  1. Create a contract each dependency must filfull. Here its helpful for the method to return a tuple of bool isHealthy and string? description as this will be displayed when http://myapi/healthz is queried.
1
2
3
4
5
6
7
8
public interface IHealthCheckClient
{
/// <summary>
/// Interface to be implemented by Services and used by the ServiceHealthCheck
/// </summary>
/// <returns></returns>
public Task<(bool isHealthy, string? description)> IsHealthyAsync();
}
  1. Now leverage inheritance to extend the clients interface, this is a bit of a hack but the alternative is to add IsHealthyAsync to each clients interface -_-
1
2
3
4
public interface IDependency1Client : IHealthCheckClient
{
...
}
  1. Now implement the IsHealthyAsync method from IHealthCheckClient to query the ping endpoint. Example: http://dependency-1/ping
1
2
3
4
5
6
7
8
9
10
11
public class Dependency1Client : IDependency1Client
{
public async Task<(bool isHealthy, string? description)> IsHealthyAsync()
{
var response = await _client.GetAsync("/ping");

return response.IsSuccessStatusCode
? (true, null)
: (false, $"Service failed with {response.StatusCode} at ping endpoint");
}
}
  1. Create a service health check class that takes a generic type where the type is IHealthCheckClient and additionally implement IHealthCheck
  • \HealthChecks\ServiceHealthCheck.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace MyApi.HealthChecks;

public class ServiceHealthCheck<T> : IHealthCheck where T : IHealthCheckClient
{
private readonly IHealthCheckClient _service;

public ServiceHealthCheck(T service)
{
_service = service;
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var (isHealthy, description) = await _service.IsHealthyAsync();
var healthStatus = isHealthy ? HealthStatus.Healthy : HealthStatus.Unhealthy
return new HealthCheckResult(healthStatus, description);
}
}
  1. Extend the the service collection in Program.cs to add named health checks, its a fluent api so you can chain them.
1
2
3
services.AddHealthChecks()
.AddCheck<ServiceHealthCheck<IDependency1Client>>("Dependency1Service")
.AddCheck<ServiceHealthCheck<IDependency2Client>>("Dependency2Service");
  1. Still in Program.cs expose the healthz endpoint. The z in healthz is often used as a shorthand for “health check”. In the context of software development, a “health check” is a routine that tests whether a particular component of a system is functioning properly. The “z” is added to the end of “health” to create a unique and distinct word that can be used as a URL endpoint for checking the health of a particular system or component. This convention is often used in the context of microservices architecture, where multiple small services are combined to create a larger system.
1
2
app.MapHealthChecks("/healthz")
.RequireAuthorization();

You can also use a ResponseWriter to format the response nicely.

References