feat(api): health-пробы /health/live и /health/ready (P0-4)
/health/live — liveness без зависимостей (Predicate=false). /health/ready — readiness: DatabaseReadyHealthCheck (CanConnect + нет неприменённых миграций), тег ready, 503 если не готово. JSON-ответ по каждому чеку. docker-compose api healthcheck + Dockerfile.api → /health/ready, web ждёт api service_healthy. /health сохранён для обратной совместимости. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6f1566c2c3
commit
82bf53bb5c
|
|
@ -32,7 +32,7 @@ ENV DOTNET_NOLOGO=1
|
|||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
|
||||
CMD curl -fsS http://localhost:8080/health || exit 1
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
|
||||
CMD curl -fsS http://localhost:8080/health/ready || exit 1
|
||||
|
||||
ENTRYPOINT ["dotnet", "foodmarket.Api.dll"]
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ services:
|
|||
ports:
|
||||
- "8080:8080" # api
|
||||
|
||||
healthcheck:
|
||||
# Готовность = БД отвечает + миграции применены (см. /health/ready).
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health/ready || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
- api-data:/app/App_Data
|
||||
- api-logs:/app/logs
|
||||
|
|
@ -44,7 +52,8 @@ services:
|
|||
container_name: food-market-web
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
api:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8081:80" # web SPA, not on 80 (legacy nginx holds it)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace foodmarket.Api.Infrastructure.Health;
|
||||
|
||||
/// <summary>Readiness-проба: приложение готово принимать трафик только когда
|
||||
/// БД отвечает И все миграции применены. Если есть неприменённые миграции —
|
||||
/// схема не соответствует коду (например, контейнер поднялся раньше, чем
|
||||
/// отработал <c>Migrate()</c>, или откатили не ту версию) — отдаём Unhealthy,
|
||||
/// чтобы оркестратор/прокси не слал на этот инстанс запросы.</summary>
|
||||
public sealed class DatabaseReadyHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public DatabaseReadyHealthCheck(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await _db.Database.CanConnectAsync(cancellationToken))
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("Нет соединения с БД.");
|
||||
}
|
||||
|
||||
var pending = (await _db.Database.GetPendingMigrationsAsync(cancellationToken)).ToList();
|
||||
if (pending.Count > 0)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy(
|
||||
$"Неприменённые миграции: {pending.Count} (первая: {pending[0]}).",
|
||||
data: new Dictionary<string, object> { ["pendingMigrations"] = pending });
|
||||
}
|
||||
|
||||
return HealthCheckResult.Healthy("БД доступна, миграции применены.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("Ошибка проверки готовности БД.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -145,6 +145,12 @@
|
|||
// Anti-brute-force на /connect/token и /api/auth/signup (5/мин + 20/час на IP).
|
||||
builder.Services.AddAuthRateLimiting();
|
||||
|
||||
// Health-пробы: liveness (процесс жив) и readiness (БД + миграции применены).
|
||||
// Readiness-чек помечен тегом "ready", чтобы /health/live его не запускал.
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck<foodmarket.Api.Infrastructure.Health.DatabaseReadyHealthCheck>(
|
||||
"database", tags: ["ready"]);
|
||||
|
||||
// Email-отправка через MailKit. Singleton — внутри открывает scope для
|
||||
// свежего DbContext'а, поэтому конфиг (PlatformSettings) перечитывается
|
||||
// на каждой отправке без рестарта приложения.
|
||||
|
|
@ -220,6 +226,22 @@
|
|||
|
||||
app.MapControllers();
|
||||
|
||||
// Liveness: процесс отвечает — без обращения к зависимостям (Predicate=false
|
||||
// => ни один чек не запускается). Используется для рестарта зависшего контейнера.
|
||||
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
|
||||
{
|
||||
Predicate = _ => false,
|
||||
ResponseWriter = WriteHealthResponse,
|
||||
});
|
||||
|
||||
// Readiness: запускает чеки с тегом "ready" (БД + миграции). 503 если не готово.
|
||||
app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
|
||||
{
|
||||
Predicate = check => check.Tags.Contains("ready"),
|
||||
ResponseWriter = WriteHealthResponse,
|
||||
});
|
||||
|
||||
// Backward-compat: /health = liveness (Dockerfile/nginx исторически бьют сюда).
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "ok", time = DateTime.UtcNow }));
|
||||
|
||||
app.MapGet("/api/_debug/whoami", (HttpContext ctx) =>
|
||||
|
|
@ -290,3 +312,22 @@
|
|||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
|
||||
// JSON-ответ health-проб: общий статус + по каждому чеку. text/plain по умолчанию
|
||||
// неудобен для мониторинга; отдаём структурировано.
|
||||
static Task WriteHealthResponse(HttpContext context,
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.HealthReport report)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
return context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
status = report.Status.ToString(),
|
||||
totalDurationMs = report.TotalDuration.TotalMilliseconds,
|
||||
checks = report.Entries.Select(e => new
|
||||
{
|
||||
name = e.Key,
|
||||
status = e.Value.Status.ToString(),
|
||||
description = e.Value.Description,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue