163 lines
8.6 KiB
C#
163 lines
8.6 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Sentinel marker used by <see cref="AddSiteEventLogHealthMetricsBridge"/> to
|
|
/// implement an idempotency guard. Because the reporter is registered via a
|
|
/// factory-lambda overload of <c>AddHostedService</c>, its
|
|
/// <see cref="Microsoft.Extensions.DependencyInjection.ServiceDescriptor.ImplementationType"/>
|
|
/// is <see langword="null"/> — checking it would be a silent no-op. Registering
|
|
/// this marker as a singleton and guarding on its <c>ServiceType</c> gives a
|
|
/// reliable, allocation-free sentinel that works regardless of how the hosted
|
|
/// service was wired.
|
|
/// </summary>
|
|
private sealed class SiteEventLogHealthMetricsBridgeMarker { }
|
|
|
|
/// <summary>
|
|
/// Register site-side health monitoring services (metric collection + periodic reporting).
|
|
/// Call this on site nodes only. For central, call AddCentralHealthAggregation() instead.
|
|
/// </summary>
|
|
/// <param name="services">The DI service collection to register into.</param>
|
|
/// <returns>The same <paramref name="services"/> instance, for call chaining.</returns>
|
|
public static IServiceCollection AddSiteHealthMonitoring(this IServiceCollection services)
|
|
{
|
|
AddOptionsValidation(services);
|
|
services.AddSingleton<ISiteHealthCollector, SiteHealthCollector>();
|
|
services.AddHostedService<HealthReportSender>();
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register shared health monitoring services (safe for both central and site).
|
|
/// Does not start the HealthReportSender — call AddSiteHealthMonitoring() on site nodes for that.
|
|
/// </summary>
|
|
/// <param name="services">The DI service collection to register into.</param>
|
|
/// <returns>The same <paramref name="services"/> instance, for call chaining.</returns>
|
|
public static IServiceCollection AddHealthMonitoring(this IServiceCollection services)
|
|
{
|
|
AddOptionsValidation(services);
|
|
services.AddSingleton<ISiteHealthCollector, SiteHealthCollector>();
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register central-side health aggregation services. Includes the
|
|
/// <see cref="CentralHealthReportLoop"/> that generates a self-report
|
|
/// for the central cluster so it appears on /monitoring/health.
|
|
/// </summary>
|
|
/// <param name="services">The DI service collection to register into.</param>
|
|
/// <returns>The same <paramref name="services"/> instance, for call chaining.</returns>
|
|
public static IServiceCollection AddCentralHealthAggregation(this IServiceCollection services)
|
|
{
|
|
AddOptionsValidation(services);
|
|
services.AddSingleton<CentralHealthAggregator>();
|
|
services.AddSingleton<ICentralHealthAggregator>(sp => sp.GetRequiredService<CentralHealthAggregator>());
|
|
services.AddHostedService(sp => sp.GetRequiredService<CentralHealthAggregator>());
|
|
services.AddHostedService<CentralHealthReportLoop>();
|
|
|
|
// 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<IKpiSampleSource, Kpi.SiteHealthKpiSampleSource>());
|
|
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<long> delegate instead of ISiteEventLogger.</b>
|
|
/// A direct <c>HealthMonitoring → SiteEventLogging</c> reference is avoided to
|
|
/// prevent an undesirable low-level coupling: <c>SiteEventLogging</c> is a
|
|
/// leaf component that should not pull in higher-level infrastructure. The
|
|
/// <see cref="Func{TResult}"/> delegate seam keeps the reference one-way and
|
|
/// loose: the caller (Host site wiring) captures
|
|
/// <c>ISiteEventLogger.FailedWriteCount</c> as a lambda and passes it here.
|
|
/// Note: <c>HealthMonitoring → StoreAndForward → SiteEventLogging</c> already
|
|
/// exists as a transitive path, so a direct reference would not introduce a
|
|
/// cycle — the delegate is purely a coupling-avoidance measure.
|
|
/// </para>
|
|
/// <para>
|
|
/// Idempotent — a <see cref="SiteEventLogHealthMetricsBridgeMarker"/> singleton
|
|
/// is used as the sentinel. Because the reporter is registered via a factory-lambda
|
|
/// overload of <c>AddHostedService</c>, its
|
|
/// <see cref="Microsoft.Extensions.DependencyInjection.ServiceDescriptor.ImplementationType"/>
|
|
/// is <see langword="null"/>; checking it would be a silent no-op and a second
|
|
/// call would spin up a second polling timer. Guarding on the marker's
|
|
/// <c>ServiceType</c> is always reliable regardless of how the hosted service
|
|
/// was wired (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<ISiteEventLogger>().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 — 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<SiteEventLogHealthMetricsBridgeMarker>();
|
|
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
|
|
/// intervals, or a <c>CentralOfflineTimeout</c> shorter than
|
|
/// <c>OfflineTimeout</c>) is rejected with a clear, key-naming message when the
|
|
/// hosted services resolve their options at startup — rather than crashing
|
|
/// later inside a <see cref="PeriodicTimer"/> constructor with an opaque
|
|
/// <see cref="ArgumentOutOfRangeException"/>. Idempotent so it is safe when
|
|
/// more than one of the registration methods above is called.
|
|
/// </summary>
|
|
private static void AddOptionsValidation(IServiceCollection services)
|
|
{
|
|
services.TryAddEnumerable(
|
|
ServiceDescriptor.Singleton<IValidateOptions<HealthMonitoringOptions>, HealthMonitoringOptionsValidator>());
|
|
}
|
|
}
|