ASP.NET Core Health Checks
Trigger On
- deploy lên Kubernetes/Docker — cần
livenessProbe, readinessProbe endpoints
- load balancer (nginx, AWS ALB, Azure App Gateway) cần health endpoint
- monitor service health (dependencies up/down, latency)
- separate liveness vs readiness checks
- thêm probe cho DB, Redis, queue, external API
- dashboard health visualization (
AspNetCore.HealthChecks.UI)
Documentation
Liveness vs Readiness — bắt buộc tách 2
|
Liveness |
Readiness |
| Hỏi gì |
App có còn alive không? |
App có sẵn sàng nhận traffic không? |
| Fail = |
Restart container |
Tạm thời ngưng route traffic |
| Check gì |
App process responsive (chỉ self-check, KHÔNG check dependencies) |
DB, Redis, dependencies, init complete |
| Endpoint |
/healthz/live |
/healthz/ready |
| K8s probe |
livenessProbe |
readinessProbe |
Quan trọng: liveness KHÔNG check DB. Nếu DB down, app không cần restart — chỉ readiness fail (k8s ngưng route traffic, app vẫn chạy). Liveness check DB → DB down → restart loop vô tận.
Setup cơ bản
// Program.cs
builder.Services.AddHealthChecks()
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("Default")!,
name: "sql",
failureStatus: HealthStatus.Unhealthy,
tags: ["db", "ready"])
.AddRedis(
redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
tags: ["cache", "ready"])
.AddUrlGroup(
new Uri("https://api.partner.com/health"),
name: "partner-api",
tags: ["external", "ready"]);
var app = builder.Build();
// Liveness — chỉ check app self (không check tag)
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
Predicate = _ => false, // không chạy bất kỳ registered check, chỉ trả OK nếu app live
ResponseWriter = WriteHealthResponse
});
// Readiness — chạy mọi check có tag "ready"
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = WriteHealthResponse
});
app.Run();
static Task WriteHealthResponse(HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
var response = new
{
status = report.Status.ToString(),
totalDuration = report.TotalDuration.TotalMilliseconds,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalMilliseconds,
error = e.Value.Exception?.Message
})
};
return context.Response.WriteAsync(JsonSerializer.Serialize(response));
}
Common probes — packages
# Database
dotnet add package AspNetCore.HealthChecks.SqlServer
dotnet add package AspNetCore.HealthChecks.Npgsql # PostgreSQL
dotnet add package AspNetCore.HealthChecks.MySql
# Cache
dotnet add package AspNetCore.HealthChecks.Redis
# Messaging
dotnet add package AspNetCore.HealthChecks.RabbitMQ
dotnet add package AspNetCore.HealthChecks.Kafka
# External
dotnet add package AspNetCore.HealthChecks.Uris
# Hệ thống
dotnet add package AspNetCore.HealthChecks.System # disk, memory
Custom Health Check
public class StripeHealthCheck(IStripeClient stripe) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var sw = Stopwatch.StartNew();
await stripe.GetService<AccountService>().GetSelfAsync(cancellationToken: cancellationToken);
sw.Stop();
// Degraded nếu chậm
if (sw.ElapsedMilliseconds > 2000)
return HealthCheckResult.Degraded($"Stripe slow: {sw.ElapsedMilliseconds}ms");
return HealthCheckResult.Healthy($"Stripe OK ({sw.ElapsedMilliseconds}ms)");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Stripe API unreachable", ex);
}
}
}
// Register
builder.Services.AddHealthChecks()
.AddCheck<StripeHealthCheck>("stripe", tags: ["external", "ready"]);
Health Status — 3 levels
| Status |
Khi dùng |
Effect |
Healthy |
Working bình thường |
LB route traffic |
Degraded |
Working nhưng chậm/limited |
LB route nhưng có thể alert |
Unhealthy |
Không serve được |
LB ngưng route, k8s restart (nếu liveness) |
Health Check UI Dashboard
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.Client
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage # hoặc SQLite/SqlServer
builder.Services.AddHealthChecksUI(opt =>
{
opt.SetEvaluationTimeInSeconds(30);
opt.MaximumHistoryEntriesPerEndpoint(60);
opt.AddHealthCheckEndpoint("My App", "/healthz/ready");
}).AddInMemoryStorage();
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse // format cho UI
});
app.MapHealthChecksUI(opt => opt.UIPath = "/healthz/ui");
Truy cập /healthz/ui → dashboard với history, charts.
Kubernetes Probe Config
# deployment.yaml
spec:
containers:
- name: myapp
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 2
Tag Strategy
.AddSqlServer(..., tags: ["db", "ready", "critical"])
.AddRedis(..., tags: ["cache", "ready"])
.AddUrlGroup(..., tags: ["external", "ready", "non-critical"])
// Multiple endpoints với filter khác nhau
app.MapHealthChecks("/healthz/critical", new()
{
Predicate = c => c.Tags.Contains("critical")
});
app.MapHealthChecks("/healthz/full", new()
{
Predicate = _ => true
});
Anti-patterns
| Anti-pattern |
Vấn đề |
Fix |
| Liveness check DB |
DB down → k8s restart loop |
Liveness Predicate = _ => false (chỉ self) |
| Single endpoint cho cả 2 |
Không tách behavior |
/healthz/live + /healthz/ready riêng |
| No timeout trong custom check |
Probe hang → k8s tưởng app dead |
Pass cancellationToken, set timeout |
| Check external API trong liveness |
Network blip → restart |
External checks chỉ ở readiness |
| Health endpoint không protected nhưng leak detail |
Info disclosure (DB version, schema) |
Filter response, hoặc require auth cho /healthz/full |
Predicate = _ => true cho liveness |
Chạy mọi check (slow) |
Chỉ readiness chạy full |
| Quên handle exception trong custom check |
IHealthCheck lib auto wrap nhưng custom logic dễ throw |
Try/catch return Unhealthy(ex) |
| Probe interval quá ngắn (1s) |
Spam DB |
periodSeconds: 10-30 |
Performance
- Cache health result: nếu probe heavy (full DB query), cache 5-10s tránh hammer DB
- Parallel execution: built-in
HealthCheckService chạy parallel — đặt timeout reasonable cho mỗi check
- Lazy register: probe chỉ chạy khi endpoint hit, không background
Validate
- Tách
/healthz/live (no deps) và /healthz/ready (full deps)
- Liveness KHÔNG check DB/Redis/external
- Readiness include all critical deps với tags
- Custom check có exception handling, return
Unhealthy(ex)
- K8s probe config với
initialDelay, period, timeout, failureThreshold reasonable
- Health endpoint không leak sensitive info (DB connection string, schema)
- Timeout trong custom check honor
CancellationToken
- (Optional) UI dashboard cho ops monitoring
Hand off to
- Logging health events →
aspnet-logging
- Hosting/middleware order →
aspnet-core
- DB connection management →
entity-framework-core
- Background task health →
background-jobs