feat(telemetry.serilog): TraceContextEnricher for trace<->log correlation
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.Telemetry.Serilog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stamps <c>trace_id</c> and <c>span_id</c> from <see cref="Activity.Current"/> onto every Serilog
|
||||||
|
/// log event, enabling a log line to be correlated back to its originating trace in a backend.
|
||||||
|
/// When <see cref="Activity.Current"/> is null (no active span — background services, startup,
|
||||||
|
/// non-traced paths) the enricher emits nothing; it does NOT inject empty strings or zero values.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TraceContextEnricher : ILogEventEnricher
|
||||||
|
{
|
||||||
|
/// <summary>Serilog property name carrying the W3C trace id.</summary>
|
||||||
|
public const string TraceIdPropertyName = "trace_id";
|
||||||
|
|
||||||
|
/// <summary>Serilog property name carrying the W3C span id.</summary>
|
||||||
|
public const string SpanIdPropertyName = "span_id";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds <c>trace_id</c>/<c>span_id</c> properties from <see cref="Activity.Current"/> when an
|
||||||
|
/// activity is active; otherwise leaves the event untouched.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logEvent">The log event to enrich.</param>
|
||||||
|
/// <param name="propertyFactory">Factory used to create the trace-context properties.</param>
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,8 @@ public static class ZbSerilogConfig
|
|||||||
|
|
||||||
enrich.WithProperty(ZbLogEnricherNames.NodeHostname, Environment.MachineName);
|
enrich.WithProperty(ZbLogEnricherNames.NodeHostname, Environment.MachineName);
|
||||||
|
|
||||||
|
enrich.With(new TraceContextEnricher());
|
||||||
|
|
||||||
return loggerConfiguration;
|
return loggerConfiguration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
@@ -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<ActivityContext> _) =>
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user