API Health Checks in .NET

Monitoring the health of your application is essential, particularly when it relies on multiple dependencies such as databases, external services, or other resources. API health checks in .NET provide a simple way to monitor the state of these dependencies and ensure everything is functioning as expected. In this blog, we'll walk through implementing API health checks in .NET, using a demo project as an example.

The code repository can be found at https://github.com/vaishnavkishan/HealthCheck-Demo

Key Features of Health Checks

1. Multiple Checks for Different Dependencies

In .NET, you can configure health checks for various dependencies like databases, cache services, or external APIs. Each check evaluates a specific component's status, helping you identify issues before they become critical.

builder.Services.AddHealthChecks()
    .AddCheck<SampleHealthCheck>("basic_check")
    .AddCheck<SampleHealthCheck1>("advance_check", HealthStatus.Degraded);

Here, SampleHealthCheck and SampleHealthCheck1 represent different health checks. You can register as many checks as needed for your application.

2. Degraded Responses for Non-Critical Dependencies

In cases where a non-critical dependency fails, you can configure the health check to return a Degraded status instead of Unhealthy. This shows that while some parts of the system are underperforming, the system as a whole is still operational.

builder.Services.AddHealthChecks()
    .AddCheck<SampleHealthCheck1>("advance_check", HealthStatus.Degraded);

In this example, SampleHealthCheck1 returns a Degraded status, indicating that the dependency is non-critical.

3. Detailed Status for Each Dependency

The health check returns a detailed status for each dependency, helping you understand which parts of your system are facing issues.

app.MapHealthChecks("health", new HealthCheckOptions 
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

When you hit the /health endpoint, you'll get a JSON response detailing the status of each dependency.

4. Error Details

In case of failures, the health check response includes exception message, making it easier to diagnose and fix issues. This is particularly helpful for pinpointing the root cause of problems. Whether to send exception details in the response or not can also be configured based on environment.

public class SampleHealthCheck1 : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        return Task.FromResult(
            new HealthCheckResult(
                context.Registration.FailureStatus, "An unhealthy result.",new Exception("This is exception")));
    }
}

Here, SampleHealthCheck1 returns exception message along with the degraded status.

5. Custom Data in Responses

You can include custom data in the health check responses, providing additional context for each check. This is useful for debugging and monitoring.

Code Example:

public class SampleHealthCheck1 : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
          Dictionary<string, object> data = new Dictionary<string, object> {
            { "SomeData", "value1" },
            { "OtherData", "value2" } 
            };

        return Task.FromResult(
            new HealthCheckResult(
                context.Registration.FailureStatus, "An unhealthy result.", null, data));
     }
}

In this code, custom data is returned along with the health check result.

6. Filtering Health Checks Based on Tags

You can assign tags to different health checks and filter them based on these tags. For example, you might have a separate health check endpoint for critical dependencies and another for non-critical checks.

Code Example:

builder.Services.AddHealthChecks()
    .AddCheck<StartupHealthCheck>("startup", tags: new[] { "ready" });

app.MapHealthChecks("health/ready", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
    Predicate = healthCheck => healthCheck.Tags.Contains("ready")
});

In this example, the StartupHealthCheck is tagged with ready, and the /health/ready endpoint only returns checks with that tag.

7. Kubernetes Readiness and Liveness Probes

Health checks can be integrated with Kubernetes' readiness and liveness probes, ensuring your application is only exposed to traffic when it's ready, and that it can automatically recover if it becomes unhealthy.

Code Example:

// Liveness probe, runs all checks and returns healthy if all checks are healthy
app.MapHealthChecks("health/live", new HealthCheckOptions 
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

// Readiness probe, waits for 15 seconds after app starts and then returns healthy
app.MapHealthChecks("health/ready", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
    Predicate = healthCheck => healthCheck.Tags.Contains("ready")
});

In this setup, the /health/live endpoint checks the liveness of all components, while the /health/ready endpoint focuses on readiness.

Running the Demo

To try out the API health check implementation in .NET, follow these simple steps:

  1. Clone the repository:

     git clone https://github.com/vaishnavkishan/HealthCheck-Demo.git
    
  2. Install the .NET 8 SDK: Make sure you have the .NET 8 SDK installed. You can download it from the official .NET website if you don't have it installed yet.

  3. Run the application:

     dotnet run
    
  4. Access the health check endpoints: Once the application is running, you can check the health of your API by navigating to the following URLs in your browser or using tools like curl or Postman:

Sample Health Check Response

Below is a sample response from a health check endpoint:

{
    "status": "Unhealthy",
    "totalDuration": "00:00:00.0104583",
    "entries": {
        "basic_check": {
            "data": {},
            "description": "A healthy result.",
            "duration": "00:00:00.0006799",
            "status": "Healthy",
            "tags": []
        },
        "advance_check": {
            "data": {
                "SomeData": "value1",
                "key2": "value2"
            },
            "description": "An unhealthy result.",
            "duration": "00:00:00.0007156",
            "exception": "This is exception",
            "status": "Degraded",
            "tags": []
        },
        "startup": {
            "data": {},
            "description": "That startup task is still running.",
            "duration": "00:00:00.0010888",
            "status": "Unhealthy",
            "tags": [
                "ready"
            ]
        }
    }
}

As you can see from the response, different checks can return different statuses like Healthy, Degraded, or Unhealthy, and you can also include custom data and error details for deeper insights.

Conclusion

Implementing API health checks in .NET is a straightforward and effective way to monitor the health of your application and its dependencies. With features like multiple checks, degraded responses, custom data, and Kubernetes integration, you can ensure your application remains resilient and easy to troubleshoot.

Now it's your turn to try it out in your own project!

References

  1. https://github.com/vaishnavkishan/HealthCheck-Demo

  2. https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-8.0