NejCommon.NET/Health.cs
2026-09-09 22:14:13 +02:00

146 lines
5.9 KiB
C#

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<TBuilder>(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<IConfiguration>(),
sp.GetRequiredService<ILogger<PostgresSqlHealthCheck>>(),
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<HealthCheckPublisherOptions>(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, int>
{
[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, int>
{
[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, int>
{
[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);
}
}