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 quick information if system is okay or not. This blog post shows how ASP.NET Core health checks work.
Health checks in glance
Health check is quick check for system health. It can be simple yes-no style check but it can also be check of multiple components. Health check is indicator giving brief information. It’s like visiting a doctor for regular healt checking. Doctor makes multiple checks, the quick ones, and tells if everything is okay or not. If something needs more attention then doctor agrees with patient for special appointment to get better idea what’s going on.
Almost same way work also health checks for systems. Health checks doesn’t provide:
- extensive logs of health status of system,
- analytic and telemetric data to debug system,
- deeper monitoring data.
Health checks are just about the current health status of system and maybe it’s components.
From practice I can tell that most useful and painless health checks are small and fast ones that doesn’t put much load to system. Health check that takes 30 seconds is clear indication that something is terribly wrong with 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 given interval and log it to their own data storage. Health checks we write must provide this information fast and reliable way.
Health checks in ASP.NET Core
ASP.NET Core has healthchecks 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 route for health checks and tells us 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 minimalistic base to do it.
Checking connection to external system
One of most popular health checks is checking if connection to some external system works. It’s 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 may possibly go wrong if we try to ping some other machine in network. My list of most important scenarios is here:
- Other machine is not available or refuses connection (possible case for exception).
- Other machine is available but connection or the machine itself is very slow (our health check for this external dependency may take more time than expected).
From here I get to 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 inline health check directly to Startup class. Yes, I know, it’s too much code for simple lambda in 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 address of external system to something that doesn’t exist in my local networkt then health check fails when I refresh health check page in browse.
Same way we can use very small ping timeout to try Degraded status.
Using health check classes
Let’s move our health check to separate class. It’s not hack but supported scenario in ASP.NET Core. There’s 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 Startup class we must register our custom health check type so ASP.NET Core knows it.
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHealthChecks()
.AddCheck<PingHealthCheck>("ping");
// ...
}
We can keep code of our health checks in separate classes and Startup class is clean again.
Making ping check configurable
I’m sorry but now software architecture and design is taking over my sober senses and I will make a quick jump away from health check 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 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 like 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 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 custom output writer and use health check options to injects it to 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 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 seems basic and miminalistic it is good base for implementing system specific health checks. It’s easy, it’s logical and not too tricky to get most important concepts. I specially 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 doesn’t put unexpected load to system components and external services.