From a6040355bff2262fb17de69d3b0984356a8eb7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:58:08 +0200 Subject: [PATCH 1/6] ref(logs): read TraceId and ParentSpanId from Active-Span or Propagation-Context --- .../SentryStructuredLogger.cs | 6 ++--- src/Sentry.Serilog/SentrySink.Structured.cs | 24 +------------------ .../Internal/DefaultSentryStructuredLogger.cs | 6 ++--- src/Sentry/SentryLog.cs | 22 +++++++++++++++++ .../SentryStructuredLoggerTests.cs | 19 +++++++++------ .../SentryStructuredLoggerTests.cs | 21 +++++++++------- 6 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs index 36e68454a6..23f549e0d0 100644 --- a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs +++ b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs @@ -42,7 +42,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } var timestamp = _clock.GetUtcNow(); - var traceHeader = _hub.GetTraceHeader() ?? SentryTraceHeader.Empty; + SentryLog.GetTraceIdAndSpanId(_hub, out var traceId, out var spanId); var level = logLevel.ToSentryLogLevel(); Debug.Assert(level != default); @@ -81,11 +81,11 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } } - SentryLog log = new(timestamp, traceHeader.TraceId, level, message) + SentryLog log = new(timestamp, traceId, level, message) { Template = template, Parameters = parameters.DrainToImmutable(), - ParentSpanId = traceHeader.SpanId, + ParentSpanId = spanId, }; log.SetDefaultAttributes(_options, _sdk); diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index 6584afb934..5b22ab7d97 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -7,7 +7,7 @@ internal sealed partial class SentrySink { private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEvent logEvent, string formatted, string? template) { - GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes); SentryLog log = new(logEvent.Timestamp, traceId, logEvent.Level.ToSentryLogLevel(), formatted) @@ -27,28 +27,6 @@ private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEve hub.Logger.CaptureLog(log); } - private static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId) - { - var span = hub.GetSpan(); - if (span is not null) - { - traceId = span.TraceId; - spanId = span.SpanId; - return; - } - - var scope = hub.GetScope(); - if (scope is not null) - { - traceId = scope.PropagationContext.TraceId; - spanId = scope.PropagationContext.SpanId; - return; - } - - traceId = SentryId.Empty; - spanId = null; - } - private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEvent, out ImmutableArray> parameters, out List> attributes) { var propertyNames = new HashSet(); diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 42d61705f1..1f13191ed2 100644 --- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs +++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs @@ -27,7 +27,7 @@ internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemC private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action? configureLog) { var timestamp = _clock.GetUtcNow(); - var traceHeader = _hub.GetTraceHeader() ?? SentryTraceHeader.Empty; + SentryLog.GetTraceIdAndSpanId(_hub, out var traceId, out var spanId); string message; try @@ -51,11 +51,11 @@ private protected override void CaptureLog(SentryLogLevel level, string template @params = builder.DrainToImmutable(); } - SentryLog log = new(timestamp, traceHeader.TraceId, level, message) + SentryLog log = new(timestamp, traceId, level, message) { Template = template, Parameters = @params, - ParentSpanId = traceHeader.SpanId, + ParentSpanId = spanId, }; try diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index 7e58fec173..dfadc729f1 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -257,4 +257,26 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndObject(); } + + internal static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId) + { + var span = hub.GetSpan(); + if (span is not null) + { + traceId = span.TraceId; + spanId = span.SpanId; + return; + } + + var scope = hub.GetScope(); + if (scope is not null) + { + traceId = scope.PropagationContext.TraceId; + spanId = scope.PropagationContext.SpanId; + return; + } + + traceId = SentryId.Empty; + spanId = null; + } } diff --git a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs index dcf7e0a4c5..127f1b474e 100644 --- a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs @@ -51,10 +51,12 @@ public Fixture() public void EnableLogs(bool isEnabled) => Options.Value.Experimental.EnableLogs = isEnabled; public void SetMinimumLogLevel(LogLevel logLevel) => Options.Value.ExperimentalLogging.MinimumLogLevel = logLevel; - public void WithTraceHeader(SentryId traceId, SpanId parentSpanId) + public void WithActiveSpan(SentryId traceId, SpanId parentSpanId) { - var traceHeader = new SentryTraceHeader(traceId, parentSpanId, null); - Hub.GetTraceHeader().Returns(traceHeader); + var span = Substitute.For(); + span.TraceId.Returns(traceId); + span.SpanId.Returns(parentSpanId); + Hub.GetSpan().Returns(span); } public SentryStructuredLogger GetSut() @@ -83,7 +85,7 @@ public void Log_LogLevel_CaptureLog(LogLevel logLevel, SentryLogLevel expectedLe { var traceId = SentryId.Create(); var parentSpanId = SpanId.Create(); - _fixture.WithTraceHeader(traceId, parentSpanId); + _fixture.WithActiveSpan(traceId, parentSpanId); var logger = _fixture.GetSut(); EventId eventId = new(123, "EventName"); @@ -127,15 +129,18 @@ public void Log_LogLevelNone_DoesNotCaptureLog() } [Fact] - public void Log_WithoutTraceHeader_CaptureLog() + public void Log_WithoutActiveSpan_CaptureLog() { + var scope = new Scope(_fixture.Options.Value); + _fixture.Hub.GetSpan().Returns((ISpan?)null); + _fixture.Hub.SubstituteConfigureScope(scope); var logger = _fixture.GetSut(); logger.Log(LogLevel.Information, new EventId(123, "EventName"), new InvalidOperationException("message"), "Message with {Argument}.", "argument"); var log = _fixture.CapturedLogs.Dequeue(); - log.TraceId.Should().Be(SentryTraceHeader.Empty.TraceId); - log.ParentSpanId.Should().Be(SentryTraceHeader.Empty.SpanId); + log.TraceId.Should().Be(scope.PropagationContext.TraceId); + log.ParentSpanId.Should().Be(scope.PropagationContext.SpanId); } [Fact] diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index b0a2e6e3a5..75836fcfb4 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -26,8 +26,10 @@ public Fixture() Hub.IsEnabled.Returns(true); - var traceHeader = new SentryTraceHeader(TraceId, ParentSpanId.Value, null); - Hub.GetTraceHeader().Returns(traceHeader); + var span = Substitute.For(); + span.TraceId.Returns(TraceId); + span.SpanId.Returns(ParentSpanId.Value); + Hub.GetSpan().Returns(span); ExpectedAttributes = new Dictionary(1) { @@ -46,11 +48,14 @@ public Fixture() public Dictionary ExpectedAttributes { get; } - public void WithoutTraceHeader() + public void WithoutActiveSpan() { - Hub.GetTraceHeader().Returns((SentryTraceHeader?)null); - TraceId = SentryId.Empty; - ParentSpanId = SpanId.Empty; + Hub.GetSpan().Returns((ISpan?)null); + + var scope = new Scope(); + Hub.SubstituteConfigureScope(scope); + TraceId = scope.PropagationContext.TraceId; + ParentSpanId = scope.PropagationContext.SpanId; } public SentryStructuredLogger GetSut() => SentryStructuredLogger.Create(Hub, Options, Clock, BatchSize, BatchTimeout); @@ -93,9 +98,9 @@ public void Create_Disabled_CachedDisabledInstance() } [Fact] - public void Log_WithoutTraceHeader_CapturesEnvelope() + public void Log_WithoutActiveSpan_CapturesEnvelope() { - _fixture.WithoutTraceHeader(); + _fixture.WithoutActiveSpan(); _fixture.Options.Experimental.EnableLogs = true; var logger = _fixture.GetSut(); From a36720b7e3250e69191f786c53fa0baeaf7f9218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:30:19 +0200 Subject: [PATCH 2/6] test: complete test coverage --- test/Sentry.Tests/SentryLogTests.cs | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index 3393137b85..3220f50461 100644 --- a/test/Sentry.Tests/SentryLogTests.cs +++ b/test/Sentry.Tests/SentryLogTests.cs @@ -382,6 +382,58 @@ public void WriteTo_Attributes_AsJson() entry => entry.Message.Should().Match("*null*is not supported*ignored*") ); } + + [Fact] + public void GetTraceIdAndSpanId_WithActiveSpan_ReturnAsOut() + { + // Arrange + var span = Substitute.For(); + span.TraceId.Returns(SentryId.Create()); + span.SpanId.Returns(SpanId.Create()); + + var hub = Substitute.For(); + hub.GetSpan().Returns(span); + + // Act + SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + + // Assert + traceId.Should().Be(span.TraceId); + spanId.Should().Be(span.SpanId); + } + + [Fact] + public void GetTraceIdAndSpanId_WithoutActiveSpan_ReturnAsOut() + { + // Arrange + var hub = Substitute.For(); + hub.GetSpan().Returns((ISpan)null); + + var scope = new Scope(); + hub.SubstituteConfigureScope(scope); + + // Act + SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + + // Assert + traceId.Should().Be(scope.PropagationContext.TraceId); + spanId.Should().Be(scope.PropagationContext.SpanId); + } + + [Fact] + public void GetTraceIdAndSpanId_WithoutIds_ReturnAsOut() + { + // Arrange + var hub = Substitute.For(); + hub.GetSpan().Returns((ISpan)null); + + // Act + SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + + // Assert + traceId.Should().Be(SentryId.Empty); + spanId.Should().BeNull(); + } } file static class AssertExtensions From 5b42cad32c0c8a70491ea3449c3b6e6db47d51f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:08:21 +0200 Subject: [PATCH 3/6] fix: only set sentry.trace.parent_span_id when Span was active --- CHANGELOG.md | 1 + src/Sentry/SentryLog.cs | 23 +++++++++++++------ .../SentryStructuredLoggerTests.cs | 2 +- .../SentrySinkTests.Structured.cs | 2 +- test/Sentry.Tests/SentryLogTests.cs | 8 +++---- .../SentryStructuredLoggerTests.cs | 2 +- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cdf7c28d0..241e16d98a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes +- Parent-Span-IDs are no longer sent with Structured Logs when recorded without an active Span ([#4565](https://github.com/getsentry/sentry-dotnet/pull/4565)) - Upload linked PDBs to fix non-IL-stripped symbolication for iOS ([#4527](https://github.com/getsentry/sentry-dotnet/pull/4527)) - In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532)) - Fixed WASM0001 warning when building Blazor WebAssembly projects ([#4519](https://github.com/getsentry/sentry-dotnet/pull/4519)) diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index dfadc729f1..ebce002ccd 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -1,13 +1,19 @@ using Sentry.Extensibility; using Sentry.Infrastructure; +using Sentry.Internal; using Sentry.Protocol; namespace Sentry; /// -/// Represents the Sentry Log protocol. +/// Represents a Sentry Structured Log. /// This API is experimental and it may change in the future. /// +/// +/// Sentry Docs: . +/// Sentry Developer Documentation: . +/// Sentry .NET SDK Docs: . +/// [Experimental(DiagnosticId.ExperimentalFeature)] [DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")] public sealed class SentryLog @@ -260,23 +266,26 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) internal static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId) { - var span = hub.GetSpan(); - if (span is not null) + var activeSpan = hub.GetSpan(); + if (activeSpan is not null) { - traceId = span.TraceId; - spanId = span.SpanId; + traceId = activeSpan.TraceId; + spanId = activeSpan.SpanId; return; } + // set "sentry.trace.parent_span_id" to the ID of the Span that was active when the Log was collected + // do not set "sentry.trace.parent_span_id" if there was no active Span + spanId = null; + var scope = hub.GetScope(); if (scope is not null) { traceId = scope.PropagationContext.TraceId; - spanId = scope.PropagationContext.SpanId; return; } + Debug.Assert(hub is not Hub, "In case of a 'full' Hub, there is always a Scope. Otherwise (disabled) there is no Scope, but this branch should be unreachable."); traceId = SentryId.Empty; - spanId = null; } } diff --git a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs index 127f1b474e..f810fd9d14 100644 --- a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs @@ -140,7 +140,7 @@ public void Log_WithoutActiveSpan_CaptureLog() var log = _fixture.CapturedLogs.Dequeue(); log.TraceId.Should().Be(scope.PropagationContext.TraceId); - log.ParentSpanId.Should().Be(scope.PropagationContext.SpanId); + log.ParentSpanId.Should().BeNull(); } [Fact] diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs index b7cb36b76f..7b98e8181c 100644 --- a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs @@ -112,7 +112,7 @@ public void Emit_StructuredLogging_LogEvent(bool withActiveSpan) log.Parameters[1].Should().BeEquivalentTo(new KeyValuePair("Sequence", "[41, 42, 43]")); log.Parameters[2].Should().BeEquivalentTo(new KeyValuePair("Dictionary", """[("key": "value")]""")); log.Parameters[3].Should().BeEquivalentTo(new KeyValuePair("Structure", """[42, "42"]""")); - log.ParentSpanId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.SpanId : _fixture.Scope.PropagationContext.SpanId); + log.ParentSpanId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.SpanId : null); log.TryGetAttribute("sentry.environment", out object? environment).Should().BeTrue(); environment.Should().Be("test-environment"); diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index 3220f50461..f96638dfe1 100644 --- a/test/Sentry.Tests/SentryLogTests.cs +++ b/test/Sentry.Tests/SentryLogTests.cs @@ -384,7 +384,7 @@ public void WriteTo_Attributes_AsJson() } [Fact] - public void GetTraceIdAndSpanId_WithActiveSpan_ReturnAsOut() + public void GetTraceIdAndSpanId_WithActiveSpan_HasBothTraceIdAndSpanId() { // Arrange var span = Substitute.For(); @@ -403,7 +403,7 @@ public void GetTraceIdAndSpanId_WithActiveSpan_ReturnAsOut() } [Fact] - public void GetTraceIdAndSpanId_WithoutActiveSpan_ReturnAsOut() + public void GetTraceIdAndSpanId_WithoutActiveSpan_HasOnlyTraceIdButNoSpanId() { // Arrange var hub = Substitute.For(); @@ -417,11 +417,11 @@ public void GetTraceIdAndSpanId_WithoutActiveSpan_ReturnAsOut() // Assert traceId.Should().Be(scope.PropagationContext.TraceId); - spanId.Should().Be(scope.PropagationContext.SpanId); + spanId.Should().BeNull(); } [Fact] - public void GetTraceIdAndSpanId_WithoutIds_ReturnAsOut() + public void GetTraceIdAndSpanId_WithoutIds_ShouldBeUnreachable() { // Arrange var hub = Substitute.For(); diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 75836fcfb4..bee10461ff 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -55,7 +55,7 @@ public void WithoutActiveSpan() var scope = new Scope(); Hub.SubstituteConfigureScope(scope); TraceId = scope.PropagationContext.TraceId; - ParentSpanId = scope.PropagationContext.SpanId; + ParentSpanId = null; } public SentryStructuredLogger GetSut() => SentryStructuredLogger.Create(Hub, Options, Clock, BatchSize, BatchTimeout); From 2326d97741195fa1b3d660f3708ab36bf465d6d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:13:38 +0200 Subject: [PATCH 4/6] revert: doc changes --- src/Sentry/SentryLog.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index ebce002ccd..f3061950c4 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -6,14 +6,9 @@ namespace Sentry; /// -/// Represents a Sentry Structured Log. +/// Represents the Sentry Log protocol. /// This API is experimental and it may change in the future. /// -/// -/// Sentry Docs: . -/// Sentry Developer Documentation: . -/// Sentry .NET SDK Docs: . -/// [Experimental(DiagnosticId.ExperimentalFeature)] [DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")] public sealed class SentryLog From a80bd4f6fc527a7e33d15a6af1c44ab510960891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:26:37 +0200 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1379a0fc38..e9cd43f52f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ ### Fixes -- Parent-Span-IDs are no longer sent with Structured Logs when recorded without an active Span ([#4565](https://github.com/getsentry/sentry-dotnet/pull/4565)) - Templates are no longer sent with Structured Logs that have no parameters ([#4544](https://github.com/getsentry/sentry-dotnet/pull/4544)) +- Parent-Span-IDs are no longer sent with Structured Logs when recorded without an active Span ([#4565](https://github.com/getsentry/sentry-dotnet/pull/4565)) - Upload linked PDBs to fix non-IL-stripped symbolication for iOS ([#4527](https://github.com/getsentry/sentry-dotnet/pull/4527)) - In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532)) - Fixed WASM0001 warning when building Blazor WebAssembly projects ([#4519](https://github.com/getsentry/sentry-dotnet/pull/4519)) From 50ee186ac59ed9044bdacb820ff897064575df0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:36:51 +0200 Subject: [PATCH 6/6] post merge change --- src/Sentry/SentryLog.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index 6993406ce2..844d71a778 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -1,5 +1,4 @@ using Sentry.Extensibility; -using Sentry.Infrastructure; using Sentry.Internal; using Sentry.Protocol;