using System.Diagnostics.Metrics; using ZB.MOM.WW.MxGateway.Server.Metrics; namespace ZB.MOM.WW.MxGateway.Tests.Metrics; public sealed class GatewayMetricsTests { /// Verifies that snapshot reflects all metric updates. [Fact] public void GetSnapshot_ReflectsSessionWorkerCommandEventAndFaultUpdates() { using GatewayMetrics metrics = new(); metrics.SessionOpened(); metrics.WorkerStarted(TimeSpan.FromMilliseconds(250)); metrics.CommandStarted("Register"); metrics.CommandSucceeded("Register", TimeSpan.FromMilliseconds(10)); metrics.CommandStarted("WriteSecured"); metrics.CommandFailed("WriteSecured", "AuthorizationFailed", TimeSpan.FromMilliseconds(12)); metrics.EventReceived("session-1", "OnDataChange"); metrics.EventReceived("session-1", "OnDataChange"); metrics.SetWorkerEventQueueDepth(7); metrics.AdjustGrpcEventStreamQueueDepth(3); metrics.QueueOverflow("session-events"); metrics.Fault("CommandTimeout"); metrics.WorkerKilled("CommandTimeout"); metrics.WorkerStopped("Killed"); metrics.HeartbeatFailed("session-1"); metrics.StreamDisconnected("ClientCancelled"); metrics.SessionClosed(); GatewayMetricsSnapshot snapshot = metrics.GetSnapshot(); Assert.Equal(0, snapshot.OpenSessions); Assert.Equal(0, snapshot.WorkersRunning); Assert.Equal(7, snapshot.WorkerEventQueueDepth); Assert.Equal(3, snapshot.GrpcEventStreamQueueDepth); Assert.Equal(1, snapshot.SessionsOpened); Assert.Equal(1, snapshot.SessionsClosed); Assert.Equal(2, snapshot.CommandsStarted); Assert.Equal(1, snapshot.CommandsSucceeded); Assert.Equal(1, snapshot.CommandsFailed); Assert.Equal(2, snapshot.EventsReceived); Assert.Equal(1, snapshot.QueueOverflows); Assert.Equal(1, snapshot.Faults); Assert.Equal(1, snapshot.WorkerKills); Assert.Equal(1, snapshot.WorkerExits); Assert.Equal(1, snapshot.HeartbeatFailures); Assert.Equal(1, snapshot.StreamDisconnects); Assert.Equal(1, snapshot.CommandFailuresByMethod["WriteSecured"]); Assert.Equal(2, snapshot.EventsByFamily["OnDataChange"]); Assert.Equal(2, snapshot.EventsBySession["session-1"]); } /// Verifies that negative queue depth is rejected. [Fact] public void SetEventQueueDepth_RejectsNegativeDepth() { using GatewayMetrics metrics = new(); ArgumentOutOfRangeException exception = Assert.Throws( () => metrics.SetWorkerEventQueueDepth(-1)); Assert.Equal("depth", exception.ParamName); } /// /// Verifies that increments /// mxgateway.alarms.provider_switches by one with the expected from/to/reason tags. /// The listener filters by the specific instance /// to avoid cross-talk between parallel tests. /// [Fact] public void AlarmProviderSwitched_IncrementsCounterWithExpectedTags() { using GatewayMetrics metrics = new(); using MeterListener listener = new(); long capturedValue = 0; string? capturedFrom = null; string? capturedTo = null; string? capturedReason = null; listener.InstrumentPublished = (instrument, meterListener) => { if (ReferenceEquals(instrument.Meter, metrics.Meter) && instrument.Name == "mxgateway.alarms.provider_switches") { meterListener.EnableMeasurementEvents(instrument); } }; listener.SetMeasurementEventCallback( (instrument, measurement, tags, _) => { if (!ReferenceEquals(instrument.Meter, metrics.Meter) || instrument.Name != "mxgateway.alarms.provider_switches") { return; } capturedValue += measurement; foreach (KeyValuePair tag in tags) { switch (tag.Key) { case "from": capturedFrom = tag.Value as string; break; case "to": capturedTo = tag.Value as string; break; case "reason": capturedReason = tag.Value as string; break; } } }); listener.Start(); metrics.AlarmProviderSwitched(1, 2, AlarmProviderSwitchReason.Failover); Assert.Equal(1, capturedValue); Assert.Equal("1", capturedFrom); Assert.Equal("2", capturedTo); Assert.Equal("failover", capturedReason); Assert.Equal(1, metrics.GetSnapshot().AlarmProviderSwitchCount); } /// /// Verifies that updates the /// mxgateway.alarms.provider_mode observable gauge to the value. /// [Fact] public void AlarmProviderSwitched_UpdatesProviderModeGauge() { using GatewayMetrics metrics = new(); using MeterListener listener = new(); int? capturedMode = null; listener.InstrumentPublished = (instrument, meterListener) => { if (ReferenceEquals(instrument.Meter, metrics.Meter) && instrument.Name == "mxgateway.alarms.provider_mode") { meterListener.EnableMeasurementEvents(instrument); } }; listener.SetMeasurementEventCallback( (instrument, measurement, _, _) => { if (ReferenceEquals(instrument.Meter, metrics.Meter) && instrument.Name == "mxgateway.alarms.provider_mode") { capturedMode = measurement; } }); listener.Start(); metrics.AlarmProviderSwitched(1, 2, AlarmProviderSwitchReason.Failover); listener.RecordObservableInstruments(); Assert.Equal(2, capturedMode); } /// /// Verifies that increments /// mxgateway.heartbeats.failed without emitting a session_id tag: the tag is /// unbounded cardinality (every session mints a new exporter time series), so per-session /// attribution is deliberately kept out of the exported counter (SEC-20). /// [Fact] public void HeartbeatFailed_IncrementsCounterWithoutSessionIdTag() { using GatewayMetrics metrics = new(); using MeterListener listener = new(); long capturedValue = 0; bool sawSessionIdTag = false; listener.InstrumentPublished = (instrument, meterListener) => { if (ReferenceEquals(instrument.Meter, metrics.Meter) && instrument.Name == "mxgateway.heartbeats.failed") { meterListener.EnableMeasurementEvents(instrument); } }; listener.SetMeasurementEventCallback( (instrument, measurement, tags, _) => { if (!ReferenceEquals(instrument.Meter, metrics.Meter) || instrument.Name != "mxgateway.heartbeats.failed") { return; } capturedValue += measurement; foreach (KeyValuePair tag in tags) { if (tag.Key == "session_id") { sawSessionIdTag = true; } } }); listener.Start(); metrics.HeartbeatFailed("session-1"); Assert.Equal(1, capturedValue); Assert.False(sawSessionIdTag); Assert.Equal(1, metrics.GetSnapshot().HeartbeatFailures); } /// Verifies that removing session events only affects that session. [Fact] public void RemoveSessionEvents_RemovesOnlyThatSession() { using GatewayMetrics metrics = new(); metrics.EventReceived("session-1", "OnDataChange"); metrics.EventReceived("session-2", "OnWriteComplete"); metrics.RemoveSessionEvents("session-1"); GatewayMetricsSnapshot snapshot = metrics.GetSnapshot(); Assert.Equal(2, snapshot.EventsReceived); Assert.False(snapshot.EventsBySession.ContainsKey("session-1")); Assert.Equal(1, snapshot.EventsBySession["session-2"]); Assert.Equal(1, snapshot.EventsByFamily["OnDataChange"]); Assert.Equal(1, snapshot.EventsByFamily["OnWriteComplete"]); } }