Finding out the status of a service or other services that our service depends on is made easier by health checks. If your services are being loaded across multiple service nodes by a load balancer, for instance, Health checks can be useful. The load balancer can use the node’s health status to direct requests to the healthy node if there are multiple nodes. To find out how the dependent services are doing, we could also conduct health checks.
For instance, we could use the health status of the API or database server that our service uses to determine the health of our service. In this scenario, we could downgrade the health status of our service to degraded or unhealthy if the dependent service is in a degraded or unhealthy status. Health checks are endpoints in ASP.Net Core APIs that reveal the service health to other services.
We must first register health check services with AddHealthChecks in the ConfigureServices
method of the Startup class in order to add a fundamental health check to an ASP.Net Core application. Then, using the MapHealthChecks
extension method, we must add the EndpointMiddleware
to the IApplicationBuilder
and add a health check endpoint. We can give the endpoint any name we like.
ASP.NET Core 3.1 – 5
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks(); } public void Configure(IApplicationBuilder app) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapHealthChecks("/health"); }); } }
ASP.NET Core 6
var builder = WebApplication.CreateBuilder(args); builder.Services.AddHealthChecks(); var app = builder.Build(); app.MapHealthChecks("/health"); app.Run();
Create a custom health check
We must first implement the IHealthCheck interface before we can create a custom health check. We must specifically implement the IHealthCheck interface’s CheckHealthAsync method. To determine the health status of the HealthCheckResult, we can perform any tasks inside the method.
Using Microsoft.Extensions.Diagnostics.HealthChecks; namespace WeatherApi; public class MemoryHealthCheck : IHealthCheck { private const long Threshold = 1024L * 1024L * 1024L; public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { var allocatedBytesInManagedMemory = GC.GetTotalMemory(forceFullCollection: false); var data = new Dictionary<string, object>() { { "AllocatedBytesInManagedMemory", allocatedBytesInManagedMemory }, { "NumberOfTimesGarbageCollectionOccurredForGen0Collection", GC.CollectionCount(0) }, { "NumberOfTimesGarbageCollectionOccurredForGen1Collection", GC.CollectionCount(1) }, { "NumberOfTimesGarbageCollectionOccurredForGen2Collection", GC.CollectionCount(2) } }; if (allocatedBytesInManagedMemory > Threshold) { var result = new HealthCheckResult( status: context.Registration.FailureStatus, description: $"Allocated bytes in managed memory is > {Threshold}", data: data); return Task.FromResult(result); } var healthyResult = new HealthCheckResult( status: HealthStatus.Healthy, description: $"Allocated bytes in managed memory is <= {Threshold}", data: data); return Task.FromResult(healthyResult); } }
To determine if the service is healthy, I only consider a condition from the health check mentioned above. MemoryHealthCheck
returns the default failure status (Unhealthy
by default) if it is not healthy. When we add the health check to the IHealthChecksBuilder
, we have the option to alter the default failure status for the health check.
builder.Services .AddHealthChecks() .AddCheck<MemoryHealthCheck>(name: "memory", failureStatus: HealthStatus.Degraded);
Customize the HTTP status codes and response for the health check
Regardless of the health check status, a health check endpoint will by default return a 200 OK
status code when we call it. Depending on the results of the health check, it may be useful to generate a specific status code at times. This can be done by setting the HealthCheckOptions
‘ ResultStatusCodes
property. In the example below, the endpoint will return a 500 InternalServerError
if the health status has deteriorated. The endpoint will respond with a 503 ServiceUnavailable
response when the health status is unhealthy.
app.MapHealthChecks(pattern: "/ready", options: new HealthCheckOptions { ResponseWriter = WriteHealthCheckResponseAsync, ResultStatusCodes = { [HealthStatus.Degraded] = StatusCodes.Status500InternalServerError, [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable, }, Predicate = _ => true });
For health checks, the default responses (report) are Healthy
, Unhealthy
, or Degraded
. By giving a delegate to the ResponseWriter
of HealthCheckOptions
, we can alter the response (report).
static Task WriteHealthCheckResponseAsync( HttpContext httpContext, HealthReport healthReport) { httpContext.Response.ContentType = "application/json"; var dependencyHealthChecks = healthReport.Entries.Select(entry => new { Name = entry.Key, Discription = entry.Value.Description, Status = entry.Value.Status.ToString(), DurationInSeconds = entry.Value.Duration.TotalSeconds.ToString("0:0.00"), Data = entry.Value.Data, Exception = entry.Value.Exception?.Message }); var healthCheckResponse = new { Status = healthReport.Status.ToString(), TotalCheckExecutionTimeInSeconds = healthReport.TotalDuration.TotalSeconds.ToString("0:0.00"), DependencyHealthChecks = dependencyHealthChecks }; var serializerOptions = new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var responseString = JsonSerializer.Serialize(healthCheckResponse, serializerOptions); return httpContext.Response.WriteAsync(responseString); }
The above delegate creates the following report.
{ "status": "Healthy", "totalCheckExecutionDurationInSeconds": "0:0.12", "dependencyHealthChecks": [ { "name": "memory", "discription": "Reports memory status of the application.", "status": "Healthy", "durationInSeconds": "0:0.02", "data": { "AllocatedBytesInManagedMemory": 1907368, "NumberOfTimesGarbageCollectionOccurredForGen0Collection": 2, "NumberOfTimesGarbageCollectionOccurredForGen1Collection": 2, "NumberOfTimesGarbageCollectionOccurredForGen2Collection": 0 } } ] }
Filter health checks
Your application may subject you to a number of health checks. You can use tags and the Predicate
property of HealthCheckOptions
to filter out the health checks you need if you do not need to return the status of every health check. A HealthCheckRegistration
type parameter is taken by the predicate property. The HealthCheckRegistration
‘s Tags property can be used to filter the health checks. When adding the health checks, we can set the tag
(s) using the AddCheck
extension’s tags parameter.
builder.Services .AddHealthChecks() .AddCheck<MemoryHealthCheck>( name: "memory", failureStatus: HealthStatus.Unhealthy, tags: new[] { "memory" }); COPY COPY app.MapHealthChecks(pattern: "/ready", options: new HealthCheckOptions { Predicate = registration => registration.Tags.Contains("memory") }); ;
Andriy Kravets is writer and experience .NET developer and like .NET for regular development. He likes to build cross-platform libraries/software with .NET.