diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs b/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs index e943b705..afc500f0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs @@ -27,6 +27,24 @@ namespace ZB.MOM.WW.ScadaBridge.Host; /// public static class SiteServiceRegistration { + /// + /// Every OpenTelemetry is told to observe. + /// + /// + /// ZbTelemetryOptions.Meters is an ALLOWLIST, and an unlisted meter's instruments + /// are simply never observed — no error, no warning, no missing-metric signal anywhere. + /// The LocalDb replication meter was silently absent from the rig's /metrics + /// scrape until the Phase 1 live gate went looking for it (the plan had assumed it + /// flowed through automatically). Hoisted to a named constant so the list is + /// assertable; adding a meter anywhere in the product means adding it here. + /// + public static readonly string[] ObservedMeters = + [ + ScadaBridgeTelemetry.MeterName, + // Emitted only on site nodes; harmless on central, which never records against it. + LocalDbMetrics.MeterName, + ]; + /// Registers all DI services required for the site role. /// The service collection to register into. /// Application configuration for options binding. @@ -262,7 +280,7 @@ public static class SiteServiceRegistration o.ServiceName = "scadabridge"; o.SiteId = config["ScadaBridge:Node:SiteId"] ?? "central"; o.NodeRole = config["ScadaBridge:Node:Role"]; - o.Meters = [ScadaBridgeTelemetry.MeterName]; + o.Meters = ObservedMeters; if (Enum.TryParse(config["ScadaBridge:Telemetry:Exporter"], ignoreCase: true, out var exporter)) o.Exporter = exporter; var otlp = config["ScadaBridge:Telemetry:OtlpEndpoint"]; diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbWiringTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbWiringTests.cs index cff13488..c2d3fc91 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbWiringTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbWiringTests.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.HealthMonitoring; +using ZB.MOM.WW.Telemetry; using ZB.MOM.WW.LocalDb; using ZB.MOM.WW.LocalDb.Replication; using ZB.MOM.WW.ScadaBridge.Host; @@ -196,6 +198,23 @@ public class SiteLocalDbWiringTests : IDisposable Assert.Null(report.LocalDbOplogBacklog); } + [Fact] + public void Site_Telemetry_Allowlists_TheLocalDbReplicationMeter() + { + // ZbTelemetryOptions.Meters is an ALLOWLIST: an unlisted Meter's instruments are + // never observed, with no error and no missing-metric warning anywhere. The + // replication meter was silently absent from the rig's /metrics scrape until the + // live gate looked for it, so pin the allowlist rather than trusting the next + // reader to remember. + // + // This asserts on the allowlist constant, not on OTel's observed-meter set: + // AddZbTelemetry applies its options to a local instance and never registers + // IOptions, so there is nothing resolvable to interrogate. The end-to-end proof + // that localdb_* actually reaches /metrics is the Task 12 live gate; this test + // exists to stop the entry being dropped again. + Assert.Contains(LocalDbMetrics.MeterName, SiteServiceRegistration.ObservedMeters); + } + [Fact] public void Site_LocalDb_CreatesTheConfiguredFile() {