Liveness and Readiness in Asp.net core

Liveness and Readiness in Asp.net core

In ASP.NET Core, you can implement liveness and readiness probes to ensure that your application is healthy and ready to serve traffic. These probes are commonly used in containerized applications orchestrated by Kubernetes, but they are also useful for general health checks in any hosting environment.

Steps to Implement Liveness and Readiness Probes:

Install the Health Checks Package:

First, you need to install the required NuGet package for health checks.

dotnet add package Microsoft.AspNetCore.Diagnostics.HealthChecks

Configure Health Checks in Startup.cs (or Program.cs for .NET 6 and later)

You can define both the liveness and readiness checks using the ASP.NET Core health check framework.

Example for ASP.NET Core 6+ (Program.cs):

var builder = WebApplication.CreateBuilder(args);

// Add health check services
builder.Services.AddHealthChecks()
    .AddCheck("liveness", () => HealthCheckResult.Healthy("Liveness check passed")) // Basic liveness check
    .AddCheck<DatabaseHealthCheck>("readiness"); // Custom readiness check for DB or other services

var app = builder.Build();

// Map health check endpoints
app.MapHealthChecks("/health/liveness", new HealthCheckOptions
{
    Predicate = (check) => check.Name == "liveness"
});

app.MapHealthChecks("/health/readiness", new HealthCheckOptions
{
    Predicate = (check) => check.Name == "readiness"
});

app.Run();


In this example:

Liveness Check: Ensures that the application is alive (i.e., it hasn't crashed).

Readiness Check: Determines if the application is ready to handle requests (e.g., if dependencies such as databases are available).

    • /health/liveness returns the liveness status.
    • /health/readiness returns the readiness status.

Create a Custom Health Check (Optional)

You can create custom health checks to verify specific dependencies like databases, message queues, etc. Example for a database readiness

public class DatabaseHealthCheck : IHealthCheck
{
    private readonly YourDbContext _dbContext;

    public DatabaseHealthCheck(YourDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            // Check if DB connection is alive
            await _dbContext.Database.CanConnectAsync(cancellationToken);
            return HealthCheckResult.Healthy("Database is ready");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Database is not ready", ex);
        }
    }
}

Add Custom Health Check to DI

Register the custom health check in the Program.cs.

builder.Services.AddScoped<DatabaseHealthCheck>();

Test Your Health Checks

Once you've implemented the health checks, you can test them by visiting the URLs:

    • Liveness probe: https://localhost:5001/health/liveness
    • Readiness probe: https://localhost:5001/health/readiness

Customizing Health Check Responses

You can customize the output or response format of your health checks using the ResponseWriter option, for example, to return JSON:

app.MapHealthChecks("/health/liveness", new HealthCheckOptions
{
    Predicate = (check) => check.Name == "liveness",
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";
        var json = JsonSerializer.Serialize(new
        {
            status = report.Status.ToString(),
            checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString() })
        });
        await context.Response.WriteAsync(json);
    }
});

This way, you can implement both liveness and readiness probes efficiently in ASP.NET Core applications.

Better Approach

To improve the health check implementation, you can replace the Predicate with Tags to make it more flexible and scalable. By using tags, you can categorize your health checks (e.g., "liveness" and "readiness") and then filter by tags when defining the health check endpoints. This is more maintainable, especially when you have many health checks.

Here’s how you can implement the liveness and readiness probes using tags in ASP.NET Core:

Program.cs Configuration (ASP.NET Core 6+):

You can add tags to each health check when registering them. Later, you can use these tags to filter the health checks at the endpoint level.

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);

// Register Health Check services and add tags to differentiate checks
builder.Services.AddHealthChecks()
    .AddCheck("LivenessCheck", () => HealthCheckResult.Healthy("Liveness check passed"), tags: new[] { "liveness" }) // Tag for liveness
    .AddCheck<DatabaseHealthCheck>("ReadinessCheck", tags: new[] { "readiness" }); // Tag for readiness check

var app = builder.Build();

// Map health check endpoints using tags

// Liveness endpoint (filters health checks by "liveness" tag)
app.MapHealthChecks("/health/liveness", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("liveness"),
    ResponseWriter = WriteHealthCheckResponse
});

// Readiness endpoint (filters health checks by "readiness" tag)
app.MapHealthChecks("/health/readiness", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("readiness"),
    ResponseWriter = WriteHealthCheckResponse
});

app.Run();

// Custom response writer to format the health check output
static Task WriteHealthCheckResponse(HttpContext context, HealthReport report)
{
    context.Response.ContentType = "application/json";
    
    var json = JsonSerializer.Serialize(new
    {
        status = report.Status.ToString(),
        checks = report.Entries.Select(entry => new
        {
            name = entry.Key,
            status = entry.Value.Status.ToString(),
            description = entry.Value.Description,
            duration = entry.Value.Duration.TotalMilliseconds
        })
    });

    return context.Response.WriteAsync(json);
}

Explanation:

  • Using Tags:
    • Each health check is tagged as either "liveness" or "readiness". This makes it easier to filter the checks by their purpose.
    • Instead of using Predicate, we filter checks based on their tags using check.Tags.Contains("tag_name").
  • Endpoints:
    • /health/liveness will only execute health checks with the tag "liveness".
    • /health/readiness will only execute health checks with the tag "readiness".
  • Custom Response Writer:
    • The WriteHealthCheckResponse method is used to format the JSON response to include the status of each check along with additional metadata like the description and duration.

Testing the Probes:

After running the application, you can visit the following URLs to test:

  • Liveness Probe: https://localhost:5001/health/liveness
  • Readiness Probe: https://localhost:5001/health/readiness

This setup will give you a clean and maintainable health check structure using tags, and the probes will be ready for orchestration systems like Kubernetes or health check systems in other hosting environments.