Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
159 lines
5.1 KiB
C#
159 lines
5.1 KiB
C#
using System.Collections.Generic;
|
|
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.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Scanner.WebService.Diagnostics;
|
|
using StellaOps.Scanner.WebService.Options;
|
|
using StellaOps.Scanner.Surface.Env;
|
|
using StellaOps.Scanner.Surface.Validation;
|
|
|
|
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<HealthDocument>(StatusCodes.Status200OK)
|
|
.AllowAnonymous();
|
|
|
|
group.MapGet("/readyz", HandleReady)
|
|
.WithName("scanner.ready")
|
|
.Produces<ReadyDocument>(StatusCodes.Status200OK)
|
|
.AllowAnonymous();
|
|
}
|
|
|
|
private static IResult HandleHealth(
|
|
ServiceStatus status,
|
|
IOptions<ScannerWebServiceOptions> 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<IResult> HandleReady(
|
|
ServiceStatus status,
|
|
ISurfaceValidatorRunner validatorRunner,
|
|
ISurfaceEnvironment surfaceEnvironment,
|
|
ILoggerFactory loggerFactory,
|
|
HttpContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ApplyNoCache(context.Response);
|
|
|
|
ArgumentNullException.ThrowIfNull(loggerFactory);
|
|
|
|
var logger = loggerFactory.CreateLogger("Scanner.WebService.Health");
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var success = true;
|
|
string? error = null;
|
|
|
|
try
|
|
{
|
|
var validationContext = SurfaceValidationContext.Create(
|
|
context.RequestServices,
|
|
"Scanner.WebService.ReadyCheck",
|
|
surfaceEnvironment.Settings,
|
|
properties: new Dictionary<string, object?>
|
|
{
|
|
["path"] = context.Request.Path.ToString()
|
|
});
|
|
|
|
await validatorRunner.EnsureAsync(validationContext, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
catch (SurfaceValidationException ex)
|
|
{
|
|
success = false;
|
|
error = ex.Message;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
success = false;
|
|
error = ex.Message;
|
|
logger.LogError(ex, "Surface validation failed during ready check.");
|
|
}
|
|
finally
|
|
{
|
|
stopwatch.Stop();
|
|
}
|
|
|
|
status.RecordReadyCheck(success, stopwatch.Elapsed, error);
|
|
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);
|
|
|
|
var statusCode = success ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
|
return Json(document, statusCode);
|
|
}
|
|
|
|
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>(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);
|
|
}
|