Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.Monitoring/Configurations/HealthCheckConfigurations.cs
T
2026-07-13 22:52:57 +03:30

81 lines
3.7 KiB
C#

using Baya.Infrastructure.Monitoring.HealthChecks;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
namespace Baya.Infrastructure.Monitoring.Configurations;
/// <summary>
/// Health checks split into <b>liveness</b> (is the process up?) and <b>readiness</b> (can it serve traffic —
/// are its dependencies reachable?), refinement-phase-9 §9.2. An orchestrator restarts on a failed liveness
/// probe but only routes traffic on a passing readiness probe, so a broken log DB or object storage takes the
/// instance out of rotation without a restart loop.
/// <list type="bullet">
/// <item><c>/healthz/live</c> — process only (always healthy while the host runs); never touches a dependency.</item>
/// <item><c>/healthz/ready</c> — the app DB, the log DB (deployed only), and the object-storage write probe.</item>
/// <item><c>/HealthCheck</c> — the aggregate (every check), retained for backward compatibility.</item>
/// </list>
/// </summary>
public static class HealthCheckConfigurations
{
private const string Ready = "ready";
private const string Live = "live";
public static WebApplicationBuilder ConfigureHealthChecks(this WebApplicationBuilder builder)
{
var checks = builder.Services.AddHealthChecks();
// Liveness: the process answers. Deliberately dependency-free so a dependency outage never restarts a
// healthy instance — that is readiness's job.
checks.AddCheck("self", () => HealthCheckResult.Healthy(), tags: [Live]);
// Readiness: the system of record.
checks.AddSqlServer(builder.Configuration.GetConnectionString("SqlServer")!, name: "sql-app", tags: [Ready]);
// The Serilog SQL sink only runs in deployed environments (Development/Testing log to console/file), and
// its connection string is a placeholder there — so only gate readiness on it where it is actually used.
if (!builder.Environment.IsDevelopment() && !builder.Environment.IsEnvironment("Testing"))
{
var logDb = builder.Configuration.GetConnectionString("logDb");
if (!string.IsNullOrWhiteSpace(logDb))
checks.AddSqlServer(logDb, name: "sql-logs", tags: [Ready]);
}
// Object storage (verification docs, avatars, invoice PDFs) — a real write round-trip.
checks.AddCheck<ObjectStorageWriteHealthCheck>("object-storage", tags: [Ready]);
// Redis (ICacheService / IDistributedLock) is single-process today (refinement-phase-7); when it becomes a
// real external dependency for a multi-instance deployment, add a "redis" check tagged Ready here.
return builder;
}
public static WebApplication UseHealthChecks(this WebApplication app)
{
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains(Live),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}).ShortCircuit();
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains(Ready),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}).ShortCircuit();
// Aggregate endpoint (all checks) — retained for backward compatibility with existing probes/dashboards.
app.MapHealthChecks("/HealthCheck", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}).ShortCircuit();
return app;
}
}