Health checks are a critical part of any distributed system. Especially in the era of microservices, it is extremely important to understand if the service is running healthy or not. In this blog, I will walk through how to implement Health Checks in ASP.Net Core.
ASP.Net Core provides support for health checks out of the box through middleware and extension methods.
What are the different aspects of an application that we need through health checks?
- Firstly, finding out dependencies such as a database or other services are alive or not
- Secondly, the usage of physical resources on the machine
We usually use the health checks for monitoring applications and to see how the applications are behaving. The health check results are also in use for scaling out applications based on how the service is degrading in response time.
To further dig deeper into the topic let us start by creating an ASP.Net Core Web application. And let us demonstrate the feature.
Creating a new ASP.Net Core Web Application
First of all, we will create a new ASP.Net Core web application. To achieve that, I will open up Visual Studio 2019, and select the menu option File -> New -> Project.
Once the new project creation window pops up, I will select the ASP.Net Core Web Application. And then I will click on the Next button.
Secondly, on the next page of the pop-up, I will provide the name of the project in the Project Name field as HealthCheck.Demo. And then click on the Create button.
Finally, on the final page, I will select the API template option. And I will keep other values default (ASP.Net Core 3.1) and click on the Create button.
Setting up Health Checks in ASP.Net Core
Once the project is created, I will set up a basic health check. For that, I will start with the Startup class.
Change in ConfigureServices method
In the ConfigureServices
of the Startup
class, I will add the health check system into the dependency injections container. For that, I will call the AddHealthChecks
extension method on the IServiceCollection
instance. The method will return an instance of IHealthChecksBuilder
.
As the name suggests the IHealthChecksBuilder
provides methods to set up and chain health checks.
As a first step, I will call the AddCheck
method on the IHealthChecksBuilder
instance returned by the AddHealthChecks
.
The AddCheck
takes two parameters, the first one is a name for the health check. And the second one is a Func
delegate which returns the HealthCheckResult
struct.
For the time being, I will just return HealthCheckResult.Healthy
as the return.
services.AddHealthChecks()
.AddCheck("Test Health Check", () => HealthCheckResult.Healthy());
Change in Configure method
Once the dependency injection container setup is complete, it is time to set up the health check middleware. For that, I will update the implementation of the UseEndpoints
extension method call to the IApplicationBuilder
instance.
Inside of the delegate call for the UseEndpoints
method parameter; I will call the MapHealthChecks
extension method on the IEndpointRouteBuilder
instance.
The MapHealthChecks
method takes the URL pattern as the parameter. I will pass api/health as the parameter value. You can use what suits your application.
Microsoft uses /health as the endpoint in all the examples of their documentation here.
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("api/health");
endpoints.MapControllers();
});
The complete change in code inside of the Startup
class is below:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
namespace HealthCheck.Demo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHealthChecks()
.AddCheck("Test Health Check", () => HealthCheckResult.Healthy());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("api/health");
endpoints.MapControllers();
});
}
}
}
Running the application
Once the code is complete, I will run the application to test the health check endpoint.
After running the application, if I go to the /api/health endpoint I will see the response as Healthy.
Using Multiple Health Checks Providers
Now, in a real-life scenario, we will never return a HealthCheckResult.Healthy
from the health checks return and call it a day. For health checks, we will try to validate multiple aspects of the application to see if they are working as expected.
For example, let us say we want to check the state of our database. And make sure that we are able to successfully connect to the database.
For that, we will create a new class, DbHealthCheckProvider
. The only responsibility of this class is to connect to the database. And make sure that the connection was successful.
I will create this class as a static class and it will have a single static method Check
. The method Check will take the connection string of the database. Now since that class will be called from the Startup
class, and will not be used anywhere, it is fair to do this. Also, we will never have to test this class, for obvious reasons; it does not add any value.
For this to work, I will install the NuGet package System.Data.SqlClient.
Inside of the Check
method, I will open a connection to my local SQL Server And if the connection was successful, I will return HealthCheckResult.Healthy
. Otherwise, I will return HealthCheckResult.Unhealthy
from the method.
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Data.SqlClient;
namespace HealthCheck.Demo
{
public static class DbHealthCheckProvider
{
public static HealthCheckResult Check(string connectionString)
{
// Code to check if DB is running
try
{
using var connection = new SqlConnection(connectionString);
connection.Open();
return HealthCheckResult.Healthy();
}
catch
{
return HealthCheckResult.Unhealthy();
}
}
}
}
Setting up Startup class
Once the DbHealthCheckProvider
class is ready, I will set up the Startup
to use this class.
For that, I will update the ConfigureServices
method. And I will change the delegate implementation inside the AddCheck
method. Instead of calling an inline delegate to return HealthCheckResult.Healthy
. Now, I will call the DbHealthCheckProvider.Check
.
And for the database connection string, I will pass an empty string. This should result in an Unhealthy
response when we run the application.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHealthChecks()
.AddCheck("DB Health Check", () =>
DbHealthCheckProvider.Check(""));
}
Once the code inside of Startup
is configured to use the new DbHealthCheckProvider
class, I will run the application.
As expected, when we navigate to the /api/health endpoint, we can see an Unhealthy response back.
In the above console response, we can see that the Health Checks system logs a fail in the console. This log can be used to identify what exactly failed. As you can see it prints the name of the health checks “DB Health Check”.
The HealthCheckResult.Unhealthy
or HealthCheckResult.Healthy
method takes an optional string description parameter. This is very handy when it comes to logging.
To demonstrate that I will update the implementation of the HealthCheckResult.Unhealthy
method inside of the DbHealthCheckProvider
. And I will add a description to the Unhealthy
method.
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Data.SqlClient;
namespace HealthCheck.Demo
{
public static class DbHealthCheckProvider
{
public static HealthCheckResult Check(string connectionString)
{
// Code to check if DB is running
try
{
using var connection = new SqlConnection(connectionString);
connection.Open();
return HealthCheckResult.Healthy();
}
catch
{
return HealthCheckResult.Unhealthy("Could not connect to database!");
}
}
}
}
Now if I run the application and navigate to the /api/health endpoint. I will see the description text “Could not connect to database!” will print out in the console.
Queue Health Checks Provider
Now, let us say we our application also connects to a RabbitMQ server for processing messages. Now to check if we are able to connect to RabbitMQ server, we will need to write a health check provider.
Putting all health checks inside of a single class makes things difficult to manage. As well as harder to find out what was the cause of the issue.
Hence, I will create a new class, MqHealthCheckProvider
. This will also be a static class with a single static method Check
. For this class, we will just return HealthCheckResult.Healthy
from the method, we will not implement any real RabbitMQ connectivity for the demo.
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace HealthCheck.Demo
{
public static class MqHealthCheckProvider
{
public static HealthCheckResult Check(string mqUri)
{
// Code to check if MQ is running
return HealthCheckResult.Healthy();
}
}
}
Once the MqHealthCheckProvider
is ready, I will update the Startup
class to configure the MqHealthCheckProvider
.
Since the AddCheck
method also returns IHealthChecksBuilder
instance, I will chain another AddCheck
to configure the MqHealthCheckProvider
.
And for this case as well when we call the Check
method of the MqHealthCheckProvider
class, we will pass the MQ Server string as an empty string.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHealthChecks()
.AddCheck("DB Health Check", () =>
DbHealthCheckProvider.Check(""))
.AddCheck("MQ Health Check", () =>
MqHealthCheckProvider.Check(""));
}
Now when I run the application, I will still see the response as Unhealthy. This is because the health check is the aggregation of all the health checkpoints configured.
Hence, I will update the DbHealthCheckProvider.Check
call to pass a valid connection string to make that call as healthy as well.
I will get the connection string from the appsettings.json
file.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHealthChecks()
.AddCheck("DB Health Check", () =>
DbHealthCheckProvider.Check(Configuration["Connection"]))
.AddCheck("MQ Health Check", () =>
MqHealthCheckProvider.Check(""));
}
Now if I run the application, I will see Healthy in response.
Using Generic AddCheck method
The next thing I am going to walk through is how to use the generic AddCheck
method. For that let us consider our application is interacting with third part SAAS software like Sendgrid through an HTTP connection.
Firstly, I will create a new class that is responsible for checking if an HTTP request to Sendgrid is successful or not. I will name the class as SendgridHealthCheckProvider
.
This class will implement the interface IHealthCheck
, which is part of the namespace Microsoft.Extensions.Diagnostics.HealthChecks
. I will implement the method CheckHealthAsync
from the interface.
For the time being for the demo, I will return HealthCheckResult.Healthy
from the method. I will not make a real HTTP call outside.
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Threading;
using System.Threading.Tasks;
namespace HealthCheck.Demo
{
public class SendgridHealthCheckProvider : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult(HealthCheckResult.Healthy());
}
}
}
Once the SendgridHealthCheckProvider
is ready, I will go and update the Startup
class to configure it.
For the configuration, I will use the generic version of the AddCheck
method.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHealthChecks()
.AddCheck("DB Health Check", () => DbHealthCheckProvider.Check(Configuration["Connection"]))
.AddCheck("MQ Health Check", () => MqHealthCheckProvider.Check(""))
.AddCheck<SendgridHealthCheckProvider>("Sendgrid Health Check");
}
Finally, I will run the application. And as expected, I will get a Healthy response from the API call.
Implementing Degraded option
One of the options of the HealthCheckResult
struct that we have not tried so far is the Degraded
. Now the question is when we will use this option?
Whenever a database or some external service is taking more than the SLA, we can send a Degraded response. This can be achieved by using a timer to check the time taken by the external dependency.
To demonstrate that, let us update the MqHealthCheckProvider
class. Now in the Check
method, it will return HealthCheckResult.Degraded
.
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace HealthCheck.Demo
{
public static class MqHealthCheckProvider
{
public static HealthCheckResult Check(string mqUri)
{
// Code to check if MQ is running
return HealthCheckResult.Degraded("MQ is running slow!");
}
}
}
Now if we run the application, we can see that the response is Degraded.
And also as expected the console log will print a warning, along with the same of the health check. And the description message “MQ is running slow!”.
Health Checks in ASP.Net Core through Postman
Now, the question is, how the monitoring tools will interpret this response. Well, the best way to implement that will be through checking the HTTP status code. And if needed further investigation then checking the response body.
Let us run the requests through the postman and see how it all works. So to test that, we will just run the application as it is running now. Which means it will return a Degraded response.
If we navigate to the /api/health, we will see the HTTP Status code is still 200, but the response body is Degraded.
Now let us see how an Unhealthy response will look like in postman. Specifically what status code will it return.
For doing that, let us just update the Startup
classes ConfigureServices
method. And for the AddCheck
of the DbHealthCheckProvider
, let us pass an empty connection string. This will create an Unhealthy response.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHealthChecks()
.AddCheck("DB Health Check", () => DbHealthCheckProvider.Check(""))
.AddCheck("MQ Health Check", () => MqHealthCheckProvider.Check(""))
.AddCheck<SendgridHealthCheckProvider>("Sendgrid Health Check");
}
Once I make this change, let us run the application and check the postman response.
In the response, we can see that the HTTP status code is 503 Service Unavailable. And the response string is Unhealthy. Based on the HTTP status code any motoring tool can find out if the service is up or down.
For a Healthy response, we will get an HTTP Status Code of 200 and Healthy in the response string.
Conclusion
The Health Checks in ASP.Net Core implementation is really awesome in my opinion. It is super simple to implement and integrate with individual implementation classes.
I have done a YouTube video going through the concepts here.
The source code is available in Github here.