feat(health): wire ISiteEventLogger.FailedWriteCount into SiteHealthReport (#30, M2.16)

Add SiteHealthReport.SiteEventLogWriteFailures (trailing optional long = 0,
additive-only), ISiteHealthCollector.SetSiteEventLogWriteFailures (default
no-op so existing fakes compile), and SiteEventLogFailureCountReporter
(hosted service in HealthMonitoring, Func<long> delegate to avoid the
HealthMonitoring → StoreAndForward → SiteEventLogging cycle).

Registration helper AddSiteEventLogHealthMetricsBridge added to
HealthMonitoring.ServiceCollectionExtensions; wired in
SiteServiceRegistration after AddSiteEventLogging.

Tests: SiteEventLogWriteFailuresMetricTests (4 collector tests) +
SiteEventLogFailureCountReporterTests (2 poller tests) in
HealthMonitoring.Tests. 79/79 HealthMonitoring.Tests green,
59/59 SiteEventLogging.Tests green, 0 warnings.
This commit is contained in:
Joseph Doherty
2026-06-16 07:14:54 -04:00
parent e1ee37e508
commit d81f747434
9 changed files with 394 additions and 6 deletions
@@ -1,5 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring;
@@ -50,6 +52,68 @@ public static class ServiceCollectionExtensions
return services;
}
/// <summary>
/// Site Event Logging (#12) M2.16 (#30) — register the
/// <see cref="SiteEventLogFailureCountReporter"/> hosted service that
/// periodically reads the cumulative event-log write-failure count and
/// pushes it into <see cref="ISiteHealthCollector"/> as a point-in-time
/// snapshot (<c>SiteEventLogWriteFailures</c> on the site health report).
/// </summary>
/// <remarks>
/// <para>
/// Must be called AFTER <see cref="AddSiteHealthMonitoring"/> (or
/// <see cref="AddHealthMonitoring"/>) which registers the
/// <see cref="ISiteHealthCollector"/> the reporter depends on.
/// </para>
/// <para>
/// <b>Why a Func&lt;long&gt; delegate instead of ISiteEventLogger.</b>
/// <c>HealthMonitoring</c> must not reference <c>SiteEventLogging</c> directly —
/// the <c>StoreAndForward → SiteEventLogging</c> edge already exists in the
/// transitive graph, and <c>HealthMonitoring → StoreAndForward</c> is an
/// existing direct reference; adding <c>HealthMonitoring → SiteEventLogging</c>
/// would complete a cycle. The <see cref="Func{TResult}"/> delegate seam keeps
/// the dependency acyclic: the caller (Host site wiring) captures
/// <c>ISiteEventLogger.FailedWriteCount</c> as a lambda and passes it here.
/// </para>
/// <para>
/// Idempotent — a sentinel check on the
/// <see cref="SiteEventLogFailureCountReporter"/> hosted-service descriptor
/// short-circuits subsequent calls so the hosted service is not
/// double-registered (AddHostedService has no TryAdd variant).
/// </para>
/// </remarks>
/// <param name="services">The service collection to register into.</param>
/// <param name="failedWriteCountProvider">
/// A factory delegate that, given the root <see cref="IServiceProvider"/>,
/// returns a <see cref="Func{TResult}"/> that reads the current cumulative
/// event-log write-failure count. Typically:
/// <c>sp => () => sp.GetRequiredService&lt;ISiteEventLogger&gt;().FailedWriteCount</c>.
/// The factory is evaluated once at hosted-service resolution time; the inner
/// <see cref="Func{TResult}"/> is called on every poll tick.
/// </param>
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
public static IServiceCollection AddSiteEventLogHealthMetricsBridge(
this IServiceCollection services,
Func<IServiceProvider, Func<long>> failedWriteCountProvider)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(failedWriteCountProvider);
// Idempotent guard — mirrors AddAuditLogHealthMetricsBridge's
// SiteAuditBacklogReporter sentinel check.
if (services.Any(d => d.ImplementationType == typeof(SiteEventLogFailureCountReporter)))
{
return services;
}
services.AddHostedService(sp => new SiteEventLogFailureCountReporter(
failedWriteCountProvider(sp),
sp.GetRequiredService<ISiteHealthCollector>(),
sp.GetRequiredService<ILogger<SiteEventLogFailureCountReporter>>()));
return services;
}
/// <summary>
/// HealthMonitoring-014: register the <see cref="HealthMonitoringOptionsValidator"/>
/// so a misconfigured <c>ScadaBridge:HealthMonitoring</c> section (zero/negative