using System.Text.Json; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Diagnostics.HealthChecks; using NejCommon.Utils; namespace NejCommon; public static class HealthUtils { public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.Services.AddHealthChecks() // ======================================== // Kubernetes Probes // ======================================== // Liveness Probe: "Is the application running?" // If it fails, Kubernetes RESTARTS the pod .AddCheck("self", () => HealthCheckResult.Healthy("Application is running"), tags: ["live"]) // Readiness Probe: "Is the application ready to receive traffic?" // If it fails, Kubernetes REMOVES the pod from Service .AddCheck("readiness", () => HealthCheckResult.Healthy("Application is ready"), tags: ["ready"]) // ======================================== // Critical Dependencies (Database) // ======================================== // PostgreSQL - Main database .Add(new HealthCheckRegistration( name: "postgres", factory: sp => new PostgresSqlHealthCheck( sp.GetRequiredService(), sp.GetRequiredService>(), sp), failureStatus: HealthStatus.Degraded, // Degraded allows app to continue tags: ["dependency", "critical", "database"], timeout: TimeSpan.FromSeconds(15))); // ======================================== // Health Check Publisher (Periodic Checks) // ======================================== builder.Services.Configure(options => { options.Delay = TimeSpan.FromSeconds(5); // Wait 5s after startup options.Period = TimeSpan.FromSeconds(30); // Execute every 30s }); return builder; } private static async Task WriteHealthCheckResponse(HttpContext context, HealthReport report) { context.Response.ContentType = "application/json"; var response = new { Status = report.Status.ToString(), Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), TotalDuration = $"{report.TotalDuration.TotalMilliseconds}ms", Results = report.Entries.Select(e => new { Name = e.Key, Status = e.Value.Status.ToString(), Duration = $"{e.Value.Duration.TotalMilliseconds}ms", Description = e.Value.Description, Error = e.Value.Exception?.Message, Data = e.Value.Data?.Count > 0 ? e.Value.Data : null, Tags = e.Value.Tags?.ToList() }).ToList(), Summary = new { Total = report.Entries.Count, Healthy = report.Entries.Count(e => e.Value.Status == HealthStatus.Healthy), Degraded = report.Entries.Count(e => e.Value.Status == HealthStatus.Degraded), Unhealthy = report.Entries.Count(e => e.Value.Status == HealthStatus.Unhealthy) } }; await context.Response.WriteAsync( JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); } public static WebApplication MapHealthEndpoints(this WebApplication app) { var healthCheckOptions = new HealthCheckOptions { Predicate = _ => true, // All dependencies ResponseWriter = WriteHealthCheckResponse, ResultStatusCodes = new Dictionary { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable } }; var liveOptions = new HealthCheckOptions { Predicate = r => r.Tags.Contains("live"), ResponseWriter = WriteHealthCheckResponse, ResultStatusCodes = new Dictionary { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable } }; var readyOptions = new HealthCheckOptions { Predicate = r => r.Tags.Contains("ready"), ResponseWriter = WriteHealthCheckResponse, ResultStatusCodes = new Dictionary { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable } }; // Map endpoints to both /health and /healthz (K8s compatibility) MapDetailedHealthEndpoints(app, "/health", healthCheckOptions, liveOptions, readyOptions); MapDetailedHealthEndpoints(app, "/healthz", healthCheckOptions, liveOptions, readyOptions); return app; } private static void MapDetailedHealthEndpoints(WebApplication app, string v, HealthCheckOptions healthCheckOptions, HealthCheckOptions liveOptions, HealthCheckOptions readyOptions) { app.MapHealthChecks(v, healthCheckOptions); app.MapHealthChecks(v + "/live", liveOptions); app.MapHealthChecks(v + "/ready", readyOptions); } }