Files
scadaproj/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health/ZbHealthEndpointExtensions.cs
T
Joseph Doherty 544a6ddb77 Fix all baseline code-review findings across the six shared libraries
Resolves the 35 findings from the 2026-06-01 baseline (commit 26ba1c7),
test-first for every behavioral change. +51 tests (331 -> 382 passing, 0 failed).

- Telemetry-001 (HIGH): RedactionEnricher now honours property removal, so a
  redactor that drops a key actually scrubs the secret from the event.
- Auth: LDAP validator ValidateOnStart; API-key verify no longer fails on a
  best-effort MarkUsed write or a corrupt scopes column (fail-closed); LDAP cert
  validation hook; KeyPrefix persistence aligned; README algorithm corrected.
- Health: Akka checks return Degraded (not throw) when the cluster isn't up yet;
  GrpcDependencyHealthCheck catch-all; null 'description' rendered; composite
  endpoint builder; XML docs shipped.
- Audit: CompositeAuditWriter no longer re-throws OperationCanceledException;
  TruncatingAuditRedactor over-redact scrubs Target + safe negative max; options
  record; XML docs shipped.
- Configuration: TryAddEnumerable idempotent registration; consistent port
  quoting; strict invariant port parsing; XML docs + README packaged.
- Theme: mobile toggle is now CSS-only (no Bootstrap JS); token/CSS hygiene;
  XML docs on the public parameter surface.

Shared-contract/spec docs updated where the code was the source of truth
(observability service.instance.id, MapZbMetrics, redactor reach). All changes
additive/back-compatible at v0.1.0. code-reviews bookkeeping follows separately.
2026-06-01 11:22:14 -04:00

115 lines
5.0 KiB
C#

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace ZB.MOM.WW.Health;
/// <summary>
/// Maps the canonical ZB.MOM.WW three-tier health endpoints in one call.
/// </summary>
public static class ZbHealthEndpointExtensions
{
/// <summary>
/// Maps the three health tiers:
/// <list type="bullet">
/// <item><description><c>/health/ready</c> — runs only checks tagged <see cref="ZbHealthTags.Ready"/>.</description></item>
/// <item><description><c>/health/active</c> — runs only checks tagged <see cref="ZbHealthTags.Active"/>.</description></item>
/// <item><description><c>/healthz</c> — bare process liveness; runs no checks (always 200 while the process is up).</description></item>
/// </list>
/// All three are anonymous. Status mapping is the ASP.NET Core default:
/// Healthy/Degraded → 200, Unhealthy → 503.
/// </summary>
/// <remarks>
/// Does NOT call <c>services.AddHealthChecks()</c> — the caller registers probes and their tags.
/// The readiness and active tiers use the canonical JSON writer
/// (<see cref="ZbHealthWriter.WriteJsonAsync"/>) unless overridden via
/// <see cref="ZbHealthEndpointOptions.ResponseWriter"/>. The liveness tier runs no checks and
/// emits a minimal <c>200 OK</c> body.
/// </remarks>
/// <returns>
/// A composite <see cref="IEndpointConventionBuilder"/> that fans every chained convention out to
/// <em>all three</em> health endpoints (readiness, active, and liveness). For example,
/// <c>endpoints.MapZbHealth().RequireHost("…")</c> gates all three endpoints, as a caller expects.
/// </returns>
public static IEndpointConventionBuilder MapZbHealth(
this IEndpointRouteBuilder endpoints,
ZbHealthEndpointOptions? options = null)
{
ArgumentNullException.ThrowIfNull(endpoints);
options ??= new ZbHealthEndpointOptions();
var responseWriter = options.ResponseWriter ?? ZbHealthWriter.WriteJsonAsync;
var ready = endpoints.MapHealthChecks(options.ReadyPath, new HealthCheckOptions
{
Predicate = static c => c.Tags.Contains(ZbHealthTags.Ready),
ResponseWriter = responseWriter,
}).AllowAnonymous();
var active = endpoints.MapHealthChecks(options.ActivePath, new HealthCheckOptions
{
Predicate = static c => c.Tags.Contains(ZbHealthTags.Active),
ResponseWriter = responseWriter,
}).AllowAnonymous();
// Liveness: run no checks. The endpoint returns 200 as long as the process can respond.
// No JSON writer — the empty report would carry no useful data, so the framework default
// (a minimal plain-text body) is sufficient.
var live = endpoints.MapHealthChecks(options.LivePath, new HealthCheckOptions
{
Predicate = static _ => false,
}).AllowAnonymous();
return new CompositeEndpointConventionBuilder(ready, active, live);
}
/// <summary>
/// An <see cref="IEndpointConventionBuilder"/> that forwards each convention to several
/// underlying builders, so conventions chained onto the result of
/// <see cref="MapZbHealth(IEndpointRouteBuilder, ZbHealthEndpointOptions?)"/> apply to all three
/// health endpoints rather than just one.
/// </summary>
private sealed class CompositeEndpointConventionBuilder : IEndpointConventionBuilder
{
private readonly IEndpointConventionBuilder[] _builders;
public CompositeEndpointConventionBuilder(params IEndpointConventionBuilder[] builders) =>
_builders = builders;
public void Add(Action<EndpointBuilder> convention)
{
foreach (var builder in _builders)
builder.Add(convention);
}
public void Finally(Action<EndpointBuilder> finalConvention)
{
foreach (var builder in _builders)
builder.Finally(finalConvention);
}
}
/// <summary>
/// Maps the three health tiers, configuring options inline. See the other
/// <see cref="MapZbHealth(IEndpointRouteBuilder, ZbHealthEndpointOptions?)"/> overload for tier semantics.
/// </summary>
/// <param name="endpoints">The endpoint route builder to map onto.</param>
/// <param name="configure">Callback that mutates a fresh <see cref="ZbHealthEndpointOptions"/>.</param>
/// <returns>
/// A composite <see cref="IEndpointConventionBuilder"/> that fans chained conventions out to all
/// three health endpoints.
/// </returns>
public static IEndpointConventionBuilder MapZbHealth(
this IEndpointRouteBuilder endpoints,
Action<ZbHealthEndpointOptions> configure)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(configure);
var options = new ZbHealthEndpointOptions();
configure(options);
return endpoints.MapZbHealth(options);
}
}