diff --git a/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs b/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs
index 5ed29ea..201d3e0 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs
@@ -183,7 +183,7 @@ public sealed class MxAccessGatewayService(
// call is cancelled, instead of leaving it to the transport's own timeout.
await responseStream.WriteAsync(publicEvent, context.CancellationToken).ConfigureAwait(false);
metrics.RecordEventStreamSend(
- publicEvent.Family.ToString(),
+ MxEventFamilyNames.Get(publicEvent.Family),
Stopwatch.GetElapsedTime(sendStartTimestamp));
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Metrics/MxEventFamilyNames.cs b/src/ZB.MOM.WW.MxGateway.Server/Metrics/MxEventFamilyNames.cs
new file mode 100644
index 0000000..1fe54d4
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Server/Metrics/MxEventFamilyNames.cs
@@ -0,0 +1,28 @@
+using System.Collections.Frozen;
+using ZB.MOM.WW.MxGateway.Contracts.Proto;
+
+namespace ZB.MOM.WW.MxGateway.Server.Metrics;
+
+///
+/// Cached names for the per-event metric tags.
+///
+///
+/// allocates a new string on every call, and the event-received
+/// and stream-send metrics tag every event with its family — once per event at the worker
+/// boundary and once per event per subscriber on the stream. The names are fixed by the
+/// contract, so they are computed once here. A value the contract does not define falls back
+/// to , which is the same string the uncached call produced.
+///
+public static class MxEventFamilyNames
+{
+ private static readonly FrozenDictionary Names =
+ Enum.GetValues()
+ .Distinct()
+ .ToFrozenDictionary(family => family, family => family.ToString());
+
+ /// Gets the name of an event family.
+ /// The event family.
+ /// The family's name, identical to .
+ public static string Get(MxEventFamily family) =>
+ Names.TryGetValue(family, out string? name) ? name : family.ToString();
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs
index 873dcdd..fc8b48b 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs
@@ -692,7 +692,7 @@ public sealed class WorkerClient : IWorkerClient
{
if (workerEvent.Event is not null)
{
- _metrics?.EventReceived(SessionId, workerEvent.Event.Family.ToString());
+ _metrics?.EventReceived(SessionId, MxEventFamilyNames.Get(workerEvent.Event.Family));
}
if (_eventStaging.Writer.TryWrite(workerEvent))
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Metrics/MxEventFamilyNamesTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Metrics/MxEventFamilyNamesTests.cs
new file mode 100644
index 0000000..953280f
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Metrics/MxEventFamilyNamesTests.cs
@@ -0,0 +1,41 @@
+using ZB.MOM.WW.MxGateway.Contracts.Proto;
+using ZB.MOM.WW.MxGateway.Server.Metrics;
+
+namespace ZB.MOM.WW.MxGateway.Tests.Metrics;
+
+public sealed class MxEventFamilyNamesTests
+{
+ ///
+ /// Every defined family maps to exactly the name produces, so
+ /// switching the per-event metric tags to the cached lookup changes no exported tag value.
+ ///
+ [Fact]
+ public void Get_ForEveryDefinedFamily_MatchesEnumToString()
+ {
+ foreach (MxEventFamily family in Enum.GetValues())
+ {
+ Assert.Equal(family.ToString(), MxEventFamilyNames.Get(family));
+ }
+ }
+
+ /// A defined family returns the same cached string instance on every call (no per-event allocation).
+ [Fact]
+ public void Get_ForDefinedFamily_ReturnsCachedInstance()
+ {
+ Assert.Same(
+ MxEventFamilyNames.Get(MxEventFamily.OnDataChange),
+ MxEventFamilyNames.Get(MxEventFamily.OnDataChange));
+ }
+
+ ///
+ /// A numeric value the contract does not define (a newer worker, or a malformed frame) still
+ /// produces the form rather than throwing.
+ ///
+ [Fact]
+ public void Get_ForUndefinedValue_FallsBackToToString()
+ {
+ MxEventFamily undefined = (MxEventFamily)987654;
+
+ Assert.Equal(undefined.ToString(), MxEventFamilyNames.Get(undefined));
+ }
+}