diff --git a/src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs b/src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs index 8727478..623dac2 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics.Metrics; +using System.Globalization; namespace ZB.MOM.WW.MxGateway.Server.Metrics; @@ -22,6 +23,7 @@ public sealed class GatewayMetrics : IDisposable private readonly Counter _heartbeatFailuresCounter; private readonly Counter _streamDisconnectsCounter; private readonly Counter _retryAttemptsCounter; + private readonly Counter _alarmProviderSwitchesCounter; private readonly Histogram _workerStartupLatencyHistogram; private readonly Histogram _commandLatencyHistogram; private readonly Histogram _eventStreamSendLatencyHistogram; @@ -34,6 +36,7 @@ public sealed class GatewayMetrics : IDisposable private int _workersRunning; private int _workerEventQueueDepth; private int _grpcEventStreamQueueDepth; + private int _alarmProviderMode; private long _sessionsOpened; private long _sessionsClosed; private long _commandsStarted; @@ -68,6 +71,7 @@ public sealed class GatewayMetrics : IDisposable _heartbeatFailuresCounter = _meter.CreateCounter("mxgateway.heartbeats.failed"); _streamDisconnectsCounter = _meter.CreateCounter("mxgateway.grpc.streams.disconnected"); _retryAttemptsCounter = _meter.CreateCounter("mxgateway.retries.attempted"); + _alarmProviderSwitchesCounter = _meter.CreateCounter("mxgateway.alarms.provider_switches"); _workerStartupLatencyHistogram = _meter.CreateHistogram("mxgateway.workers.startup.duration", "s"); _commandLatencyHistogram = _meter.CreateHistogram("mxgateway.commands.duration", "s"); _eventStreamSendLatencyHistogram = _meter.CreateHistogram("mxgateway.events.stream_send.duration", "s"); @@ -76,6 +80,7 @@ public sealed class GatewayMetrics : IDisposable _meter.CreateObservableGauge("mxgateway.workers.running", GetWorkersRunning); _meter.CreateObservableGauge("mxgateway.events.worker_queue.depth", GetWorkerEventQueueDepth); _meter.CreateObservableGauge("mxgateway.events.grpc_stream_queue.depth", GetGrpcEventStreamQueueDepth); + _meter.CreateObservableGauge("mxgateway.alarms.provider_mode", GetAlarmProviderMode); } /// @@ -377,6 +382,26 @@ public sealed class GatewayMetrics : IDisposable _retryAttemptsCounter.Add(1, new KeyValuePair("area", area)); } + /// + /// Records that the alarm provider switched modes and updates the current provider mode gauge. + /// + /// Provider mode before the switch (1=alarmmgr, 2=subtag, 0=unknown). + /// Provider mode after the switch (1=alarmmgr, 2=subtag, 0=unknown). + /// Human-readable reason for the switch. + public void AlarmProviderSwitched(int fromMode, int toMode, string reason) + { + lock (_syncRoot) + { + _alarmProviderMode = toMode; + } + + _alarmProviderSwitchesCounter.Add( + 1, + new KeyValuePair("from", fromMode.ToString(CultureInfo.InvariantCulture)), + new KeyValuePair("to", toMode.ToString(CultureInfo.InvariantCulture)), + new KeyValuePair("reason", reason ?? string.Empty)); + } + /// /// Returns a snapshot of all current metric values. /// @@ -455,6 +480,14 @@ public sealed class GatewayMetrics : IDisposable } } + private int GetAlarmProviderMode() + { + lock (_syncRoot) + { + return _alarmProviderMode; + } + } + private static void Increment(Dictionary values, string key) { values.TryGetValue(key, out long currentValue); diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Metrics/GatewayMetricsTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Metrics/GatewayMetricsTests.cs index ac1f9c2..2ac7ff1 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Metrics/GatewayMetricsTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Metrics/GatewayMetricsTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.Metrics; using ZB.MOM.WW.MxGateway.Server.Metrics; namespace ZB.MOM.WW.MxGateway.Tests.Metrics; @@ -63,6 +64,98 @@ public sealed class GatewayMetricsTests 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 (Tests-027). + /// + [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, "test"); + + Assert.Equal(1, capturedValue); + Assert.Equal("1", capturedFrom); + Assert.Equal("2", capturedTo); + Assert.Equal("test", capturedReason); + } + + /// + /// 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, "test"); + listener.RecordObservableInstruments(); + + Assert.Equal(2, capturedMode); + } + /// Verifies that removing session events only affects that session. [Fact] public void RemoveSessionEvents_RemovesOnlyThatSession()