diff --git a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/TraceContextEnricher.cs b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/TraceContextEnricher.cs
new file mode 100644
index 0000000..f2b88cd
--- /dev/null
+++ b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/TraceContextEnricher.cs
@@ -0,0 +1,43 @@
+using System.Diagnostics;
+using Serilog.Core;
+using Serilog.Events;
+
+namespace ZB.MOM.WW.Telemetry.Serilog;
+
+///
+/// Stamps trace_id and span_id from onto every Serilog
+/// log event, enabling a log line to be correlated back to its originating trace in a backend.
+/// When is null (no active span — background services, startup,
+/// non-traced paths) the enricher emits nothing; it does NOT inject empty strings or zero values.
+///
+public sealed class TraceContextEnricher : ILogEventEnricher
+{
+ /// Serilog property name carrying the W3C trace id.
+ public const string TraceIdPropertyName = "trace_id";
+
+ /// Serilog property name carrying the W3C span id.
+ public const string SpanIdPropertyName = "span_id";
+
+ ///
+ /// Adds trace_id/span_id properties from when an
+ /// activity is active; otherwise leaves the event untouched.
+ ///
+ /// The log event to enrich.
+ /// Factory used to create the trace-context properties.
+ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
+ {
+ ArgumentNullException.ThrowIfNull(logEvent);
+ ArgumentNullException.ThrowIfNull(propertyFactory);
+
+ var activity = Activity.Current;
+ if (activity is null)
+ {
+ return;
+ }
+
+ logEvent.AddPropertyIfAbsent(
+ propertyFactory.CreateProperty(TraceIdPropertyName, activity.TraceId.ToString()));
+ logEvent.AddPropertyIfAbsent(
+ propertyFactory.CreateProperty(SpanIdPropertyName, activity.SpanId.ToString()));
+ }
+}
diff --git a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogConfig.cs b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogConfig.cs
index 416d27d..927fe58 100644
--- a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogConfig.cs
+++ b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogConfig.cs
@@ -64,6 +64,8 @@ public static class ZbSerilogConfig
enrich.WithProperty(ZbLogEnricherNames.NodeHostname, Environment.MachineName);
+ enrich.With(new TraceContextEnricher());
+
return loggerConfiguration;
}
}
diff --git a/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Serilog.Tests/TraceContextEnricherTests.cs b/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Serilog.Tests/TraceContextEnricherTests.cs
new file mode 100644
index 0000000..e6d88c5
--- /dev/null
+++ b/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Serilog.Tests/TraceContextEnricherTests.cs
@@ -0,0 +1,66 @@
+using System.Diagnostics;
+using Serilog;
+using Serilog.Core;
+using Serilog.Events;
+using Serilog.Sinks.InMemory;
+using ZB.MOM.WW.Telemetry;
+using ZB.MOM.WW.Telemetry.Serilog;
+
+namespace ZB.MOM.WW.Telemetry.Serilog.Tests;
+
+public sealed class TraceContextEnricherTests
+{
+ private const string SourceName = "ZB.MOM.WW.Telemetry.Serilog.Tests.TraceContext";
+
+ private static Logger BuildLogger(InMemorySink sink) =>
+ new LoggerConfiguration()
+ .Enrich.With(new TraceContextEnricher())
+ .WriteTo.Sink(sink)
+ .CreateLogger();
+
+ private static string? ScalarOrNull(LogEvent logEvent, string propertyName) =>
+ logEvent.Properties.TryGetValue(propertyName, out var value) && value is ScalarValue scalar
+ ? scalar.Value?.ToString()
+ : null;
+
+ [Fact]
+ public void Active_activity_stamps_trace_id_and_span_id()
+ {
+ using var listener = new ActivityListener
+ {
+ ShouldListenTo = source => source.Name == SourceName,
+ Sample = (ref ActivityCreationOptions _) =>
+ ActivitySamplingResult.AllDataAndRecorded,
+ };
+ ActivitySource.AddActivityListener(listener);
+
+ using var activitySource = new ActivitySource(SourceName);
+ var sink = new InMemorySink();
+ using var logger = BuildLogger(sink);
+
+ using var activity = activitySource.StartActivity("unit-test");
+ Assert.NotNull(activity);
+ Assert.NotNull(Activity.Current);
+
+ logger.Information("traced");
+
+ var logEvent = Assert.Single(sink.LogEvents);
+ Assert.Equal(Activity.Current!.TraceId.ToString(), ScalarOrNull(logEvent, "trace_id"));
+ Assert.Equal(Activity.Current!.SpanId.ToString(), ScalarOrNull(logEvent, "span_id"));
+ }
+
+ [Fact]
+ public void No_active_activity_omits_trace_id_and_span_id()
+ {
+ Assert.Null(Activity.Current);
+
+ var sink = new InMemorySink();
+ using var logger = BuildLogger(sink);
+
+ logger.Information("untraced");
+
+ var logEvent = Assert.Single(sink.LogEvents);
+ Assert.False(logEvent.Properties.ContainsKey("trace_id"));
+ Assert.False(logEvent.Properties.ContainsKey("span_id"));
+ }
+}