using System.Diagnostics; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using StellaOps.Scanner.WebService.Diagnostics; using StellaOps.Scanner.WebService.Options; namespace StellaOps.Scanner.WebService.Endpoints; internal static class HealthEndpoints { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); public static void MapHealthEndpoints(this IEndpointRouteBuilder endpoints) { ArgumentNullException.ThrowIfNull(endpoints); var group = endpoints.MapGroup("/"); group.MapGet("/healthz", HandleHealth) .WithName("scanner.health") .Produces(StatusCodes.Status200OK) .AllowAnonymous(); group.MapGet("/readyz", HandleReady) .WithName("scanner.ready") .Produces(StatusCodes.Status200OK) .AllowAnonymous(); } private static IResult HandleHealth( ServiceStatus status, IOptions options, HttpContext context) { ApplyNoCache(context.Response); var snapshot = status.CreateSnapshot(); var uptimeSeconds = Math.Max((snapshot.CapturedAt - snapshot.StartedAt).TotalSeconds, 0d); var telemetry = new TelemetrySnapshot( Enabled: options.Value.Telemetry.Enabled, Logging: options.Value.Telemetry.EnableLogging, Metrics: options.Value.Telemetry.EnableMetrics, Tracing: options.Value.Telemetry.EnableTracing); var document = new HealthDocument( Status: "healthy", StartedAt: snapshot.StartedAt, CapturedAt: snapshot.CapturedAt, UptimeSeconds: uptimeSeconds, Telemetry: telemetry); return Json(document, StatusCodes.Status200OK); } private static async Task HandleReady( ServiceStatus status, HttpContext context, CancellationToken cancellationToken) { ApplyNoCache(context.Response); await Task.CompletedTask; status.RecordReadyCheck(success: true, latency: TimeSpan.Zero, error: null); var snapshot = status.CreateSnapshot(); var ready = snapshot.Ready; var document = new ReadyDocument( Status: ready.IsReady ? "ready" : "unready", CheckedAt: ready.CheckedAt, LatencyMs: ready.Latency?.TotalMilliseconds, Error: ready.Error); return Json(document, StatusCodes.Status200OK); } private static void ApplyNoCache(HttpResponse response) { response.Headers.CacheControl = "no-store, no-cache, max-age=0, must-revalidate"; response.Headers.Pragma = "no-cache"; response.Headers["Expires"] = "0"; } private static IResult Json(T value, int statusCode) { var payload = JsonSerializer.Serialize(value, JsonOptions); return Results.Content(payload, "application/json", Encoding.UTF8, statusCode); } internal sealed record TelemetrySnapshot( bool Enabled, bool Logging, bool Metrics, bool Tracing); internal sealed record HealthDocument( string Status, DateTimeOffset StartedAt, DateTimeOffset CapturedAt, double UptimeSeconds, TelemetrySnapshot Telemetry); internal sealed record ReadyDocument( string Status, DateTimeOffset CheckedAt, double? LatencyMs, string? Error); }