using System.Diagnostics.Metrics; using ZB.MOM.WW.ScadaBridge.Commons.Observability; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Observability; /// /// ScadaBridge#81: the instruments added beyond the first set. Each is observed through a /// filtered to the emitting (test) thread, because the meter is /// process-global and other tests emit on it concurrently. /// public sealed class ExtendedInstrumentTests { private static List<(long Value, Dictionary Tags)> Capture(string instrument, Action act) { var tid = Environment.CurrentManagedThreadId; var got = new List<(long, Dictionary)>(); using var listener = new MeterListener { InstrumentPublished = (i, l) => { if (i.Meter.Name == ScadaBridgeTelemetry.MeterName && i.Name == instrument) l.EnableMeasurementEvents(i); } }; listener.SetMeasurementEventCallback((_, m, tags, _) => { if (Environment.CurrentManagedThreadId != tid) return; var d = new Dictionary(); foreach (var t in tags) d[t.Key] = t.Value; got.Add((m, d)); }); listener.Start(); act(); return got; } /// A state change emits one transition tagged with the new state; repeating the same state emits none. [Fact] public void DataConnectionTransitions_EmitOnlyOnRealStateChange() { var name = "xfer-" + Guid.NewGuid().ToString("N"); try { var got = Capture("scadabridge.site.data_connections.transitions", () => { ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Connecting); ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Connected); ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Connected); ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Disconnected); }); Assert.Equal( new object?[] { "Connecting", "Connected", "Disconnected" }, got.Select(g => g.Tags["state"]).ToArray()); } finally { ScadaBridgeTelemetry.RemoveDataConnection(name); } } /// Store-and-forward activity is counted by action and category. [Fact] public void StoreAndForwardActivity_IsCountedByActionAndCategory() { var got = Capture("scadabridge.store_and_forward.activity", () => ScadaBridgeTelemetry.RecordStoreAndForwardActivity("Parked", "ExternalSystem")); var m = Assert.Single(got); Assert.Equal(1, m.Value); Assert.Equal("Parked", m.Tags["action"]); Assert.Equal("ExternalSystem", m.Tags["category"]); } /// Deployment failures are counted by reason. [Fact] public void DeploymentsFailed_IsCountedByReason() { var got = Capture("scadabridge.deployments.failed", () => ScadaBridgeTelemetry.RecordDeploymentFailed("timeout")); var m = Assert.Single(got); Assert.Equal("timeout", m.Tags["reason"]); } /// A script run emits both the run counter and the duration histogram, clamping negatives. [Fact] public void ScriptRun_EmitsCounterAndDuration() { var runs = Capture("scadabridge.site.script.runs", () => ScadaBridgeTelemetry.RecordScriptRun("failed", 5)); Assert.Equal("failed", Assert.Single(runs).Tags["outcome"]); var durations = Capture("scadabridge.site.script.duration", () => ScadaBridgeTelemetry.RecordScriptRun("completed", -3)); var d = Assert.Single(durations); Assert.Equal(0, d.Value); Assert.Equal("completed", d.Tags["outcome"]); } }