Create .NET Core health checks with custom response payload

Last Updated on by

Post summary: How to extend custom .NET Core health checks so the response JSON provides more information.

The code used for this blog post is located in dotnet.core.templates GitHub repository.

Heath checks in .NET Core

Health checks in .NET Core is a middleware that provides a possibility to report an application’s health. This allows monitoring of the application and taking corrective actions in case of issues. For e.g., if an application reports to be unhealthy, then the load balancer can exclude it from the infrastructure and appropriate alarms to be raised. More in about health checks can be read in Health checks in ASP.NET Core page.

Adding a health check

In order to add a health check in a .NET Core application, then reference to Microsoft.AspNetCore.Diagnostics.HealthChecks package has to be added. Health checks themselves are classes implementing IHealthCheck interface.

public class VersionHealthCheck : IHealthCheck
{
	private readonly AppConfig _config;

	public VersionHealthCheck(IOptions<AppConfig> options)
	{
		_config = options.Value;
	}

	public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
				CancellationToken cancellationToken = new CancellationToken())
	{
		return Task.FromResult(string.IsNullOrEmpty(_config.Version)
			? HealthCheckResult.Unhealthy()
			: HealthCheckResult.Healthy());
	}
}

Health checks have to be registered in Startup.cs file:

public void ConfigureServices(IServiceCollection services)
{
	services.AddHealthChecks()
		.AddCheck<VersionHealthCheck>("Version Health Check");
}

As a last step health checks endpoint has to be configured in Startup.cs file:

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

Now health checks report is available at <HOSTNAME>/health URL. If everything is good, the response is 200 OK with content Healthy. In case of issues, the response is 503 Service Unavailable with content Unhealthy.

Extend the health checks response

As stated above health checks are mainly intended for machine usage. I have had cases in practice, in which just looking into the health check allows faster problem solving rather than looking into the logs. For this reason, investing in more explanatory health checks is worth it. Below is a code snippet on how to have more information into the health check response payload. A new static class HealthCheckExtensions with MapCustomHealthChecks method can be added.

public static class HealthCheckExtensions
{
	public static IEndpointConventionBuilder MapCustomHealthChecks(
		this IEndpointRouteBuilder endpoints, string serviceName)
	{
		return endpoints.MapHealthChecks("/health", new HealthCheckOptions
		{
			ResponseWriter = async (context, report) =>
			{
				var result = JsonConvert.SerializeObject(
					new HealthResult
					{
						Name = serviceName,
						Status = report.Status.ToString(),
						Duration = report.TotalDuration,
						Info = report.Entries.Select(e => new HealthInfo
						{
							Key = e.Key,
							Description = e.Value.Description,
							Duration = e.Value.Duration,
							Status = Enum.GetName(typeof(HealthStatus),
													e.Value.Status),
							Error = e.Value.Exception?.Message
						}).ToList()
					}, Formatting.None,
					new JsonSerializerSettings
					{
						NullValueHandling = NullValueHandling.Ignore
					});
				context.Response.ContentType = MediaTypeNames.Application.Json;
				await context.Response.WriteAsync(result);
			}
		});
	}
}

All the formatting in the code depends on two additional data classes HealthInfo and HealthResult.

public class HealthInfo
{
	public string Key { get; set; }
	public string Description { get; set; }
	public TimeSpan Duration { get; set; }
	public string Status { get; set; }
	public string Error { get; set; }
}
public class HealthResult
{
	public string Name { get; set; }
	public string Status { get; set; }
	public TimeSpan Duration { get; set; }
	public ICollection<HealthInfo> Info { get; set; }
}

Registering the endpoint happens with the same code as in the default case, with the difference that the MapCustomHealthChecks extension method is used:

public void Configure(IApplicationBuilder app)
{
	app.UseEndpoints(endpoints =>
	{
		endpoints.MapCustomHealthChecks("Service Name");
	});
}

Now it is possible to have some more elaborate health checks, which can capture exception for e.g. and return it as well.

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
		CancellationToken cancellationToken = new CancellationToken())
{
	try
	{
		var message = $"Version is healthy: {_config.Version}";
		return Task.FromResult(HealthCheckResult.Healthy(message));
	}
	catch (Exception ex)
	{
		var message = "There is an error with version health check";
		return Task.FromResult(HealthCheckResult.Unhealthy(message, ex));
	}
}

In the case of 503 Service Unavailable, health check gives more details, which in some cases can be enough to resolve the issue without having to dig into the logs.

{
	"Name": "Service Name",
	"Status": "Unhealthy",
	"Duration": "00:00:00.0159186",
	"Info": [
		{
			"Key": "Version Health Check",
			"Description": "There is an error with version health check",
			"Duration": "00:00:00.0010564",
			"Status": "Unhealthy",
			"Error": "Exception's message text"
		}
	]
}

Conclusion

.NET Core health checks are a convenient way for automatic service monitoring and taking corrective actions. With a small effort, they can be enhanced so they can be made useful for people trying to identify what the issues with the services are.

Related Posts

Category: C# | Tags: