146 lines
5.9 KiB
C#
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);
|
|
}
|
|
} |