using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Kpi; /// /// Site Health for the "KPI History & /// Trends" backbone. Unlike the other sample sources — which each query a /// central repository — this one reads the in-memory /// state, snapshotting the latest /// /// each tracked site has reported into per-site () /// rows. /// /// /// /// Each sampling pass the central recorder enumerates this source and calls /// , which iterates /// and, for every site /// whose is non-null, emits the /// fixed metric catalog below. Sites known only via heartbeats (null /// ) contribute nothing. /// /// /// The aggregator is registered as a singleton (its state is shared, immutable, /// and safe to hand straight to callers); this source is nonetheless registered /// DI-scoped for consistency with the recorder's per-tick scope and the other /// sample sources. A scoped consumer resolving a singleton dependency is the /// normal DI lifetime relationship and introduces no captive-dependency issue. /// /// public sealed class SiteHealthKpiSampleSource : IKpiSampleSource { // ── Metric catalog (the agreed metric-name strings for this source) ── // Declaration order matches the emission order in AddSiteSnapshot. Charted metrics // share the public Commons catalog so source + UI trend page key off one symbol; // the uncharted internal metrics stay private here. private const string MetricConnectionsUp = "connectionsUp"; private const string MetricConnectionsDown = KpiMetrics.SiteHealth.ConnectionsDown; private const string MetricScriptErrors = KpiMetrics.SiteHealth.ScriptErrors; private const string MetricAlarmEvalErrors = KpiMetrics.SiteHealth.AlarmEvalErrors; private const string MetricSfBufferDepth = KpiMetrics.SiteHealth.SfBufferDepth; private const string MetricDeadLetters = KpiMetrics.SiteHealth.DeadLetters; private const string MetricParkedMessages = "parkedMessages"; private const string MetricDeployedInstances = "deployedInstances"; private const string MetricEnabledInstances = "enabledInstances"; private const string MetricDisabledInstances = "disabledInstances"; private const string MetricAuditBacklogPending = "auditBacklogPending"; private const string MetricEventLogWriteFailures = KpiMetrics.SiteHealth.EventLogWriteFailures; private readonly ICentralHealthAggregator _aggregator; /// /// Creates the sample source over the central in-memory health aggregator. /// /// The central health aggregator holding per-site state. public SiteHealthKpiSampleSource(ICentralHealthAggregator aggregator) { ArgumentNullException.ThrowIfNull(aggregator); _aggregator = aggregator; } /// public string Source => KpiSources.SiteHealth; /// public Task> CollectAsync( DateTime capturedAtUtc, CancellationToken cancellationToken = default) { var samples = new List(); foreach (var (siteId, state) in _aggregator.GetAllSiteStates()) { // Sites known only via heartbeats (no report yet) contribute nothing. if (state.LatestReport is not { } report) { continue; } AddSiteSnapshot(samples, capturedAtUtc, siteId, report); } return Task.FromResult>(samples); } /// /// Appends the per-site metric catalog for one site's latest report, /// mapping each metric to the corresponding /// /// field. /// private static void AddSiteSnapshot( List samples, DateTime capturedAtUtc, string siteId, ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport report) { // Connection statuses: Connected counts as up, everything else // (Disconnected / Connecting / Error) counts as down. var connectionsUp = 0; var connectionsDown = 0; foreach (var status in report.DataConnectionStatuses.Values) { if (status == ConnectionHealth.Connected) { connectionsUp++; } else { connectionsDown++; } } // Sum of the store-and-forward buffer depths across all buffers. long sfBufferDepth = 0; foreach (var depth in report.StoreAndForwardBufferDepths.Values) { sfBufferDepth += depth; } samples.Add(Sample(capturedAtUtc, MetricConnectionsUp, siteId, connectionsUp)); samples.Add(Sample(capturedAtUtc, MetricConnectionsDown, siteId, connectionsDown)); samples.Add(Sample(capturedAtUtc, MetricScriptErrors, siteId, report.ScriptErrorCount)); samples.Add(Sample(capturedAtUtc, MetricAlarmEvalErrors, siteId, report.AlarmEvaluationErrorCount)); samples.Add(Sample(capturedAtUtc, MetricSfBufferDepth, siteId, sfBufferDepth)); samples.Add(Sample(capturedAtUtc, MetricDeadLetters, siteId, report.DeadLetterCount)); samples.Add(Sample(capturedAtUtc, MetricParkedMessages, siteId, report.ParkedMessageCount)); samples.Add(Sample(capturedAtUtc, MetricDeployedInstances, siteId, report.DeployedInstanceCount)); samples.Add(Sample(capturedAtUtc, MetricEnabledInstances, siteId, report.EnabledInstanceCount)); samples.Add(Sample(capturedAtUtc, MetricDisabledInstances, siteId, report.DisabledInstanceCount)); // Nested audit-backlog object may be null (no data yet) — treat as 0. samples.Add(Sample( capturedAtUtc, MetricAuditBacklogPending, siteId, report.SiteAuditBacklog?.PendingCount ?? 0)); samples.Add(Sample( capturedAtUtc, MetricEventLogWriteFailures, siteId, report.SiteEventLogWriteFailures)); } private static KpiSample Sample( DateTime capturedAtUtc, string metric, string scopeKey, double value) => new() { Source = KpiSources.SiteHealth, Metric = metric, Scope = KpiScopes.Site, ScopeKey = scopeKey, Value = value, CapturedAtUtc = capturedAtUtc, }; }