feat: Add health check monitoring for ProfileWorker

ProfileWorker changes:
- Implement IHealthCheck interface
- Replace _workCount with DateTime-based tracking
- Track _lastSuccessfulRun, _consecutiveFailures, _lastException
- Graceful shutdown handling (OperationCanceledException)
- Health states: Healthy, Degraded (1-2 failures), Unhealthy (3x interval)

DependencyInjection changes:
- Register ProfileWorker as singleton (for health check access)
- Use factory pattern for IHostedService registration

Program.cs changes:
- Add health check service with ProfileWorker
- Map /health endpoint (full JSON response with all checks)
- Map /health/ready endpoint (filtered by 'ready' tag)
- Custom JSON response writer with detailed metrics
This commit is contained in:
2026-07-13 12:19:11 +02:00
parent b48e3d1823
commit 8a05d86285
3 changed files with 128 additions and 10 deletions

View File

@@ -101,6 +101,10 @@ builder.Services.AddHangfireServer();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add health checks
builder.Services.AddHealthChecks()
.AddCheck<ProfileWorker>("profile-worker", tags: new[] { "ready", "worker" });
// Add Serilog.UI with SQLite provider - use same path from configuration
var serilogUiLogDirectory = builder.Configuration.GetValue<string>("Application:LogDirectory")
?? throw new InvalidOperationException("Application:LogDirectory not found in configuration.");
@@ -139,6 +143,36 @@ app.UseSerilogUi();
app.MapControllers();
// Map health check endpoints
app.MapHealthChecks("/health", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = System.Text.Json.JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
timestamp = DateTime.UtcNow,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalMilliseconds,
exception = e.Value.Exception?.Message,
data = e.Value.Data
})
});
await context.Response.WriteAsync(result);
}
});
app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
// Redirect root path to Hangfire dashboard
app.MapGet("/", () => Results.Redirect("/hangfire"));