using System.Collections.Concurrent; using ZB.MOM.WW.ScadaBridge.AuditLog.Payload; namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central; /// /// Audit Log (#23) M6 Bundle E (T8, T9) — central singleton implementation of /// . Owns thread-safe /// counters for /// CentralAuditWriteFailures + AuditRedactionFailure and a /// per-site latched stalled-state map fed by the /// . Also implements the /// writer surfaces ( + /// ) so a single concrete object /// is the source of truth — DI binds those two interfaces to this same /// singleton instance on the central composition root. /// /// /// /// Why one type for read + write. The writer interfaces are tiny /// (Increment()) and the read surface needs visibility of those /// counters anyway — having a single class own both means the /// Interlocked field IS the snapshot value, no extra plumbing needed. /// Mirrors the /// pattern where /// the collector both receives and exposes the metric. /// /// /// Stalled-state plumbing. The per-site stalled latch lives directly /// on this snapshot. is the /// EventStream subscriber that pushes /// publications in via /// . Keeping the dictionary on this type (rather /// than reading the tracker on every access) lets the snapshot be constructed /// without an dependency — the tracker /// is wired up later from the Akka bootstrap, once the system is built. /// /// public sealed class AuditCentralHealthSnapshot : IAuditCentralHealthSnapshot, ICentralAuditWriteFailureCounter, IAuditRedactionFailureCounter, IAuditInboundCeilingHitsCounter { private int _centralAuditWriteFailures; private int _auditRedactionFailure; private int _auditInboundCeilingHits; private readonly ConcurrentDictionary _stalled = new(); /// public int CentralAuditWriteFailures => Interlocked.CompareExchange(ref _centralAuditWriteFailures, 0, 0); /// public int AuditRedactionFailure => Interlocked.CompareExchange(ref _auditRedactionFailure, 0, 0); /// public int AuditInboundCeilingHits => Interlocked.CompareExchange(ref _auditInboundCeilingHits, 0, 0); /// public IReadOnlyDictionary SiteAuditTelemetryStalled => new Dictionary(_stalled); /// /// Apply a publication /// observed by . Public /// so the tracker (which lives in the same assembly but is constructed /// later from the Akka host) can push without a friend reference; /// readers should call . /// /// The event carrying the site ID and new stalled state. public void ApplyStalled(SiteAuditTelemetryStalledChanged evt) { if (evt is null) return; _stalled[evt.SiteId] = evt.Stalled; } /// void ICentralAuditWriteFailureCounter.Increment() => Interlocked.Increment(ref _centralAuditWriteFailures); /// void IAuditRedactionFailureCounter.Increment() => Interlocked.Increment(ref _auditRedactionFailure); /// void IAuditInboundCeilingHitsCounter.Increment() => Interlocked.Increment(ref _auditInboundCeilingHits); }