From eb2514f1fccc3dbf028fc4f5c7d8982344e6ead6 Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 13 Jul 2026 13:02:11 +0200 Subject: [PATCH] Add health check UI and Serilog dashboard integration Introduced a `/health-ui` route with a custom HTML-based health check UI, leveraging `HealthCheckService`. Added `GenerateHealthCheckHtml` to dynamically render health check results with Bootstrap styling, auto-refresh, and detailed status reporting. Enabled Serilog self-diagnostics and integrated `app.UseSerilogUi()` for a Serilog log dashboard. Enhanced user experience with navigation links, animations, and responsive design. Improved observability and monitoring capabilities. --- ECMJobRunner.WebCron/Program.cs | 317 ++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) diff --git a/ECMJobRunner.WebCron/Program.cs b/ECMJobRunner.WebCron/Program.cs index 80e0ce8..1f40d1b 100644 --- a/ECMJobRunner.WebCron/Program.cs +++ b/ECMJobRunner.WebCron/Program.cs @@ -5,10 +5,12 @@ using ECMJobRunner.WebCron.ProfileWorker; using Hangfire; using Hangfire.SqlServer; using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Serilog; using Serilog.Ui.Core.Extensions; using Serilog.Ui.SqliteDataProvider.Extensions; using Serilog.Ui.Web.Extensions; +using System.Text; // Enable Serilog self-diagnostics Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine($"[SERILOG] {msg}")); @@ -138,6 +140,19 @@ app.UseHangfireDashboard("/hangfire", new DashboardOptions Authorization = new[] { new AllowAllDashboardAuthorizationFilter() } }); +// Add Health Check UI route (accessible outside Hangfire dashboard) +app.MapGet("/health-ui", async (HealthCheckService healthCheckService, HttpContext context) => +{ + var report = await healthCheckService.CheckHealthAsync(context.RequestAborted); + + var html = GenerateHealthCheckHtml(report); + + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(html); + + return Results.Empty; +}); + // Add Serilog.UI Dashboard app.UseSerilogUi(); @@ -186,3 +201,305 @@ finally { Log.CloseAndFlush(); } + +// Helper method to generate health check HTML +static string GenerateHealthCheckHtml(HealthReport report) +{ + var sb = new StringBuilder(); + + sb.AppendLine(@" + + + + + + Health Check - ECMJobRunner + + + + +"); + + // Navigation + sb.Append(@" +
+
"); + + // Append emojis separately (not in verbatim string) + sb.AppendLine(); + sb.Append(" 🔧 Hangfire Dashboard"); + sb.AppendLine(); + sb.Append(" 📋 Logs (Serilog.UI)"); + sb.AppendLine(); + sb.Append(" 📊 Health API"); + sb.AppendLine(); + sb.Append(" 💚 Health UI"); + sb.AppendLine(); + sb.AppendLine("
"); + + // Overall Status Header + var statusClass = report.Status switch + { + HealthStatus.Healthy => "status-healthy", + HealthStatus.Degraded => "status-degraded", + HealthStatus.Unhealthy => "status-unhealthy", + _ => "" + }; + + var badgeClass = report.Status switch + { + HealthStatus.Healthy => "badge-healthy", + HealthStatus.Degraded => "badge-degraded", + HealthStatus.Unhealthy => "badge-unhealthy", + _ => "badge-secondary" + }; + + var icon = report.Status switch + { + HealthStatus.Healthy => "✅", + HealthStatus.Degraded => "⚠️", + HealthStatus.Unhealthy => "❌", + _ => "❓" + }; + + sb.Append($@" +
+
+
+

"); + sb.Append(icon); + sb.Append($@" Health Check Status

+

Last checked: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC

+
+
+

{report.Status}

+
+
+
+ +
+
+
+
"); + sb.AppendLine(); + sb.Append(" 📊 Summary"); + sb.AppendLine(); + sb.Append($@"
+
+
+ Overall Status: + {report.Status} +
+
+ Total Duration: + {report.TotalDuration.TotalMilliseconds:F0} ms +
+
+ Checks: + {report.Entries.Count} +
+
+
+
+ +
"); + + // Individual Checks + foreach (var entry in report.Entries) + { + var checkStatusClass = entry.Value.Status switch + { + HealthStatus.Healthy => "status-healthy", + HealthStatus.Degraded => "status-degraded", + HealthStatus.Unhealthy => "status-unhealthy", + _ => "" + }; + + var checkBadgeClass = entry.Value.Status switch + { + HealthStatus.Healthy => "badge-healthy", + HealthStatus.Degraded => "badge-degraded", + HealthStatus.Unhealthy => "badge-unhealthy", + _ => "badge-secondary" + }; + + var checkIcon = entry.Value.Status switch + { + HealthStatus.Healthy => "✅", + HealthStatus.Degraded => "⚠️", + HealthStatus.Unhealthy => "❌", + _ => "❓" + }; + + sb.Append($@" +
+
+
+ "); + sb.Append(checkIcon); + sb.Append($@" {entry.Key} + {entry.Value.Status} +
+
+
+
+ Description: + {entry.Value.Description ?? "N/A"} +
+
+ Duration: + {entry.Value.Duration.TotalMilliseconds:F0} ms +
"); + + if (entry.Value.Exception != null) + { + sb.AppendLine($@" +
+ Exception: + {System.Net.WebUtility.HtmlEncode(entry.Value.Exception.Message)} +
"); + } + + if (entry.Value.Data.Any()) + { + sb.AppendLine(@" +
+ Details: + "); + + foreach (var data in entry.Value.Data) + { + var value = data.Value?.ToString() ?? "N/A"; + + // Format DateTime values + if (data.Value is DateTime dt) + { + value = dt.ToString("yyyy-MM-dd HH:mm:ss"); + } + else if (data.Value is TimeSpan ts) + { + value = $"{ts.TotalSeconds:F1}s"; + } + + sb.AppendLine($@" + + + + "); + } + + sb.AppendLine(@" +
{data.Key}{value}
+
"); + } + + sb.AppendLine(@" +
+
"); + } + + sb.AppendLine(@" +
+
+
+ +
"); + sb.AppendLine(); + sb.Append(" Auto-refresh in 10s"); + sb.AppendLine(); + sb.AppendLine(@"
+ + +"); + + return sb.ToString(); +}