- 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?
// 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();
// Healthy
{
"status": "Healthy"
}
// Unhealthy
{
"status": "Unhealthy",
"results": {
"database": {
"status": "Unhealthy",
"description": "Connection failed"
}
}
}
dotnet add package AspNetCore.HealthChecks.SqlServer
builder.Services.AddHealthChecks()
.AddSqlServer(
builder.Configuration.GetConnectionString("Default"),
name: "sqlserver",
failureStatus: HealthStatus.Unhealthy,
tags: new[] { "db", "sql" });
dotnet add package AspNetCore.HealthChecks.Redis
builder.Services.AddHealthChecks()
.AddRedis(
"localhost:6379",
name: "redis",
failureStatus: HealthStatus.Degraded);
builder.Services.AddHealthChecks()
.AddUrlGroup(
new Uri("https://api.external.com/health"),
name: "external-api",
failureStatus: HealthStatus.Degraded);
builder.Services.AddHealthChecks()
.AddSqlServer(
builder.Configuration.GetConnectionString("Default"),
name: "database")
.AddRedis("localhost:6379", name: "cache")
.AddUrlGroup(new Uri("https://api.external.com"), name: "external");
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");
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");
});
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);
}
});
// 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")
});
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResultStatusCodes = new Dictionary<HealthStatus, int>
{
[HealthStatus.Healthy] = 200,
[HealthStatus.Degraded] = 200,
[HealthStatus.Unhealthy] = 503
}
});
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();
{
"HealthChecksUI": {
"HealthChecks": [
{
"Name": "API Health",
"Uri": "https://localhost:5001/health"
}
],
"EvaluationTimeInSeconds": 30,
"MinimumSecondsBetweenFailureNotifications": 60
}
}
// 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
});
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 10
periodSeconds: 5
// ✅ 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();
});
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└─────────────────────────────────────────────────────────────────┘
// ✅ 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