fix(kpi): promote catalog metric literals to shared KpiMetrics constants — drift becomes a compile error (plan R2-04 T8)

This commit is contained in:
Joseph Doherty
2026-07-13 10:15:40 -04:00
parent 1c8b2bc745
commit cd93ad3976
5 changed files with 53 additions and 21 deletions
@@ -42,10 +42,10 @@ public enum KpiRollupAggregation
/// Keying by <c>(source, metric)</c> — not metric alone — is deliberate: /// Keying by <c>(source, metric)</c> — not metric alone — is deliberate:
/// <c>deliveredLastInterval</c> is emitted by both /// <c>deliveredLastInterval</c> is emitted by both
/// <see cref="KpiSources.NotificationOutbox"/> and <see cref="KpiSources.SiteCallAudit"/>, /// <see cref="KpiSources.NotificationOutbox"/> and <see cref="KpiSources.SiteCallAudit"/>,
/// so a metric name is not globally unique. Every metric string is taken from the /// so a metric name is not globally unique. Every catalogued metric string is now a shared
/// emitting source verbatim (shared <see cref="KpiMetrics"/> constants where they exist, /// <see cref="KpiMetrics"/> constant — the emitting source consumes the same constant — so an
/// otherwise the source's own private literal), so this catalog stays a faithful mirror /// emitter rename is a <strong>compile error</strong>, not a silent Gauge downgrade (R4), and
/// of what actually lands in <c>KpiSample.Metric</c>. /// this catalog stays a faithful mirror of what actually lands in <c>KpiSample.Metric</c>.
/// </para> /// </para>
/// <para> /// <para>
/// The catalog stores only the <see cref="KpiRollupAggregation.Rate"/> pairs; anything /// The catalog stores only the <see cref="KpiRollupAggregation.Rate"/> pairs; anything
@@ -57,12 +57,6 @@ public enum KpiRollupAggregation
/// </remarks> /// </remarks>
public static class KpiMetricAggregationCatalog 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";
/// <summary> /// <summary>
/// The <c>(source, metric)</c> pairs classified as <see cref="KpiRollupAggregation.Rate"/> /// The <c>(source, metric)</c> pairs classified as <see cref="KpiRollupAggregation.Rate"/>
/// (sum-per-hour). Every other emitted metric is a <see cref="KpiRollupAggregation.Gauge"/>. /// (sum-per-hour). Every other emitted metric is a <see cref="KpiRollupAggregation.Gauge"/>.
@@ -99,11 +93,11 @@ public static class KpiMetricAggregationCatalog
{ {
(KpiSources.NotificationOutbox, KpiMetrics.NotificationOutbox.DeliveredLastInterval), (KpiSources.NotificationOutbox, KpiMetrics.NotificationOutbox.DeliveredLastInterval),
(KpiSources.SiteCallAudit, KpiMetrics.SiteCallAudit.FailedLastInterval), (KpiSources.SiteCallAudit, KpiMetrics.SiteCallAudit.FailedLastInterval),
(KpiSources.SiteCallAudit, SiteCallAuditDeliveredLastInterval), (KpiSources.SiteCallAudit, KpiMetrics.SiteCallAudit.DeliveredLastInterval),
(KpiSources.SiteHealth, KpiMetrics.SiteHealth.ScriptErrors), (KpiSources.SiteHealth, KpiMetrics.SiteHealth.ScriptErrors),
(KpiSources.SiteHealth, SiteHealthAlarmEvalErrors), (KpiSources.SiteHealth, KpiMetrics.SiteHealth.AlarmEvalErrors),
(KpiSources.SiteHealth, KpiMetrics.SiteHealth.DeadLetters), (KpiSources.SiteHealth, KpiMetrics.SiteHealth.DeadLetters),
(KpiSources.SiteHealth, SiteHealthEventLogWriteFailures), (KpiSources.SiteHealth, KpiMetrics.SiteHealth.EventLogWriteFailures),
}; };
/// <summary> /// <summary>
@@ -18,11 +18,16 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
/// <strong>not</strong> a rename — changing any value would orphan historical samples. /// <strong>not</strong> a rename — changing any value would orphan historical samples.
/// </para> /// </para>
/// <para> /// <para>
/// Only the metrics a trend page actually charts are catalogued here. Sources may emit /// This catalog now carries two kinds of metric: those a trend page charts, <em>and</em>
/// additional internal metrics (e.g. the Notification Outbox <c>stuckCount</c> / /// those the <see cref="KpiMetricAggregationCatalog"/> classifies for the hourly rollup fold
/// <c>oldestPendingAgeSeconds</c>, the Site Call Audit <c>deliveredLastInterval</c> / /// (e.g. <c>SiteCallAudit/deliveredLastInterval</c>, <c>SiteHealth/alarmEvalErrors</c>,
/// <c>stuck</c> / <c>oldestPendingAgeSeconds</c>, and the bulk of the Site Health catalog) /// <c>SiteHealth/eventLogWriteFailures</c>) even if no page charts them yet. The C4/R4 lesson
/// that no page references; those stay private to their source until a page charts them. /// is that <strong>any metric named in two places needs one shared symbol</strong>: 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 <c>stuckCount</c> / <c>oldestPendingAgeSeconds</c>, the Site Call Audit <c>stuck</c> /
/// <c>oldestPendingAgeSeconds</c>, 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 /// Constants are grouped by source via nested static classes mirroring
/// <see cref="KpiSources"/>. /// <see cref="KpiSources"/>.
/// </para> /// </para>
@@ -65,6 +70,11 @@ public static class KpiMetrics
/// <summary>Cached calls that failed permanently in the last KPI interval.</summary> /// <summary>Cached calls that failed permanently in the last KPI interval.</summary>
public const string FailedLastInterval = "failedLastInterval"; public const string FailedLastInterval = "failedLastInterval";
/// <summary>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).</summary>
public const string DeliveredLastInterval = "deliveredLastInterval";
} }
/// <summary> /// <summary>
@@ -98,6 +108,16 @@ public static class KpiMetrics
/// <summary>Script-execution error count reported by the site.</summary> /// <summary>Script-execution error count reported by the site.</summary>
public const string ScriptErrors = "scriptErrors"; public const string ScriptErrors = "scriptErrors";
/// <summary>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).</summary>
public const string AlarmEvalErrors = "alarmEvalErrors";
/// <summary>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).</summary>
public const string EventLogWriteFailures = "eventLogWriteFailures";
/// <summary>Summed store-and-forward buffer depth across all buffers.</summary> /// <summary>Summed store-and-forward buffer depth across all buffers.</summary>
public const string SfBufferDepth = "sfBufferDepth"; public const string SfBufferDepth = "sfBufferDepth";
} }
@@ -40,7 +40,7 @@ public sealed class SiteHealthKpiSampleSource : IKpiSampleSource
private const string MetricConnectionsUp = "connectionsUp"; private const string MetricConnectionsUp = "connectionsUp";
private const string MetricConnectionsDown = KpiMetrics.SiteHealth.ConnectionsDown; private const string MetricConnectionsDown = KpiMetrics.SiteHealth.ConnectionsDown;
private const string MetricScriptErrors = KpiMetrics.SiteHealth.ScriptErrors; 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 MetricSfBufferDepth = KpiMetrics.SiteHealth.SfBufferDepth;
private const string MetricDeadLetters = KpiMetrics.SiteHealth.DeadLetters; private const string MetricDeadLetters = KpiMetrics.SiteHealth.DeadLetters;
private const string MetricParkedMessages = "parkedMessages"; private const string MetricParkedMessages = "parkedMessages";
@@ -48,7 +48,7 @@ public sealed class SiteHealthKpiSampleSource : IKpiSampleSource
private const string MetricEnabledInstances = "enabledInstances"; private const string MetricEnabledInstances = "enabledInstances";
private const string MetricDisabledInstances = "disabledInstances"; private const string MetricDisabledInstances = "disabledInstances";
private const string MetricAuditBacklogPending = "auditBacklogPending"; private const string MetricAuditBacklogPending = "auditBacklogPending";
private const string MetricEventLogWriteFailures = "eventLogWriteFailures"; private const string MetricEventLogWriteFailures = KpiMetrics.SiteHealth.EventLogWriteFailures;
private readonly ICentralHealthAggregator _aggregator; private readonly ICentralHealthAggregator _aggregator;
@@ -42,7 +42,7 @@ public sealed class SiteCallAuditKpiSampleSource : IKpiSampleSource
private const string MetricBuffered = KpiMetrics.SiteCallAudit.Buffered; private const string MetricBuffered = KpiMetrics.SiteCallAudit.Buffered;
private const string MetricParked = KpiMetrics.SiteCallAudit.Parked; private const string MetricParked = KpiMetrics.SiteCallAudit.Parked;
private const string MetricFailedLastInterval = KpiMetrics.SiteCallAudit.FailedLastInterval; 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 MetricStuck = "stuck";
private const string MetricOldestPendingAgeSeconds = "oldestPendingAgeSeconds"; private const string MetricOldestPendingAgeSeconds = "oldestPendingAgeSeconds";
@@ -148,4 +148,22 @@ public class KpiMetricAggregationTests
KpiRollupAggregation.Rate, KpiRollupAggregation.Rate,
KpiMetricAggregationCatalog.Resolve(KpiSources.SiteCallAudit, "deliveredLastInterval")); 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));
} }