using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi; namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring; public static class ServiceCollectionExtensions { /// /// Sentinel marker used by to /// implement an idempotency guard. Because the reporter is registered via a /// factory-lambda overload of AddHostedService, its /// /// is — checking it would be a silent no-op. Registering /// this marker as a singleton and guarding on its ServiceType gives a /// reliable, allocation-free sentinel that works regardless of how the hosted /// service was wired. /// private sealed class SiteEventLogHealthMetricsBridgeMarker { } /// /// Register site-side health monitoring services (metric collection + periodic reporting). /// Call this on site nodes only. For central, call AddCentralHealthAggregation() instead. /// /// The DI service collection to register into. /// The same instance, for call chaining. public static IServiceCollection AddSiteHealthMonitoring(this IServiceCollection services) { AddOptionsValidation(services); services.AddSingleton(); services.AddHostedService(); return services; } /// /// Register shared health monitoring services (safe for both central and site). /// Does not start the HealthReportSender — call AddSiteHealthMonitoring() on site nodes for that. /// /// The DI service collection to register into. /// The same instance, for call chaining. public static IServiceCollection AddHealthMonitoring(this IServiceCollection services) { AddOptionsValidation(services); services.AddSingleton(); return services; } /// /// Register central-side health aggregation services. Includes the /// that generates a self-report /// for the central cluster so it appears on /monitoring/health. /// /// The DI service collection to register into. /// The same instance, for call chaining. public static IServiceCollection AddCentralHealthAggregation(this IServiceCollection services) { AddOptionsValidation(services); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(); // M6 "KPI History & Trends" (K9): per-site Site Health KPI sample source. // Reads the in-memory central aggregator (a singleton) rather than a // repository; registered Scoped to match the recorder's per-tick scope // and the other M6 sample sources (a scoped source over a singleton // dependency is fine — no captive dependency). services.TryAddEnumerable( ServiceDescriptor.Scoped()); return services; } /// /// Site Event Logging (#12) M2.16 (#30) — register the /// hosted service that /// periodically reads the cumulative event-log write-failure count and /// pushes it into as a point-in-time /// snapshot (SiteEventLogWriteFailures on the site health report). /// /// /// /// Must be called AFTER (or /// ) which registers the /// the reporter depends on. /// /// /// Why a Func<long> delegate instead of ISiteEventLogger. /// A direct HealthMonitoring → SiteEventLogging reference is avoided to /// prevent an undesirable low-level coupling: SiteEventLogging is a /// leaf component that should not pull in higher-level infrastructure. The /// delegate seam keeps the reference one-way and /// loose: the caller (Host site wiring) captures /// ISiteEventLogger.FailedWriteCount as a lambda and passes it here. /// Note: HealthMonitoring → StoreAndForward → SiteEventLogging already /// exists as a transitive path, so a direct reference would not introduce a /// cycle — the delegate is purely a coupling-avoidance measure. /// /// /// Idempotent — a singleton /// is used as the sentinel. Because the reporter is registered via a factory-lambda /// overload of AddHostedService, its /// /// is ; checking it would be a silent no-op and a second /// call would spin up a second polling timer. Guarding on the marker's /// ServiceType is always reliable regardless of how the hosted service /// was wired (AddHostedService has no TryAdd variant). /// /// /// The service collection to register into. /// /// A factory delegate that, given the root , /// returns a that reads the current cumulative /// event-log write-failure count. Typically: /// sp => () => sp.GetRequiredService<ISiteEventLogger>().FailedWriteCount. /// The factory is evaluated once at hosted-service resolution time; the inner /// is called on every poll tick. /// /// The same for chaining. public static IServiceCollection AddSiteEventLogHealthMetricsBridge( this IServiceCollection services, Func> failedWriteCountProvider) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(failedWriteCountProvider); // Idempotent guard — uses the marker type rather than ImplementationType because // AddHostedService(factory-lambda) sets only ImplementationFactory and leaves // ImplementationType null; an ImplementationType == check is a silent no-op for // factory-registered services. The marker singleton's ServiceType is always set. if (services.Any(d => d.ServiceType == typeof(SiteEventLogHealthMetricsBridgeMarker))) { return services; } services.AddSingleton(); services.AddHostedService(sp => new SiteEventLogFailureCountReporter( failedWriteCountProvider(sp), sp.GetRequiredService(), sp.GetRequiredService>())); return services; } /// /// HealthMonitoring-014: register the /// so a misconfigured ScadaBridge:HealthMonitoring section (zero/negative /// intervals, or a CentralOfflineTimeout shorter than /// OfflineTimeout) is rejected with a clear, key-naming message when the /// hosted services resolve their options at startup — rather than crashing /// later inside a constructor with an opaque /// . Idempotent so it is safe when /// more than one of the registration methods above is called. /// private static void AddOptionsValidation(IServiceCollection services) { services.TryAddEnumerable( ServiceDescriptor.Singleton, HealthMonitoringOptionsValidator>()); } }