feat(api): expose /health endpoint and health dashboard
Wire the infrastructure health checks into ASP.NET Core: - /health returns JSON via UIResponseWriter and maps Unhealthy to HTTP 503 - /health-ui renders a Razor Pages dashboard with per-check status, description, tags and duration - Tag health checks (rabbitmq, workers, ready, live) for monitoring filters - Link the dashboard from the Swagger description and the page layout
This commit is contained in:
@@ -26,8 +26,9 @@
|
||||
<PackageReference Include="Serilog.UI" Version="3.2.0" />
|
||||
<PackageReference Include="Serilog.UI.SqliteProvider" Version="1.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
|
||||
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
|
||||
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.10" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
@page "/health-ui"
|
||||
@model DigitalData.MessagingService.API.Pages.HealthDashboardModel
|
||||
@{
|
||||
ViewData["Title"] = "Health Dashboard";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<style>
|
||||
h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #e2e8f0; }
|
||||
.overall { display: inline-flex; align-items: center; gap: .5rem; font-size: 1rem; font-weight: 600;
|
||||
padding: .35rem .9rem; border-radius: 999px; margin-bottom: 2rem; }
|
||||
.overall.Healthy { background: #14532d; color: #4ade80; }
|
||||
.overall.Degraded { background: #713f12; color: #fbbf24; }
|
||||
.overall.Unhealthy { background: #7f1d1d; color: #f87171; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
|
||||
.card { background: #1a1d2e; border: 1px solid #2d3148; border-radius: .75rem; padding: 1.25rem; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .75rem; }
|
||||
.card-name { font-weight: 600; font-size: .95rem; }
|
||||
.badge { font-size: .75rem; font-weight: 700; padding: .2rem .6rem; border-radius: 999px; }
|
||||
.badge-Healthy { background: #14532d; color: #4ade80; }
|
||||
.badge-Degraded { background: #713f12; color: #fbbf24; }
|
||||
.badge-Unhealthy { background: #7f1d1d; color: #f87171; }
|
||||
.desc { font-size: .82rem; color: #94a3b8; }
|
||||
.tags { margin-top: .6rem; display: flex; gap: .4rem; flex-wrap: wrap; }
|
||||
.tag { font-size: .7rem; background: #2d3148; color: #94a3b8; padding: .15rem .5rem; border-radius: 4px; }
|
||||
.duration { font-size: .75rem; color: #64748b; margin-top: .5rem; }
|
||||
.refresh { font-size: .8rem; color: #64748b; margin-bottom: 1.5rem; }
|
||||
.refresh a { color: #a78bfa; text-decoration: none; }
|
||||
</style>
|
||||
|
||||
<h1>Health Dashboard</h1>
|
||||
|
||||
<div class="overall @Model.OverallStatus.ToString()">
|
||||
<span>@(Model.OverallStatus == Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Healthy ? "?" : "?")</span>
|
||||
<span>@Model.OverallStatus</span>
|
||||
</div>
|
||||
|
||||
<p class="refresh">
|
||||
Zuletzt geprüft: <strong>@Model.CheckedAt.ToString("yyyy-MM-dd HH:mm:ss")</strong> UTC
|
||||
· <a href="/health-ui">Aktualisieren</a>
|
||||
· <a href="/health" target="_blank">JSON</a>
|
||||
</p>
|
||||
|
||||
<div class="grid">
|
||||
@foreach (var entry in Model.Entries)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-name">@entry.Name</span>
|
||||
<span class="badge badge-@entry.Status.ToString()">@entry.Status</span>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(entry.Description))
|
||||
{
|
||||
<div class="desc">@entry.Description</div>
|
||||
}
|
||||
@if (entry.Tags.Any())
|
||||
{
|
||||
<div class="tags">
|
||||
@foreach (var tag in entry.Tags)
|
||||
{
|
||||
<span class="tag">@tag</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="duration">Dauer: @entry.Duration.TotalMilliseconds.ToString("0")ms</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace DigitalData.MessagingService.API.Pages;
|
||||
|
||||
public class HealthDashboardModel(HealthCheckService healthCheckService) : PageModel
|
||||
{
|
||||
public HealthStatus OverallStatus { get; private set; }
|
||||
public DateTime CheckedAt { get; private set; }
|
||||
public IReadOnlyList<HealthEntry> Entries { get; private set; } = [];
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var report = await healthCheckService.CheckHealthAsync();
|
||||
CheckedAt = DateTime.UtcNow;
|
||||
OverallStatus = report.Status;
|
||||
Entries = report.Entries
|
||||
.Select(e => new HealthEntry(
|
||||
Name: e.Key,
|
||||
Status: e.Value.Status,
|
||||
Description: e.Value.Description,
|
||||
Duration: e.Value.Duration,
|
||||
Tags: e.Value.Tags))
|
||||
.OrderBy(e => e.Status)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public record HealthEntry(
|
||||
string Name,
|
||||
HealthStatus Status,
|
||||
string? Description,
|
||||
TimeSpan Duration,
|
||||
IEnumerable<string> Tags);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] — DigitalData MessagingService</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f1117; color: #e2e8f0; }
|
||||
header { background: #1a1d2e; border-bottom: 1px solid #2d3148; padding: 0 2rem; display: flex; align-items: center; height: 56px; gap: 1.5rem; }
|
||||
header a { color: #a78bfa; text-decoration: none; font-weight: 600; font-size: 1rem; }
|
||||
header a:hover { color: #c4b5fd; }
|
||||
header .brand { color: #e2e8f0; font-size: 1.1rem; font-weight: 700; margin-right: auto; }
|
||||
main { padding: 2rem; max-width: 1100px; margin: 0 auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<span class="brand">DigitalData MessagingService</span>
|
||||
<a href="/health-ui">Health</a>
|
||||
<a href="/serilog-ui">Logs</a>
|
||||
<a href="/swagger">API</a>
|
||||
</header>
|
||||
<main>
|
||||
@RenderBody()
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
@using DigitalData.MessagingService.API.Pages
|
||||
@namespace DigitalData.MessagingService.API.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -1,6 +1,10 @@
|
||||
using DigitalData.MessagingService.API.Middleware;
|
||||
using DigitalData.MessagingService.Application;
|
||||
using DigitalData.MessagingService.Infrastructure;
|
||||
using DigitalData.MessagingService.Infrastructure.Services.Background;
|
||||
using HealthChecks.UI.Client;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Serilog;
|
||||
using Serilog.Ui.Core.Extensions;
|
||||
using Serilog.Ui.SqliteDataProvider.Extensions;
|
||||
@@ -66,7 +70,14 @@ try
|
||||
// Register Infrastructure layer (RabbitMQ, Repositories, etc.)
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
|
||||
// Health checks — each check corresponds to a singleton registered in the Infrastructure layer
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck<ConsumerInitHealthCheck>("rabbitmq-consumers", tags: ["rabbitmq", "ready"])
|
||||
.AddCheck<RabbitMqConnectionHealthCheck>("rabbitmq-connection", tags: ["rabbitmq", "live"])
|
||||
.AddCheck<EmailSyncWorkerHealthCheck>("email-sync-worker", tags: ["workers", "live"]);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
@@ -95,6 +106,7 @@ try
|
||||
## Weiterführende Links
|
||||
|
||||
- [Serilog Log-Viewer ↗](/serilog-ui)
|
||||
- [Health Status ↗](/health-ui)
|
||||
"""
|
||||
});
|
||||
});
|
||||
@@ -155,6 +167,22 @@ try
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
// Health check endpoint — raw JSON, used by HealthChecks UI and monitoring tools
|
||||
// Returns HTTP 503 if any check tagged 'ready' is Unhealthy
|
||||
app.MapHealthChecks("/health", new HealthCheckOptions
|
||||
{
|
||||
Predicate = _ => true,
|
||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
|
||||
ResultStatusCodes =
|
||||
{
|
||||
[HealthStatus.Healthy] = StatusCodes.Status200OK,
|
||||
[HealthStatus.Degraded] = StatusCodes.Status200OK,
|
||||
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable,
|
||||
}
|
||||
});
|
||||
|
||||
app.MapRazorPages();
|
||||
|
||||
Log.Information("MessagingService API started successfully");
|
||||
|
||||
app.Run();
|
||||
|
||||
Reference in New Issue
Block a user