From cd93ad39766e4fb03baeacf70ddde0e0a83cf958 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 10:15:40 -0400 Subject: [PATCH] =?UTF-8?q?fix(kpi):=20promote=20catalog=20metric=20litera?= =?UTF-8?q?ls=20to=20shared=20KpiMetrics=20constants=20=E2=80=94=20drift?= =?UTF-8?q?=20becomes=20a=20compile=20error=20(plan=20R2-04=20T8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Types/Kpi/KpiMetricAggregation.cs | 20 +++++-------- .../Types/Kpi/KpiMetrics.cs | 30 +++++++++++++++---- .../Kpi/SiteHealthKpiSampleSource.cs | 4 +-- .../Kpi/SiteCallAuditKpiSampleSource.cs | 2 +- .../Kpi/KpiMetricAggregationTests.cs | 18 +++++++++++ 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetricAggregation.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetricAggregation.cs index df661750..a463a875 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetricAggregation.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetricAggregation.cs @@ -42,10 +42,10 @@ public enum KpiRollupAggregation /// Keying by (source, metric) — not metric alone — is deliberate: /// deliveredLastInterval is emitted by both /// and , -/// so a metric name is not globally unique. Every metric string is taken from the -/// emitting source verbatim (shared constants where they exist, -/// otherwise the source's own private literal), so this catalog stays a faithful mirror -/// of what actually lands in KpiSample.Metric. +/// so a metric name is not globally unique. Every catalogued metric string is now a shared +/// constant — the emitting source consumes the same constant — so an +/// emitter rename is a compile error, not a silent Gauge downgrade (R4), and +/// this catalog stays a faithful mirror of what actually lands in KpiSample.Metric. /// /// /// The catalog stores only the pairs; anything @@ -57,12 +57,6 @@ public enum KpiRollupAggregation /// public static class KpiMetricAggregationCatalog { - // Metric literals for the sources that keep their names private (no shared - // KpiMetrics constant exists for these). Charted metrics use the KpiMetrics catalog. - private const string SiteCallAuditDeliveredLastInterval = "deliveredLastInterval"; - private const string SiteHealthAlarmEvalErrors = "alarmEvalErrors"; - private const string SiteHealthEventLogWriteFailures = "eventLogWriteFailures"; - /// /// The (source, metric) pairs classified as /// (sum-per-hour). Every other emitted metric is a . @@ -99,11 +93,11 @@ public static class KpiMetricAggregationCatalog { (KpiSources.NotificationOutbox, KpiMetrics.NotificationOutbox.DeliveredLastInterval), (KpiSources.SiteCallAudit, KpiMetrics.SiteCallAudit.FailedLastInterval), - (KpiSources.SiteCallAudit, SiteCallAuditDeliveredLastInterval), + (KpiSources.SiteCallAudit, KpiMetrics.SiteCallAudit.DeliveredLastInterval), (KpiSources.SiteHealth, KpiMetrics.SiteHealth.ScriptErrors), - (KpiSources.SiteHealth, SiteHealthAlarmEvalErrors), + (KpiSources.SiteHealth, KpiMetrics.SiteHealth.AlarmEvalErrors), (KpiSources.SiteHealth, KpiMetrics.SiteHealth.DeadLetters), - (KpiSources.SiteHealth, SiteHealthEventLogWriteFailures), + (KpiSources.SiteHealth, KpiMetrics.SiteHealth.EventLogWriteFailures), }; /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetrics.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetrics.cs index d2230812..ffedf771 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetrics.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetrics.cs @@ -18,11 +18,16 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; /// not a rename — changing any value would orphan historical samples. /// /// -/// Only the metrics a trend page actually charts are catalogued here. Sources may emit -/// additional internal metrics (e.g. the Notification Outbox stuckCount / -/// oldestPendingAgeSeconds, the Site Call Audit deliveredLastInterval / -/// stuck / oldestPendingAgeSeconds, and the bulk of the Site Health catalog) -/// that no page references; those stay private to their source until a page charts them. +/// This catalog now carries two kinds of metric: those a trend page charts, and +/// those the classifies for the hourly rollup fold +/// (e.g. SiteCallAudit/deliveredLastInterval, SiteHealth/alarmEvalErrors, +/// SiteHealth/eventLogWriteFailures) even if no page charts them yet. The C4/R4 lesson +/// is that any metric named in two places needs one shared symbol: a private +/// literal on the emitter plus a matching private literal in the catalog drifts silently on a +/// rename. Sources may still keep genuinely single-use internal metrics (e.g. the Notification +/// Outbox stuckCount / oldestPendingAgeSeconds, the Site Call Audit stuck / +/// oldestPendingAgeSeconds, and the bulk of the Site Health catalog) private until a page +/// or the catalog references them. /// Constants are grouped by source via nested static classes mirroring /// . /// @@ -65,6 +70,11 @@ public static class KpiMetrics /// Cached calls that failed permanently in the last KPI interval. public const string FailedLastInterval = "failedLastInterval"; + + /// Cached calls delivered in the last KPI interval. Not charted by a trend + /// page today, but named in the rollup aggregation catalog as a Rate metric — so it + /// is a shared symbol to keep the emitter and catalog in lock-step (R4). + public const string DeliveredLastInterval = "deliveredLastInterval"; } /// @@ -98,6 +108,16 @@ public static class KpiMetrics /// Script-execution error count reported by the site. public const string ScriptErrors = "scriptErrors"; + /// Alarm-evaluation error count reported by the site. Named in the rollup + /// aggregation catalog as a Rate metric — shared symbol so an emitter rename is a + /// compile error, not a silent Gauge downgrade (R4). + public const string AlarmEvalErrors = "alarmEvalErrors"; + + /// Event-log write-failure count reported by the site. Named in the rollup + /// aggregation catalog as a Rate metric — shared symbol so an emitter rename is a + /// compile error, not a silent Gauge downgrade (R4). + public const string EventLogWriteFailures = "eventLogWriteFailures"; + /// Summed store-and-forward buffer depth across all buffers. public const string SfBufferDepth = "sfBufferDepth"; } diff --git a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/Kpi/SiteHealthKpiSampleSource.cs b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/Kpi/SiteHealthKpiSampleSource.cs index 1c2701ca..501cb967 100644 --- a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/Kpi/SiteHealthKpiSampleSource.cs +++ b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/Kpi/SiteHealthKpiSampleSource.cs @@ -40,7 +40,7 @@ public sealed class SiteHealthKpiSampleSource : IKpiSampleSource private const string MetricConnectionsUp = "connectionsUp"; private const string MetricConnectionsDown = KpiMetrics.SiteHealth.ConnectionsDown; private const string MetricScriptErrors = KpiMetrics.SiteHealth.ScriptErrors; - private const string MetricAlarmEvalErrors = "alarmEvalErrors"; + 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"; @@ -48,7 +48,7 @@ public sealed class SiteHealthKpiSampleSource : IKpiSampleSource private const string MetricEnabledInstances = "enabledInstances"; private const string MetricDisabledInstances = "disabledInstances"; private const string MetricAuditBacklogPending = "auditBacklogPending"; - private const string MetricEventLogWriteFailures = "eventLogWriteFailures"; + private const string MetricEventLogWriteFailures = KpiMetrics.SiteHealth.EventLogWriteFailures; private readonly ICentralHealthAggregator _aggregator; diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/Kpi/SiteCallAuditKpiSampleSource.cs b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/Kpi/SiteCallAuditKpiSampleSource.cs index bfab3778..f7a2d994 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/Kpi/SiteCallAuditKpiSampleSource.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/Kpi/SiteCallAuditKpiSampleSource.cs @@ -42,7 +42,7 @@ public sealed class SiteCallAuditKpiSampleSource : IKpiSampleSource private const string MetricBuffered = KpiMetrics.SiteCallAudit.Buffered; private const string MetricParked = KpiMetrics.SiteCallAudit.Parked; private const string MetricFailedLastInterval = KpiMetrics.SiteCallAudit.FailedLastInterval; - private const string MetricDeliveredLastInterval = "deliveredLastInterval"; + private const string MetricDeliveredLastInterval = KpiMetrics.SiteCallAudit.DeliveredLastInterval; private const string MetricStuck = "stuck"; private const string MetricOldestPendingAgeSeconds = "oldestPendingAgeSeconds"; diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Kpi/KpiMetricAggregationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Kpi/KpiMetricAggregationTests.cs index 7bbae2bf..a3cb9b08 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Kpi/KpiMetricAggregationTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Kpi/KpiMetricAggregationTests.cs @@ -148,4 +148,22 @@ public class KpiMetricAggregationTests KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(KpiSources.SiteCallAudit, "deliveredLastInterval")); } + + // ---- Task 8: catalog metric literals promoted to shared KpiMetrics constants (R4) ---- + + [Fact] + public void PromotedMetricConstants_LockHistoricalPersistedValues() + { + // Persisted data contract — these are symbol promotions, NOT renames. + Assert.Equal("deliveredLastInterval", KpiMetrics.SiteCallAudit.DeliveredLastInterval); + Assert.Equal("alarmEvalErrors", KpiMetrics.SiteHealth.AlarmEvalErrors); + Assert.Equal("eventLogWriteFailures", KpiMetrics.SiteHealth.EventLogWriteFailures); + } + + [Theory] + [InlineData(KpiSources.SiteCallAudit, KpiMetrics.SiteCallAudit.DeliveredLastInterval)] + [InlineData(KpiSources.SiteHealth, KpiMetrics.SiteHealth.AlarmEvalErrors)] + [InlineData(KpiSources.SiteHealth, KpiMetrics.SiteHealth.EventLogWriteFailures)] + public void Resolve_PromotedRatePairs_StillClassifyAsRate(string source, string metric) + => Assert.Equal(KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(source, metric)); }