merge(deferred-10): aggregated live alarm stream for Alarm Summary

Merges plan #10 (feat/live-alarm-stream) into main alongside plan #22.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 13:01:04 -04:00
36 changed files with 3327 additions and 53 deletions
@@ -47,11 +47,23 @@ public static class ScadaBridgeTelemetry
Meter.CreateCounter<long>("scadabridge.store_and_forward.replication.failures", unit: "1",
description: "S&F buffer replication operations that failed to dispatch/deliver to the peer node");
/// <summary>
/// Incremented each time a per-site live-alarm aggregator re-establishes its site-wide
/// gRPC stream — a NodeA↔NodeB failover flip or a reconcile-driven reopen after the
/// stream was given up (plan #10, Task 6). A sustained climb signals a flapping site link.
/// </summary>
private static readonly Counter<long> _liveAlarmStreamReconnects =
Meter.CreateCounter<long>("scadabridge.site.alarm_cache.reconnects", unit: "1",
description: "Live-alarm aggregator site-wide gRPC stream reconnects (NodeA↔NodeB flip or reconcile-driven reopen).");
// ---------------- Observable gauges ----------------
/// <summary>Current count of open site connections, mutated via <see cref="Interlocked"/>.</summary>
private static long _siteConnectionsUp;
/// <summary>Current count of running per-site live-alarm aggregators, mutated via <see cref="Interlocked"/>.</summary>
private static long _liveAlarmAggregatorsActive;
/// <summary>Provider that yields the live StoreAndForward queue depth; set by a later task.</summary>
private static Func<long>? _queueDepthProvider;
@@ -68,6 +80,13 @@ public static class ScadaBridgeTelemetry
() => Volatile.Read(ref _queueDepthProvider) is { } p ? p() : 0L,
unit: "items",
description: "Current StoreAndForward queue depth.");
/// <summary>Gauge reporting the number of currently running per-site live-alarm aggregators.</summary>
private static readonly ObservableGauge<long> _liveAlarmAggregatorsActiveGauge =
Meter.CreateObservableGauge<long>("scadabridge.site.alarm_cache.aggregators.active",
() => Interlocked.Read(ref _liveAlarmAggregatorsActive),
unit: "1",
description: "Number of per-site live-alarm aggregators currently running on the active central node.");
#pragma warning restore IDE0052
// ---------------- Emit helpers ----------------
@@ -94,6 +113,15 @@ public static class ScadaBridgeTelemetry
/// <summary>Records that a site connection closed (decrements the up-count gauge).</summary>
public static void SiteConnectionClosed() => Interlocked.Decrement(ref _siteConnectionsUp);
/// <summary>Records that a per-site live-alarm aggregator started (increments the active-aggregator gauge).</summary>
public static void LiveAlarmAggregatorStarted() => Interlocked.Increment(ref _liveAlarmAggregatorsActive);
/// <summary>Records that a per-site live-alarm aggregator stopped (decrements the active-aggregator gauge).</summary>
public static void LiveAlarmAggregatorStopped() => Interlocked.Decrement(ref _liveAlarmAggregatorsActive);
/// <summary>Records that a per-site live-alarm aggregator re-established its site-wide gRPC stream.</summary>
public static void RecordLiveAlarmStreamReconnect() => _liveAlarmStreamReconnects.Add(1);
/// <summary>
/// Registers the provider the StoreAndForward queue-depth gauge reads on each observation.
/// A later task supplies a provider that reads the real StoreAndForward depth. A null