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:
/// <c>deliveredLastInterval</c> is emitted by both
/// <see cref="KpiSources.NotificationOutbox"/> and <see cref="KpiSources.SiteCallAudit"/>,
/// so a metric name is not globally unique. Every metric string is taken from the
/// emitting source verbatim (shared <see cref="KpiMetrics"/> constants where they exist,
/// otherwise the source's own private literal), so this catalog stays a faithful mirror
/// of what actually lands in <c>KpiSample.Metric</c>.
/// so a metric name is not globally unique. Every catalogued metric string is now a shared
/// <see cref="KpiMetrics"/> constant — the emitting source consumes the same constant — so an
/// emitter rename is a <strong>compile error</strong>, not a silent Gauge downgrade (R4), and
/// this catalog stays a faithful mirror of what actually lands in <c>KpiSample.Metric</c>.
/// </para>
/// <para>
/// The catalog stores only the <see cref="KpiRollupAggregation.Rate"/> pairs; anything
@@ -57,12 +57,6 @@ public enum KpiRollupAggregation
/// </remarks>
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>
/// 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"/>.
@@ -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),
};
/// <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.
/// </para>
/// <para>
/// Only the metrics a trend page actually charts are catalogued here. Sources may emit
/// additional internal metrics (e.g. the Notification Outbox <c>stuckCount</c> /
/// <c>oldestPendingAgeSeconds</c>, the Site Call Audit <c>deliveredLastInterval</c> /
/// <c>stuck</c> / <c>oldestPendingAgeSeconds</c>, 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, <em>and</em>
/// those the <see cref="KpiMetricAggregationCatalog"/> classifies for the hourly rollup fold
/// (e.g. <c>SiteCallAudit/deliveredLastInterval</c>, <c>SiteHealth/alarmEvalErrors</c>,
/// <c>SiteHealth/eventLogWriteFailures</c>) even if no page charts them yet. The C4/R4 lesson
/// 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
/// <see cref="KpiSources"/>.
/// </para>
@@ -65,6 +70,11 @@ public static class KpiMetrics
/// <summary>Cached calls that failed permanently in the last KPI interval.</summary>
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>
@@ -98,6 +108,16 @@ public static class KpiMetrics
/// <summary>Script-execution error count reported by the site.</summary>
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>
public const string SfBufferDepth = "sfBufferDepth";
}
@@ -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;
@@ -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";
@@ -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));
}