using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ScadaLink.AuditLog.Central; using ScadaLink.AuditLog.Configuration; using ScadaLink.AuditLog.Site; using ScadaLink.AuditLog.Site.Telemetry; using ScadaLink.Commons.Interfaces.Services; namespace ScadaLink.AuditLog; /// /// Composition root for the Audit Log (#23) component. /// /// /// /// M1 registered + the validator. M2 Bundle E /// extends the surface with the site-side writer chain /// ( + + /// ) and the telemetry collaborators /// (, , /// , , /// ). /// /// /// Audit Log (#23) sits alongside Notification Outbox (#21) and Site Call /// Audit (#22). IAuditLogRepository is registered by /// ScadaLink.ConfigurationDatabase.ServiceCollectionExtensions.AddConfigurationDatabase, /// so the caller (the Host on the central node) must also call that. /// /// public static class ServiceCollectionExtensions { /// Configuration section bound to . public const string ConfigSectionName = "AuditLog"; /// Configuration section bound to . public const string SiteWriterSectionName = "AuditLog:SiteWriter"; /// Configuration section bound to . public const string SiteTelemetrySectionName = "AuditLog:SiteTelemetry"; /// /// Registers the Audit Log (#23) 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 . /// public static IServiceCollection AddAuditLog(this IServiceCollection services, IConfiguration config) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(config); // M1: top-level AuditLogOptions + validator (redaction policy, payload caps, etc.). services.AddOptions() .Bind(config.GetSection(ConfigSectionName)) .ValidateOnStart(); services.AddSingleton, AuditLogOptionsValidator>(); // M2 Bundle E: 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() .Bind(config.GetSection(SiteWriterSectionName)); services.AddOptions() .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(); services.AddSingleton(sp => sp.GetRequiredService()); // RingBufferFallback: drop-oldest in-memory ring used by // FallbackAuditWriter when the primary SQLite writer throws. Default // capacity is fine for M2 (1024). services.AddSingleton(); // IAuditWriteFailureCounter: NoOp default. Bundle G overrides this // binding with the real Site Health Monitoring counter. Registered // before FallbackAuditWriter so the factory can resolve it. services.AddSingleton(); // The script-thread surface is FallbackAuditWriter (primary + ring + // counter), not the raw SqliteAuditWriter — primary failures must NEVER // abort the user-facing action. services.AddSingleton(sp => new FallbackAuditWriter( primary: sp.GetRequiredService(), ring: sp.GetRequiredService(), failureCounter: sp.GetRequiredService(), logger: sp.GetRequiredService>())); // ISiteStreamAuditClient: NoOp default. M6's reconciliation work brings // the real gRPC-backed implementation (no site→central gRPC channel // exists today — sites talk to central via Akka ClusterClient only). // Bundle H's integration test substitutes a stub directly into the // SiteAuditTelemetryActor's Props.Create call. services.AddSingleton(); // M3 Bundle F: 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 M2 // chain above); the operational tracking store is SITE-ONLY (registered // by ScadaLink.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 in M2. services.AddSingleton(sp => new CachedCallTelemetryForwarder( sp.GetRequiredService(), sp.GetService(), sp.GetRequiredService>())); // M3 Bundle F: 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 (Bundle F StoreAndForward wiring change). services.AddSingleton(); services.AddSingleton( sp => sp.GetRequiredService()); // M4 Bundle B: central direct-write audit writer used by // NotificationOutboxActor (Bundle B) and Inbound API (Bundle C/D) 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 // ScadaLink.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. services.AddSingleton(); return services; } /// /// Audit Log (#23) M2 Bundle G — swap the default /// registration for the real /// bridge so the /// FallbackAuditWriter primary-failure counter surfaces in the site health /// report payload as SiteHealthReport.SiteAuditWriteFailures. /// /// /// /// Must be called AFTER both (registers the /// NoOp default this method replaces) and /// ScadaLink.HealthMonitoring.ServiceCollectionExtensions.AddHealthMonitoring /// or AddSiteHealthMonitoring (registers the /// the bridge depends on). Resolving /// without the latter throws /// at GetRequiredService /// time — by design, since a silent NoOp would mask a misconfiguration. /// /// /// Idempotent — calling twice replaces the descriptor each time without /// piling up registrations. /// /// public static IServiceCollection AddAuditLogHealthMetricsBridge(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); services.Replace( ServiceDescriptor.Singleton()); return services; } }