9cff87fe85
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
529 lines
31 KiB
C#
529 lines
31 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.Audit;
|
|
using ZB.MOM.WW.Configuration;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Kpi;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Redaction;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
|
using IAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IAuditWriter;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog;
|
|
|
|
/// <summary>
|
|
/// Composition root for the Audit Log component.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Registered <see cref="AuditLogOptions"/> + the validator. Extends the
|
|
/// surface with the site-side writer chain
|
|
/// (<see cref="SqliteAuditWriter"/> + <see cref="RingBufferFallback"/> +
|
|
/// <see cref="FallbackAuditWriter"/>) and the telemetry collaborators
|
|
/// (<see cref="ISiteAuditQueue"/>, <see cref="ISiteStreamAuditClient"/>,
|
|
/// <see cref="IAuditWriteFailureCounter"/>, <see cref="SiteAuditTelemetryOptions"/>,
|
|
/// <see cref="SqliteAuditWriterOptions"/>).
|
|
/// </para>
|
|
/// <para>
|
|
/// Audit Log sits alongside Notification Outbox and Site Call
|
|
/// Audit. <c>IAuditLogRepository</c> is registered by
|
|
/// <c>ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.ServiceCollectionExtensions.AddConfigurationDatabase</c>,
|
|
/// so the caller (the Host on the central node) must also call that.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
/// <summary>Configuration section bound to <see cref="AuditLogOptions"/>.</summary>
|
|
public const string ConfigSectionName = "AuditLog";
|
|
|
|
/// <summary>Configuration section bound to <see cref="SqliteAuditWriterOptions"/>.</summary>
|
|
public const string SiteWriterSectionName = "AuditLog:SiteWriter";
|
|
|
|
/// <summary>Configuration section bound to <see cref="SiteAuditTelemetryOptions"/>.</summary>
|
|
public const string SiteTelemetrySectionName = "AuditLog:SiteTelemetry";
|
|
|
|
/// <summary>Configuration section bound to <see cref="AuditLogPartitionMaintenanceOptions"/>.</summary>
|
|
public const string PartitionMaintenanceSectionName = "AuditLog:PartitionMaintenance";
|
|
|
|
/// <summary>Configuration section bound to <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Central.AuditLogPurgeOptions"/>.</summary>
|
|
public const string PurgeSectionName = "AuditLog:Purge";
|
|
|
|
/// <summary>Configuration section bound to <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Central.SiteAuditReconciliationOptions"/>.</summary>
|
|
public const string ReconciliationSectionName = "AuditLog:Reconciliation";
|
|
|
|
/// <summary>
|
|
/// Registers the Audit Log component services: options, the site
|
|
/// SQLite writer chain (primary + ring fallback + failure-counter sink),
|
|
/// and the site-→central telemetry collaborators. Idempotent re-registration
|
|
/// is not supported; call this exactly once per <see cref="IServiceCollection"/>.
|
|
/// </summary>
|
|
/// <param name="services">The service collection to register into.</param>
|
|
/// <param name="config">Application configuration used to bind <see cref="AuditLogOptions"/> and related options sections.</param>
|
|
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
|
public static IServiceCollection AddAuditLog(this IServiceCollection services, IConfiguration config)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
ArgumentNullException.ThrowIfNull(config);
|
|
|
|
// Top-level AuditLogOptions + validator (redaction policy, payload caps, etc.).
|
|
// Collapsed onto the shared ZB.MOM.WW.Configuration helper: it binds the
|
|
// "AuditLog" section, registers the validator, and enables ValidateOnStart in
|
|
// one call. Same section path as before; AddAuditLog is call-once per
|
|
// collection, and the helper's TryAddEnumerable is idempotent for the
|
|
// validator (a strict improvement over the previous AddSingleton).
|
|
services.AddValidatedOptions<AuditLogOptions, AuditLogOptionsValidator>(config, ConfigSectionName);
|
|
|
|
// The canonical IAuditRedactor is wired as
|
|
// ScadaBridgeAuditRedactor — same truncation + header / body /
|
|
// SQL-parameter redaction as the original pipeline, applied between
|
|
// event construction and persistence. Singleton — stateless; the
|
|
// IOptionsMonitor dependency picks up hot reloads on its own.
|
|
services.AddSingleton<IAuditRedactor, ScadaBridgeAuditRedactor>();
|
|
|
|
// Per-stage redactor-failure counter. NoOp default; this binding is
|
|
// later replaced with the Site Health Monitoring bridge that surfaces
|
|
// failures as AuditRedactionFailure on the site health report.
|
|
services.TryAddSingleton<IAuditRedactionFailureCounter, NoOpAuditRedactionFailureCounter>();
|
|
|
|
// Site writer + telemetry options bindings.
|
|
// BindConfiguration is not used because the configuration root supplied
|
|
// by the caller may not be the application root — we go through the
|
|
// section explicitly so a partial IConfiguration (e.g. a test stub
|
|
// anchored on the AuditLog section's parent) still works.
|
|
services.AddOptions<SqliteAuditWriterOptions>()
|
|
.Bind(config.GetSection(SiteWriterSectionName));
|
|
services.AddOptions<SiteAuditTelemetryOptions>()
|
|
.Bind(config.GetSection(SiteTelemetrySectionName));
|
|
|
|
// SqliteAuditWriter is a singleton with a single owned SqliteConnection
|
|
// and a background writer Task; multiple instances would race on the
|
|
// same file. Registered concretely so the ISiteAuditQueue + IAuditWriter
|
|
// forwards below resolve to the same instance — the actor must observe
|
|
// the writes made via the hot-path interface.
|
|
services.AddSingleton<SqliteAuditWriter>();
|
|
services.AddSingleton<ISiteAuditQueue>(sp => sp.GetRequiredService<SqliteAuditWriter>());
|
|
|
|
// RingBufferFallback: drop-oldest in-memory ring used by
|
|
// FallbackAuditWriter when the primary SQLite writer throws. Default
|
|
// capacity is fine (1024).
|
|
services.AddSingleton<RingBufferFallback>();
|
|
|
|
// IAuditWriteFailureCounter: NoOp default. This binding is later
|
|
// overridden with the real Site Health Monitoring counter. Registered
|
|
// before FallbackAuditWriter so the factory can resolve it.
|
|
services.AddSingleton<IAuditWriteFailureCounter, NoOpAuditWriteFailureCounter>();
|
|
|
|
// The script-thread surface is FallbackAuditWriter (primary + ring +
|
|
// counter), not the raw SqliteAuditWriter — primary failures must NEVER
|
|
// abort the user-facing action.
|
|
// The canonical IAuditRedactor singleton above is wired
|
|
// through the factory so every event written through this surface is
|
|
// truncated + redacted before it hits SQLite (and the ring on
|
|
// failure).
|
|
services.AddSingleton<IAuditWriter>(sp => new FallbackAuditWriter(
|
|
primary: sp.GetRequiredService<SqliteAuditWriter>(),
|
|
ring: sp.GetRequiredService<RingBufferFallback>(),
|
|
failureCounter: sp.GetRequiredService<IAuditWriteFailureCounter>(),
|
|
logger: sp.GetRequiredService<ILogger<FallbackAuditWriter>>(),
|
|
redactor: sp.GetRequiredService<IAuditRedactor>()));
|
|
|
|
// ISiteStreamAuditClient: NoOp default. This binding remains correct for
|
|
// central/test composition roots that have no SiteCommunicationActor.
|
|
// The real implementation is ClusterClientSiteAuditClient, which pushes
|
|
// audit telemetry to central over Akka ClusterClient via the site's
|
|
// SiteCommunicationActor — the Host wires it directly into the
|
|
// SiteAuditTelemetryActor's Props.Create call for site roles (it cannot
|
|
// be a DI singleton because it needs the SiteCommunicationActor IActorRef,
|
|
// created during Akka bootstrap, not at DI-composition time).
|
|
services.AddSingleton<ISiteStreamAuditClient, NoOpSiteStreamAuditClient>();
|
|
|
|
// Site-side dual emitter for cached-call lifecycle
|
|
// telemetry. ScriptRuntimeContext.ExternalSystem.CachedCall /
|
|
// Database.CachedWrite resolves this through DI and pushes one combined
|
|
// packet per lifecycle event; the forwarder writes the audit half
|
|
// through IAuditWriter and the operational half through the
|
|
// IOperationTrackingStore. The audit writer is always wired (the
|
|
// chain above); the operational tracking store is SITE-ONLY (registered
|
|
// by ZB.MOM.WW.ScadaBridge.SiteRuntime). On a Central composition root the tracking
|
|
// store has no registration, so the factory resolves it with GetService
|
|
// (returning null) — the forwarder degrades to "audit-only" emission,
|
|
// mirroring the lazy IAuditWriter chain established above.
|
|
services.AddSingleton<ICachedCallTelemetryForwarder>(sp =>
|
|
new CachedCallTelemetryForwarder(
|
|
sp.GetRequiredService<IAuditWriter>(),
|
|
sp.GetService<ZB.MOM.WW.ScadaBridge.Commons.Interfaces.IOperationTrackingStore>(),
|
|
sp.GetRequiredService<ILogger<CachedCallTelemetryForwarder>>(),
|
|
// INodeIdentityProvider is now required across
|
|
// every consumer in AddAuditLog. The Host's
|
|
// SiteServiceRegistration registers it as a singleton on both
|
|
// site and central paths, and the AddAuditLogTests fixture provides a
|
|
// FakeNodeIdentityProvider; a silent GetService() that
|
|
// returned null would mask a future composition root that
|
|
// forgot to register the provider.
|
|
sp.GetRequiredService<INodeIdentityProvider>()));
|
|
|
|
// Bridge the store-and-forward retry-loop observer hook
|
|
// to the cached-call forwarder so per-attempt + terminal telemetry
|
|
// emitted from the S&F retry sweep lands on the same SQLite hot-path
|
|
// as the script-thread CachedSubmit row. Registered as a singleton
|
|
// and also bound to ICachedCallLifecycleObserver so AddStoreAndForward
|
|
// can resolve it through DI.
|
|
// SourceNode-stamping: factory-resolved so the
|
|
// INodeIdentityProvider singleton can be threaded through — the
|
|
// bridge stamps SiteCallOperational.SourceNode from
|
|
// INodeIdentityProvider.NodeName on every cached-call lifecycle row.
|
|
// The provider is resolved with GetRequiredService —
|
|
// SiteServiceRegistration.BindSharedOptions registers it on both
|
|
// site and central paths, so a missing registration is a
|
|
// composition-root bug, not a silent null-SourceNode degradation.
|
|
services.AddSingleton<CachedCallLifecycleBridge>(sp => new CachedCallLifecycleBridge(
|
|
sp.GetRequiredService<ICachedCallTelemetryForwarder>(),
|
|
sp.GetRequiredService<ILogger<CachedCallLifecycleBridge>>(),
|
|
// Required, matches the other consumers in this
|
|
// composition root — the provider is always registered by
|
|
// SiteServiceRegistration.
|
|
sp.GetRequiredService<INodeIdentityProvider>()));
|
|
services.AddSingleton<ICachedCallLifecycleObserver>(
|
|
sp => sp.GetRequiredService<CachedCallLifecycleBridge>());
|
|
|
|
// Central audit-write failure counter — NoOp default
|
|
// for site/test composition roots that don't wire the central health
|
|
// snapshot. AddAuditLogCentralMaintenance below replaces this binding
|
|
// with the AuditCentralHealthSnapshot implementation so increments
|
|
// surface on the central dashboard.
|
|
services.TryAddSingleton<ICentralAuditWriteFailureCounter, NoOpCentralAuditWriteFailureCounter>();
|
|
|
|
// Inbound body-ceiling hit counter — NoOp default for
|
|
// site/test roots. AddAuditLogCentralMaintenance replaces this binding
|
|
// with the AuditCentralHealthSnapshot implementation so ceiling-hit
|
|
// counts surface on the central dashboard alongside write-failure and
|
|
// redaction-failure counters.
|
|
services.TryAddSingleton<IAuditInboundCeilingHitsCounter, NoOpAuditInboundCeilingHitsCounter>();
|
|
|
|
// Central direct-write audit writer used by
|
|
// NotificationOutboxActor and Inbound API to
|
|
// emit AuditLog rows that originate ON central, not via site telemetry.
|
|
// Singleton — the writer is stateless; its per-call scope opens a fresh
|
|
// IAuditLogRepository (a SCOPED EF Core service registered by
|
|
// ZB.MOM.WW.ScadaBridge.ConfigurationDatabase). The interface (ICentralAuditWriter)
|
|
// is intentionally distinct from IAuditWriter so site composition roots
|
|
// do not accidentally bind it; central composition roots that include
|
|
// AddConfigurationDatabase get a working implementation transparently.
|
|
// Wire the canonical IAuditRedactor into the factory so
|
|
// NotificationOutboxActor + Inbound API rows are truncated + redacted
|
|
// before they hit MS SQL.
|
|
// Also wire the ICentralAuditWriteFailureCounter
|
|
// so swallowed repo throws bump the central health counter.
|
|
services.AddSingleton<ICentralAuditWriter>(sp => new CentralAuditWriter(
|
|
sp,
|
|
sp.GetRequiredService<ILogger<CentralAuditWriter>>(),
|
|
sp.GetRequiredService<IAuditRedactor>(),
|
|
sp.GetRequiredService<ICentralAuditWriteFailureCounter>(),
|
|
// SourceNode-stamping: wire the local node identity so
|
|
// central-origin rows (Notification Outbox dispatch, Inbound API)
|
|
// carry the writing node's identifier when the caller hasn't
|
|
// already supplied one. GetRequiredService — the production
|
|
// composition root in SiteServiceRegistration registers the
|
|
// provider as a singleton on both site and central paths.
|
|
sp.GetRequiredService<INodeIdentityProvider>()));
|
|
|
|
// KPI History & Trends: the AuditLog source the central KPI
|
|
// recorder enumerates each sampling pass to snapshot Global-scope Audit
|
|
// Log KPIs (volume / error rate / backlog) into the KpiSample history
|
|
// table. Scoped to match its IAuditLogRepository dependency — a SCOPED
|
|
// EF Core service; the recorder opens a fresh scope per sampling pass.
|
|
// TryAdd-Enumerable so multiple sources can register the same interface
|
|
// (NotificationOutbox + SiteCallAudit + SiteHealth add their own) without
|
|
// any one composition root clobbering another, and re-registration is a
|
|
// no-op if AddAuditLog were ever called twice.
|
|
services.TryAddEnumerable(
|
|
ServiceDescriptor.Scoped<IKpiSampleSource, AuditLogKpiSampleSource>());
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Swap the default
|
|
/// <see cref="NoOpAuditWriteFailureCounter"/> and
|
|
/// <see cref="NoOpAuditRedactionFailureCounter"/> registrations for the
|
|
/// real <see cref="HealthMetricsAuditWriteFailureCounter"/> /
|
|
/// <see cref="HealthMetricsAuditRedactionFailureCounter"/> bridges so the
|
|
/// FallbackAuditWriter primary-failure counter AND the
|
|
/// <see cref="ScadaBridgeAuditRedactor"/> redactor-failure counter both surface in the
|
|
/// site health report payload as
|
|
/// <c>SiteHealthReport.SiteAuditWriteFailures</c> +
|
|
/// <c>SiteHealthReport.AuditRedactionFailure</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Must be called AFTER both <see cref="AddAuditLog"/> (registers the
|
|
/// NoOp defaults this method replaces) and
|
|
/// <c>ZB.MOM.WW.ScadaBridge.HealthMonitoring.ServiceCollectionExtensions.AddHealthMonitoring</c>
|
|
/// or <c>AddSiteHealthMonitoring</c> (registers the
|
|
/// <see cref="ISiteHealthCollector"/> the bridges depend on). Resolving
|
|
/// <see cref="IAuditWriteFailureCounter"/> or
|
|
/// <see cref="IAuditRedactionFailureCounter"/> without the latter throws
|
|
/// <see cref="InvalidOperationException"/> at <c>GetRequiredService</c>
|
|
/// time — by design, since a silent NoOp would mask a misconfiguration.
|
|
/// </para>
|
|
/// <para>
|
|
/// Idempotent — a sentinel check on the
|
|
/// <see cref="SiteAuditBacklogReporter"/> hosted-service descriptor
|
|
/// short-circuits subsequent calls so the hosted service is not
|
|
/// double-registered (AddHostedService has no TryAdd variant).
|
|
/// </para>
|
|
/// <para>
|
|
/// Site-side only: the central composition root keeps the NoOp
|
|
/// defaults; the central health-metric surface that would expose
|
|
/// <c>AuditRedactionFailure</c> next to the existing central counters
|
|
/// ships later.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="services">The service collection to register into.</param>
|
|
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
|
public static IServiceCollection AddAuditLogHealthMetricsBridge(this IServiceCollection services)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
|
|
// Guard against double-registration. AddHostedService is
|
|
// additive (no TryAdd variant) so a second call without this sentinel
|
|
// would spin up a second SiteAuditBacklogReporter, doubling the 30 s
|
|
// SQL probe rate and racing on the ISiteHealthCollector snapshot. The
|
|
// SiteAuditBacklogReporter descriptor is the discriminator: it's only
|
|
// registered by this helper, so its presence proves the bridge has
|
|
// already been wired.
|
|
if (services.Any(d => d.ImplementationType == typeof(SiteAuditBacklogReporter)))
|
|
{
|
|
return services;
|
|
}
|
|
|
|
services.Replace(
|
|
ServiceDescriptor.Singleton<IAuditWriteFailureCounter, HealthMetricsAuditWriteFailureCounter>());
|
|
services.Replace(
|
|
ServiceDescriptor.Singleton<IAuditRedactionFailureCounter, HealthMetricsAuditRedactionFailureCounter>());
|
|
// The site-side backlog reporter polls the
|
|
// SqliteAuditWriter every 30 s and pushes the snapshot into the
|
|
// collector so the next SiteHealthReport carries a fresh
|
|
// SiteAuditBacklog field. Registered alongside the other site-only
|
|
// metric bridges so AddAuditLog (which runs on central too) stays
|
|
// free of hosted-service registrations that would resolve a missing
|
|
// ISiteHealthCollector on central.
|
|
services.AddHostedService<SiteAuditBacklogReporter>();
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Central-only registration for the
|
|
/// <see cref="AuditLogPartitionMaintenanceService"/> hosted service plus
|
|
/// its <see cref="AuditLogPartitionMaintenanceOptions"/> binding. Must be
|
|
/// called from the Central role's composition root (not from a site
|
|
/// composition root); the underlying <c>IPartitionMaintenance</c>
|
|
/// implementation is registered by <c>AddConfigurationDatabase</c> and
|
|
/// only exists on the central node.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Separated from <see cref="AddAuditLog"/> because <c>AddAuditLog</c> is
|
|
/// also invoked from site composition roots — silently starting a
|
|
/// hosted service that resolves an unregistered dependency on a site
|
|
/// would fail every tick. Keeping the central-only registration in its
|
|
/// own helper preserves the "every <c>Add*</c> call is safe to issue
|
|
/// from any composition root" invariant.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="services">The service collection to register into.</param>
|
|
/// <param name="config">Application configuration used to bind partition maintenance options.</param>
|
|
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
|
public static IServiceCollection AddAuditLogCentralMaintenance(
|
|
this IServiceCollection services,
|
|
IConfiguration config)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
ArgumentNullException.ThrowIfNull(config);
|
|
|
|
services.AddOptions<AuditLogPartitionMaintenanceOptions>()
|
|
.Bind(config.GetSection(PartitionMaintenanceSectionName));
|
|
services.AddHostedService<AuditLogPartitionMaintenanceService>();
|
|
|
|
// I1 (review): bind the two central-singleton options HERE rather than in
|
|
// AddAuditLogCentralReconciliationClient. AkkaHostedService.RegisterCentralActors
|
|
// resolves IOptions<AuditLogPurgeOptions> / <SiteAuditReconciliationOptions>
|
|
// via GetRequiredService when it wires the AuditLogPurgeActor +
|
|
// SiteAuditReconciliationActor singletons; AddAuditLogCentralMaintenance is
|
|
// ALWAYS called on the central path (the reconciliation-client helper is the
|
|
// one that could in principle be dropped), so binding the options here means
|
|
// the singletons get a valid IOptions even if the gRPC-client helper is not
|
|
// wired — instead of a cryptic InvalidOperationException at GetRequiredService.
|
|
// Defaults are fine when the section is absent (24 h purge cadence /
|
|
// 5 min reconciliation tick); production exposes IntervalHours /
|
|
// ReconciliationIntervalSeconds only — the test-only *Override knobs are
|
|
// not intended to be set from config (see the options classes' remarks).
|
|
services.AddOptions<AuditLogPurgeOptions>()
|
|
.Bind(config.GetSection(PurgeSectionName));
|
|
services.AddOptions<SiteAuditReconciliationOptions>()
|
|
.Bind(config.GetSection(ReconciliationSectionName));
|
|
|
|
// Central health snapshot — a single object
|
|
// that owns the CentralAuditWriteFailures + AuditRedactionFailure
|
|
// Interlocked counters AND surfaces them on
|
|
// IAuditCentralHealthSnapshot. The same instance is bound to BOTH
|
|
// writer-side interfaces (ICentralAuditWriteFailureCounter +
|
|
// IAuditRedactionFailureCounter) so every central-side increment
|
|
// routes into the shared counters; site nodes keep their existing
|
|
// Site bridges (registered by AddAuditLogHealthMetricsBridge) so
|
|
// the same counter type does not shadow the site-side metric.
|
|
// The snapshot itself has no actor-system dependency — the
|
|
// per-site stalled latch is fed by SiteAuditTelemetryStalledTracker
|
|
// which the Akka bootstrap wires up after ActorSystem.Create returns
|
|
// (the tracker is NOT registered here because its construction
|
|
// requires ActorSystem, which is not a DI-resolvable singleton).
|
|
services.AddSingleton<AuditCentralHealthSnapshot>();
|
|
services.AddSingleton<IAuditCentralHealthSnapshot>(
|
|
sp => sp.GetRequiredService<AuditCentralHealthSnapshot>());
|
|
services.Replace(ServiceDescriptor.Singleton<ICentralAuditWriteFailureCounter>(
|
|
sp => sp.GetRequiredService<AuditCentralHealthSnapshot>()));
|
|
// Override the NoOp IAuditRedactionFailureCounter
|
|
// (registered by AddAuditLog) with the CentralAuditRedactionFailureCounter
|
|
// bridge so payload-filter throws on CentralAuditWriter /
|
|
// AuditLogIngestActor paths surface on the central dashboard. The
|
|
// bridge is a thin wrapper around the AuditCentralHealthSnapshot
|
|
// singleton so all central redactor failures route into the same
|
|
// counter as CentralAuditWriteFailures. The site composition root
|
|
// overrides this binding AGAIN via AddAuditLogHealthMetricsBridge —
|
|
// central nodes do not call that bridge, so this is the final
|
|
// binding on a central host. Mirrors the
|
|
// HealthMetricsAuditRedactionFailureCounter shape one-for-one.
|
|
services.Replace(ServiceDescriptor.Singleton<IAuditRedactionFailureCounter,
|
|
CentralAuditRedactionFailureCounter>());
|
|
// Replace the NoOp IAuditInboundCeilingHitsCounter with the
|
|
// AuditCentralHealthSnapshot so ceiling-hit counts surface on the
|
|
// central dashboard. Same singleton-forward pattern as
|
|
// ICentralAuditWriteFailureCounter above.
|
|
services.Replace(ServiceDescriptor.Singleton<IAuditInboundCeilingHitsCounter>(
|
|
sp => sp.GetRequiredService<AuditCentralHealthSnapshot>()));
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Audit Log — central-only registration of the production
|
|
/// <see cref="IPullAuditEventsClient"/> (<see cref="GrpcPullAuditEventsClient"/>)
|
|
/// and its unary-call invoker (<see cref="GrpcPullAuditEventsInvoker"/>) used
|
|
/// by <see cref="SiteAuditReconciliationActor"/> to pull reconciliation
|
|
/// batches from each site over the <c>PullAuditEvents</c> gRPC RPC.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Kept out of <see cref="AddAuditLog"/> — which also runs on site
|
|
/// composition roots — because the client dials sites and resolves
|
|
/// <see cref="ISiteEnumerator"/> (a central-only collaborator wired
|
|
/// alongside the reconciliation singleton). Folding it into
|
|
/// <see cref="AddAuditLog"/> would register a site-dialing client on every
|
|
/// site host, violating the "every <c>Add*</c> call is safe from any
|
|
/// composition root" invariant. This helper is the central analogue of
|
|
/// <see cref="AddAuditLogCentralMaintenance"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// The <see cref="GrpcPullAuditEventsInvoker"/> binds with default
|
|
/// <see cref="ZB.MOM.WW.ScadaBridge.Communication.CommunicationOptions"/>
|
|
/// keepalive unless an <c>IOptions<CommunicationOptions></c> is
|
|
/// already registered, in which case the configured timings flow through —
|
|
/// matching how <c>SiteStreamGrpcClientFactory</c> takes its keepalive from
|
|
/// the same options.
|
|
/// </para>
|
|
/// <para>
|
|
/// The production <see cref="ISiteEnumerator"/> (<see cref="SiteEnumerator"/>,
|
|
/// wrapping the scoped <c>ISiteRepository</c>) IS registered here — so the
|
|
/// <see cref="SiteAuditReconciliationActor"/> singleton wired in the Host can
|
|
/// resolve its enumerator + gRPC client from this central-only helper. Keeping
|
|
/// the enumerator on this central path preserves the "every <c>Add*</c> call is
|
|
/// safe from any composition root" invariant: a site host never calls this
|
|
/// helper, so it never registers a site-dialing enumerator. The
|
|
/// <see cref="AuditLogPurgeOptions"/> + <see cref="SiteAuditReconciliationOptions"/>
|
|
/// bindings live in <see cref="AddAuditLogCentralMaintenance"/> instead (I1
|
|
/// review fix) — that helper is unconditionally called on the central path, so
|
|
/// the two maintenance singletons get a valid <c>IOptions</c> even if this
|
|
/// gRPC-client helper is ever dropped.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="services">The service collection to register into.</param>
|
|
/// <param name="config">Application configuration used to bind the gRPC client's communication options (purge + reconciliation options are bound by <see cref="AddAuditLogCentralMaintenance"/>).</param>
|
|
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
|
public static IServiceCollection AddAuditLogCentralReconciliationClient(
|
|
this IServiceCollection services,
|
|
IConfiguration config)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
ArgumentNullException.ThrowIfNull(config);
|
|
|
|
// Production ISiteEnumerator: projects the config-DB Site rows into the
|
|
// reconciliation targets the SiteAuditReconciliationActor polls. Scoped
|
|
// ISiteRepository is resolved per call inside the enumerator, so the
|
|
// singleton takes the ROOT provider (mirrors the per-tick scope pattern
|
|
// in SiteAuditReconciliationActor / AuditLogPurgeActor).
|
|
services.TryAddSingleton<ISiteEnumerator>(sp => new SiteEnumerator(sp));
|
|
|
|
// I1 (review): the AuditLogPurgeOptions / SiteAuditReconciliationOptions
|
|
// bindings moved to AddAuditLogCentralMaintenance — that helper is always
|
|
// called on the central path, so the two maintenance singletons resolve a
|
|
// valid IOptions even if this gRPC-client helper is ever dropped. Keep the
|
|
// ISiteEnumerator + gRPC client registrations here (they dial sites and are
|
|
// central-only by design).
|
|
|
|
// The invoker owns the per-endpoint GrpcChannel cache, so it must be a
|
|
// singleton — a fresh invoker per resolution would leak channels.
|
|
// Resolve CommunicationOptions if present (the central Host binds it),
|
|
// otherwise fall back to defaults so this helper stays standalone.
|
|
services.TryAddSingleton<GrpcPullAuditEventsInvoker>(sp =>
|
|
{
|
|
var options = sp
|
|
.GetService<Microsoft.Extensions.Options.IOptions<
|
|
ZB.MOM.WW.ScadaBridge.Communication.CommunicationOptions>>();
|
|
return options is null
|
|
? new GrpcPullAuditEventsInvoker()
|
|
: new GrpcPullAuditEventsInvoker(options.Value);
|
|
});
|
|
services.TryAddSingleton<GrpcPullAuditEventsClient.IPullAuditEventsInvoker>(
|
|
sp => sp.GetRequiredService<GrpcPullAuditEventsInvoker>());
|
|
|
|
services.TryAddSingleton<IPullAuditEventsClient>(sp => new GrpcPullAuditEventsClient(
|
|
sp.GetRequiredService<ISiteEnumerator>(),
|
|
sp.GetRequiredService<GrpcPullAuditEventsClient.IPullAuditEventsInvoker>(),
|
|
sp.GetRequiredService<ILogger<GrpcPullAuditEventsClient>>()));
|
|
|
|
// Site Call Audit reconciliation pull client — central-only, the
|
|
// sibling of the audit pull client above. Lives here (not in the
|
|
// SiteCallAudit project) so it can reuse the central-only
|
|
// ISiteEnumerator registered just above; SiteCallAudit does not
|
|
// reference AuditLog. The invoker owns the per-endpoint GrpcChannel
|
|
// cache, so it must be a singleton (a fresh invoker per resolution
|
|
// would leak channels). CommunicationOptions flow through when bound by
|
|
// the central Host, else defaults — mirrors the audit invoker.
|
|
services.TryAddSingleton<GrpcPullSiteCallsInvoker>(sp =>
|
|
{
|
|
var options = sp
|
|
.GetService<Microsoft.Extensions.Options.IOptions<
|
|
ZB.MOM.WW.ScadaBridge.Communication.CommunicationOptions>>();
|
|
return options is null
|
|
? new GrpcPullSiteCallsInvoker()
|
|
: new GrpcPullSiteCallsInvoker(options.Value);
|
|
});
|
|
services.TryAddSingleton<GrpcPullSiteCallsClient.IPullSiteCallsInvoker>(
|
|
sp => sp.GetRequiredService<GrpcPullSiteCallsInvoker>());
|
|
|
|
services.TryAddSingleton<IPullSiteCallsClient>(sp => new GrpcPullSiteCallsClient(
|
|
sp.GetRequiredService<ISiteEnumerator>(),
|
|
sp.GetRequiredService<GrpcPullSiteCallsClient.IPullSiteCallsInvoker>(),
|
|
sp.GetRequiredService<ILogger<GrpcPullSiteCallsClient>>()));
|
|
|
|
return services;
|
|
}
|
|
}
|