diff --git a/dotnet/src/SmooAI.Logger/LogContext.cs b/dotnet/src/SmooAI.Logger/LogContext.cs
index 69761a5e..69d31726 100644
--- a/dotnet/src/SmooAI.Logger/LogContext.cs
+++ b/dotnet/src/SmooAI.Logger/LogContext.cs
@@ -23,6 +23,7 @@ public static class ContextKey
public const string RequestId = "requestId";
public const string Duration = "duration";
public const string TraceId = "traceId";
+ public const string SpanId = "spanId";
public const string Error = "error";
public const string Namespace = "namespace";
public const string Service = "service";
diff --git a/dotnet/src/SmooAI.Logger/SmooLogger.cs b/dotnet/src/SmooAI.Logger/SmooLogger.cs
index 7431d92a..052cef67 100644
--- a/dotnet/src/SmooAI.Logger/SmooLogger.cs
+++ b/dotnet/src/SmooAI.Logger/SmooLogger.cs
@@ -428,6 +428,18 @@ private void Emit(Level level, string message, object? data, Exception? error)
}
}
+ // Correlate with the active distributed trace when one is in scope: stamp the
+ // real W3C trace/span id from Activity.Current instead of the fabricated uuid
+ // seeded at construction. Falls back to the prior traceId (no spanId) when no
+ // W3C activity is active. This is captured at emit time — the span is live at
+ // the log call, not necessarily when the logger was created.
+ var activity = System.Diagnostics.Activity.Current;
+ if (activity is not null && activity.IdFormat == System.Diagnostics.ActivityIdFormat.W3C)
+ {
+ entry[ContextKey.TraceId] = activity.TraceId.ToHexString();
+ entry[ContextKey.SpanId] = activity.SpanId.ToHexString();
+ }
+
entry[ContextKey.Level] = (int)level;
entry[ContextKey.LogLevel] = level.ToWireString();
entry[ContextKey.Time] = DateTimeOffset.UtcNow.ToString("O");
diff --git a/dotnet/tests/SmooAI.Logger.Tests/TraceCorrelationTests.cs b/dotnet/tests/SmooAI.Logger.Tests/TraceCorrelationTests.cs
new file mode 100644
index 00000000..601ad81f
--- /dev/null
+++ b/dotnet/tests/SmooAI.Logger.Tests/TraceCorrelationTests.cs
@@ -0,0 +1,64 @@
+using System.Diagnostics;
+using System.Text.Json;
+using SmooAI.Logger;
+
+namespace SmooAI.Logger.Tests;
+
+///
+/// Verifies the emit-time trace correlation (th-de3805): a log emitted inside an
+/// active W3C carries that span's real trace/span id, and
+/// falls back to the fabricated correlation uuid when no activity is in scope.
+///
+public class TraceCorrelationTests
+{
+ private static (SmooLogger Logger, StringWriter Out) Build()
+ {
+ var writer = new StringWriter();
+ return (new SmooLogger(new SmooLoggerOptions { Output = writer, PrettyPrint = false }), writer);
+ }
+
+ private static JsonElement ParseSingle(string captured)
+ {
+ var line = captured.Split('\n', StringSplitOptions.RemoveEmptyEntries)[0];
+ return JsonDocument.Parse(line).RootElement;
+ }
+
+ [Fact]
+ public void Log_Within_Active_Activity_Carries_Real_TraceAndSpanId()
+ {
+ // ActivitySource only starts activities when a listener samples them.
+ using var source = new ActivitySource("smooai.logger.tests");
+ using var listener = new ActivityListener
+ {
+ ShouldListenTo = s => s.Name == "smooai.logger.tests",
+ Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData,
+ };
+ ActivitySource.AddActivityListener(listener);
+
+ var (logger, writer) = Build();
+ using var activity = source.StartActivity("op");
+ Assert.NotNull(activity);
+ Assert.Equal(ActivityIdFormat.W3C, activity!.IdFormat);
+
+ logger.LogInfo("inside span");
+ var entry = ParseSingle(writer.ToString());
+
+ Assert.Equal(activity.TraceId.ToHexString(), entry.GetProperty("traceId").GetString());
+ Assert.Equal(activity.SpanId.ToHexString(), entry.GetProperty("spanId").GetString());
+ }
+
+ [Fact]
+ public void Log_Without_Activity_Falls_Back_To_Correlation_Uuid()
+ {
+ // No listener registered here, and any ambient activity cleared.
+ Activity.Current = null;
+
+ var (logger, writer) = Build();
+ logger.LogInfo("no span");
+ var entry = ParseSingle(writer.ToString());
+
+ // Prior behavior: traceId == the fabricated correlation id, no spanId emitted.
+ Assert.Equal(entry.GetProperty("correlationId").GetString(), entry.GetProperty("traceId").GetString());
+ Assert.False(entry.TryGetProperty("spanId", out _));
+ }
+}