From 2a798a803b85c11dc8feec123a0490961c70d3fe Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 20 Feb 2025 11:05:25 +1300 Subject: [PATCH] Sync Serilog scope properties to Sentry events (#3976) --- CHANGELOG.md | 1 + src/Sentry.Serilog/SentryOptionExtensions.cs | 21 +++++++ src/Sentry.Serilog/SentrySinkExtensions.cs | 8 +++ .../SerilogScopeEventProcessor.cs | 57 +++++++++++++++++++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 5 ++ ...piApprovalTests.Run.DotNet9_0.verified.txt | 5 ++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 5 ++ .../SerilogScopeEventProcessorTests.cs | 32 +++++++++++ 8 files changed, 134 insertions(+) create mode 100644 src/Sentry.Serilog/SentryOptionExtensions.cs create mode 100644 src/Sentry.Serilog/SerilogScopeEventProcessor.cs create mode 100644 test/Sentry.Serilog.Tests/SerilogScopeEventProcessorTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6688a7bc47..4ef14d6c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Serilog scope properties are now sent with Sentry events ([#3976](https://github.com/getsentry/sentry-dotnet/pull/3976)) - The sample seed used for sampling decisions is now propagated, for use in downstream custom trace samplers ([#3951](https://github.com/getsentry/sentry-dotnet/pull/3951)) ### Fixes diff --git a/src/Sentry.Serilog/SentryOptionExtensions.cs b/src/Sentry.Serilog/SentryOptionExtensions.cs new file mode 100644 index 0000000000..94dfbc5212 --- /dev/null +++ b/src/Sentry.Serilog/SentryOptionExtensions.cs @@ -0,0 +1,21 @@ +namespace Sentry.Serilog; + +/// +/// Extensions for to add Serilog specific configuration. +/// +public static class SentryOptionExtensions +{ + /// + /// Ensures Serilog scope properties get applied to Sentry events. If you are not initialising Sentry when + /// configuring the Sentry sink for Serilog then you should call this method in the options callback for whichever + /// Sentry integration you are using to initialise Sentry. + /// + /// + /// + /// + public static T ApplySerilogScopeToEvents(this T options) where T : SentryOptions + { + options.AddEventProcessor(new SerilogScopeEventProcessor(options)); + return options; + } +} diff --git a/src/Sentry.Serilog/SentrySinkExtensions.cs b/src/Sentry.Serilog/SentrySinkExtensions.cs index f023819a41..3eb5b56c3a 100644 --- a/src/Sentry.Serilog/SentrySinkExtensions.cs +++ b/src/Sentry.Serilog/SentrySinkExtensions.cs @@ -326,6 +326,14 @@ internal static void ConfigureSentrySerilogOptions( sentrySerilogOptions.DefaultTags.Add(tag.Key, tag.Value); } } + + // This only works when the SDK is initialized using the LoggerSinkConfiguration extensions. If the SDK is + // initialized using some other integration then the processor will need to be added manually to whichever + // options are used to initialize the SDK. + if (sentrySerilogOptions.InitializeSdk) + { + sentrySerilogOptions.ApplySerilogScopeToEvents(); + } } /// diff --git a/src/Sentry.Serilog/SerilogScopeEventProcessor.cs b/src/Sentry.Serilog/SerilogScopeEventProcessor.cs new file mode 100644 index 0000000000..4b3de83fe0 --- /dev/null +++ b/src/Sentry.Serilog/SerilogScopeEventProcessor.cs @@ -0,0 +1,57 @@ +using Serilog.Context; + +namespace Sentry.Serilog; + +/// +/// Sentry event processor that applies properties from the Serilog scope to Sentry events. +/// +internal class SerilogScopeEventProcessor : ISentryEventProcessor +{ + private readonly SentryOptions _options; + + /// + /// This processor extracts properties from the Serilog context and applies these to Sentry events. + /// + public SerilogScopeEventProcessor(SentryOptions options) + { + _options = options; + _options.LogDebug("Initializing Serilog scope event processor."); + } + + /// + public SentryEvent Process(SentryEvent @event) + { + _options.LogDebug("Running Serilog scope event processor on: Event {0}", @event.EventId); + + // This is a bit of a hack. Serilog doesn't have any hooks that let us inspect the context. We can, however, + // apply the context to a dummy log event and then copy across the properties from that log event to our Sentry + // event. + // See: https://github.com/getsentry/sentry-dotnet/issues/3544#issuecomment-2307884977 + var enricher = LogContext.Clone(); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Error, null, MessageTemplate.Empty, []); + enricher.Enrich(logEvent, new LogEventPropertyFactory()); + foreach (var (key, value) in logEvent.Properties) + { + if (!@event.Tags.ContainsKey(key)) + { + // Potentially we could be doing SetData here instead of SetTag. See DefaultSentryScopeStateProcessor. + @event.SetTag( + key, + value is ScalarValue { Value: string stringValue } + ? stringValue + : value.ToString() + ); + } + } + return @event; + } + + private class LogEventPropertyFactory : ILogEventPropertyFactory + { + public LogEventProperty CreateProperty(string name, object? value, bool destructureObjects = false) + { + var scalarValue = new ScalarValue(value); + return new LogEventProperty(name, scalarValue); + } + } +} diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 893bc8a67e..1455bbc51b 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -1,6 +1,11 @@ [assembly: System.CLSCompliant(true)] namespace Sentry.Serilog { + public static class SentryOptionExtensions + { + public static T ApplySerilogScopeToEvents(this T options) + where T : Sentry.SentryOptions { } + } public class SentrySerilogOptions : Sentry.SentryOptions { public SentrySerilogOptions() { } diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 893bc8a67e..1455bbc51b 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -1,6 +1,11 @@ [assembly: System.CLSCompliant(true)] namespace Sentry.Serilog { + public static class SentryOptionExtensions + { + public static T ApplySerilogScopeToEvents(this T options) + where T : Sentry.SentryOptions { } + } public class SentrySerilogOptions : Sentry.SentryOptions { public SentrySerilogOptions() { } diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 893bc8a67e..1455bbc51b 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -1,6 +1,11 @@ [assembly: System.CLSCompliant(true)] namespace Sentry.Serilog { + public static class SentryOptionExtensions + { + public static T ApplySerilogScopeToEvents(this T options) + where T : Sentry.SentryOptions { } + } public class SentrySerilogOptions : Sentry.SentryOptions { public SentrySerilogOptions() { } diff --git a/test/Sentry.Serilog.Tests/SerilogScopeEventProcessorTests.cs b/test/Sentry.Serilog.Tests/SerilogScopeEventProcessorTests.cs new file mode 100644 index 0000000000..378849dc9f --- /dev/null +++ b/test/Sentry.Serilog.Tests/SerilogScopeEventProcessorTests.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Logging; + +namespace Sentry.Serilog.Tests; + +public class SerilogScopeEventProcessorTests +{ + [Theory] + [InlineData("42", "42")] + [InlineData(42, "42")] + public void Emit_WithException_CreatesEventWithException(object value, string expected) + { + // Arrange + var options = new SentryOptions(); + var sut = new SerilogScopeEventProcessor(options); + + using var log = new LoggerConfiguration().CreateLogger(); + var factory = new LoggerFactory().AddSerilog(log); + var logger = factory.CreateLogger(); + + // Act + SentryEvent evt; + using (logger.BeginScope(new Dictionary { ["Answer"] = value })) + { + evt = new SentryEvent(); + sut.Process(evt); + } + + // Assert + evt.Tags.Should().ContainKey("Answer"); + evt.Tags["Answer"].Should().Be(expected); + } +}