Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Health Checks

Overview Questions

  • Health Checks là gì và tại sao cần thiết?
  • Làm sao để cấu hình health check endpoints?
  • Custom health check được viết như thế nào?
  • Health check UI là gì và cách tích hợp?
  • Health check response format ra sao?

Basic Health Checks

Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add health checks
builder.Services.AddHealthChecks();

var app = builder.Build();

// Map health check endpoint
app.MapHealthChecks("/health");

// Basic health check
app.MapGet("/health/basic", () => Results.Ok("Healthy"));

app.Run();

Response

// Healthy
{
  "status": "Healthy"
}

// Unhealthy
{
  "status": "Unhealthy",
  "results": {
    "database": {
      "status": "Unhealthy",
      "description": "Connection failed"
    }
  }
}

Built-in Health Checks

Database Health Check

dotnet add package AspNetCore.HealthChecks.SqlServer
builder.Services.AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        name: "sqlserver",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "sql" });

Redis Health Check

dotnet add package AspNetCore.HealthChecks.Redis
builder.Services.AddHealthChecks()
    .AddRedis(
        "localhost:6379",
        name: "redis",
        failureStatus: HealthStatus.Degraded);

HTTP Endpoint Health Check

builder.Services.AddHealthChecks()
    .AddUrlGroup(
        new Uri("https://api.external.com/health"),
        name: "external-api",
        failureStatus: HealthStatus.Degraded);

Multiple Health Checks

builder.Services.AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        name: "database")
    .AddRedis("localhost:6379", name: "cache")
    .AddUrlGroup(new Uri("https://api.external.com"), name: "external");

Custom Health Checks

IHealthCheck Implementation

public class DiskSpaceHealthCheck : IHealthCheck
{
    private readonly ILogger<DiskSpaceHealthCheck> _logger;
    private readonly long _thresholdBytes = 1024 * 1024 * 100; // 100MB

    public DiskSpaceHealthCheck(ILogger<DiskSpaceHealthCheck> logger)
    {
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var drive = new DriveInfo(Path.GetPathRoot(Directory.GetCurrentDirectory())!);
        var freeSpace = drive.AvailableFreeSpace;

        if (freeSpace > _thresholdBytes)
        {
            return HealthCheckResult.Healthy(
                $"Free disk space: {freeSpace / 1024 / 1024}MB");
        }

        return HealthCheckResult.Unhealthy(
            $"Low disk space: {freeSpace / 1024 / 1024}MB remaining");
    }
}

// Đăng ký
builder.Services.AddHealthChecks()
    .AddCheck<DiskSpaceHealthCheck>("disk-space");

Delegate-based Health Check

builder.Services.AddHealthChecks()
    .AddCheck("memory", () =>
    {
        var memoryUsed = GC.GetGCMemoryInfo().HeapSizeBytes;
        var memoryLimit = 512L * 1024 * 1024; // 512MB

        return memoryUsed < memoryLimit
            ? HealthCheckResult.Healthy($"Memory: {memoryUsed / 1024 / 1024}MB")
            : HealthCheckResult.Unhealthy($"High memory: {memoryUsed / 1024 / 1024}MB");
    });

Health Check Options

Response Writer

app.MapHealthChecks("/health", new HealthCheckOptions
{
    // Custom response writer
    ResponseWriter = async (context, report) =>
    {
        var result = new
        {
            status = report.Status.ToString(),
            duration = report.TotalDuration,
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                data = e.Value.Data
            }),
            timestamp = DateTime.UtcNow
        };

        context.Response.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(result);
    }
});

Predicate & Tags

// Chỉ check database
app.MapHealthChecks("/health/db", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("db")
});

// Chỉ check cache
app.MapHealthChecks("/health/cache", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("cache")
});

Status Code Mapping

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResultStatusCodes = new Dictionary<HealthStatus, int>
    {
        [HealthStatus.Healthy] = 200,
        [HealthStatus.Degraded] = 200,
        [HealthStatus.Unhealthy] = 503
    }
});

Health Check UI

Setup

dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
// Program.cs
builder.Services.AddHealthChecksUI()
    .AddInMemoryStorage();

var app = builder.Build();

app.MapHealthChecks("/health");
app.MapHealthChecksUI("/health-ui");

app.Run();

Configuration

{
  "HealthChecksUI": {
    "HealthChecks": [
      {
        "Name": "API Health",
        "Uri": "https://localhost:5001/health"
      }
    ],
    "EvaluationTimeInSeconds": 30,
    "MinimumSecondsBetweenFailureNotifications": 60
  }
}

Kubernetes Probes

Liveness & Readiness

// Liveness - App is running
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    Predicate = _ => false // No checks, just return 200
});

// Readiness - App is ready to receive traffic
app.MapHealthChecks("/ready", new HealthCheckOptions
{
    Predicate = check => true // Run all checks
});

// Startup - App has started
app.MapHealthChecks("/started", new HealthCheckOptions
{
    Predicate = _ => false
});

Kubernetes Config

livenessProbe:
  httpGet:
    path: /healthz
    port: 80
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 80
  initialDelaySeconds: 10
  periodSeconds: 5

Best Practices

1. Use Appropriate Checks

// ✅ Good - lightweight checks
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "database")
    .AddRedis(redisConnection, name: "cache");

// ❌ Bad - expensive checks
builder.Services.AddHealthChecks()
    .AddCheck("full-database-scan", async () => 
    {
        // Expensive query - avoid in health check
        var count = await db.Products.CountAsync();
        return count > 0 ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy();
    });

2. Separate Liveness and Readiness

┌─────────────────────────────────────────────────────────────────┐
│                    HEALTH CHECK TYPES                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Liveness (/healthz):                                           │
│  - App is running                                               │
│  - No deadlock or infinite loop                                 │
│  - Kubernetes restarts if failed                                │
│                                                                 │
│  Readiness (/ready):                                            │
│  - App can handle requests                                      │
│  - Dependencies are available                                   │
│  - Kubernetes removes from service if failed                    │
│                                                                 │
│  Startup (/started):                                            │
│  - App has finished initialization                              │
│  - Used during startup probe                                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. Don’t Overload Health Checks

// ✅ Good - simple checks
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "db")
    .AddRedis(redisConnection, name: "cache");

// ❌ Bad - too many external dependencies
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString)
    .AddRedis(redisConnection)
    .AddUrlGroup(externalApi1)
    .AddUrlGroup(externalApi2)
    .AddUrlGroup(externalApi3); // External APIs có thể down