Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/src/SmooAI.Logger/LogContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
12 changes: 12 additions & 0 deletions dotnet/src/SmooAI.Logger/SmooLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
64 changes: 64 additions & 0 deletions dotnet/tests/SmooAI.Logger.Tests/TraceCorrelationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Diagnostics;
using System.Text.Json;
using SmooAI.Logger;

namespace SmooAI.Logger.Tests;

/// <summary>
/// Verifies the emit-time trace correlation (th-de3805): a log emitted inside an
/// active W3C <see cref="Activity"/> carries that span's real trace/span id, and
/// falls back to the fabricated correlation uuid when no activity is in scope.
/// </summary>
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<ActivityContext> _) => 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 _));
}
}
Loading