From 0abf66c137b1f84379a63a13e0895cb599d88c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:35:26 +0200 Subject: [PATCH 01/19] feat(logs): add Serilog integration --- samples/Sentry.Samples.Serilog/Program.cs | 2 + src/Sentry.Serilog/LogLevelExtensions.cs | 14 +++ src/Sentry.Serilog/SentrySink.Structured.cs | 108 ++++++++++++++++++ src/Sentry.Serilog/SentrySink.cs | 43 +++---- src/Sentry.Serilog/SentrySinkExtensions.cs | 33 +++++- src/Sentry/SentryLog.cs | 1 + .../SerilogAspNetSentrySdkTestFixture.cs | 12 +- 7 files changed, 186 insertions(+), 27 deletions(-) create mode 100644 src/Sentry.Serilog/SentrySink.Structured.cs diff --git a/samples/Sentry.Samples.Serilog/Program.cs b/samples/Sentry.Samples.Serilog/Program.cs index 59ed09548d..c1822a3714 100644 --- a/samples/Sentry.Samples.Serilog/Program.cs +++ b/samples/Sentry.Samples.Serilog/Program.cs @@ -25,6 +25,8 @@ private static void Main() // Error and higher is sent as event (default is Error) options.MinimumEventLevel = LogEventLevel.Error; options.AttachStacktrace = true; + // send structured logs to Sentry + options.Experimental.EnableLogs = true; // send PII like the username of the user logged in to the device options.SendDefaultPii = true; // Optional Serilog text formatter used to format LogEvent to string. If TextFormatter is set, FormatProvider is ignored. diff --git a/src/Sentry.Serilog/LogLevelExtensions.cs b/src/Sentry.Serilog/LogLevelExtensions.cs index 03a16ea216..3d57f908e8 100644 --- a/src/Sentry.Serilog/LogLevelExtensions.cs +++ b/src/Sentry.Serilog/LogLevelExtensions.cs @@ -42,4 +42,18 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this LogEventLevel level) _ => (BreadcrumbLevel)level }; } + + public static SentryLogLevel ToSentryLogLevel(this LogEventLevel level) + { + return level switch + { + LogEventLevel.Verbose => SentryLogLevel.Debug, + LogEventLevel.Debug => SentryLogLevel.Debug, + LogEventLevel.Information => SentryLogLevel.Info, + LogEventLevel.Warning => SentryLogLevel.Warning, + LogEventLevel.Error => SentryLogLevel.Error, + LogEventLevel.Fatal => SentryLogLevel.Fatal, + _ => (SentryLogLevel)level, + }; + } } diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs new file mode 100644 index 0000000000..490b924bee --- /dev/null +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -0,0 +1,108 @@ +using Serilog.Parsing; +using Sentry.Internal.Extensions; + +namespace Sentry.Serilog; + +internal sealed partial class SentrySink +{ + private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, string? template) + { + var traceHeader = hub.GetTraceHeader() ?? SentryTraceHeader.Empty; + GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes); + + SentryLog log = new(logEvent.Timestamp, traceHeader.TraceId, logEvent.Level.ToSentryLogLevel(), formatted) + { + Template = template, + Parameters = parameters, + ParentSpanId = traceHeader.SpanId, + }; + + var scope = hub.GetScope(); + log.SetDefaultAttributes(_options, scope?.Sdk ?? SdkVersion.Instance); + + foreach (var attribute in attributes) + { + log.SetAttribute(attribute.Key, attribute.Value); + } + + hub.Logger.CaptureLog(log); + } + + private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEvent, out ImmutableArray> parameters, out List> attributes) + { + var propertyTokens = new List(); + foreach (var token in logEvent.MessageTemplate.Tokens) + { + if (token is PropertyToken property) + { + propertyTokens.Add(property); + } + } + + var @params = ImmutableArray.CreateBuilder>(); + attributes = new List>(); + + foreach (var property in logEvent.Properties) + { + if (propertyTokens.Exists(prop => prop.PropertyName == property.Key)) + { + if (TryGetLogEventProperty(property, out var parameter)) + { + @params.Add(parameter); + } + } + else + { + if (TryGetLogEventProperty(property, out var attribute)) + { + attributes.Add(new KeyValuePair($"property.{attribute.Key}", attribute.Value)); + } + } + } + + parameters = @params.DrainToImmutable(); + return; + + static bool TryGetLogEventProperty(KeyValuePair property, out KeyValuePair value) + { + if (property.Value is ScalarValue scalarValue) + { + if (scalarValue.Value is not null) + { + value = new KeyValuePair(property.Key, scalarValue.Value); + return true; + } + } + else if (property.Value is StructureValue structureValue) + { + foreach (var prop in structureValue.Properties) + { + if (LogEventProperty.IsValidName(prop.Name)) + { + if (prop.Value is ScalarValue scalarProperty) + { + if (scalarProperty.Value is not null) + { + value = new KeyValuePair($"{property.Key}.{prop.Name}", scalarProperty.Value); + return true; + } + } + else + { + value = new KeyValuePair($"{property.Key}.{prop.Name}", prop.Value); + return true; + } + } + } + } + else if (!property.Value.IsNull()) + { + value = new KeyValuePair(property.Key, property.Value); + return true; + } + + value = default; + return false; + } + } +} diff --git a/src/Sentry.Serilog/SentrySink.cs b/src/Sentry.Serilog/SentrySink.cs index b2a6671c67..af3ae40a78 100644 --- a/src/Sentry.Serilog/SentrySink.cs +++ b/src/Sentry.Serilog/SentrySink.cs @@ -5,7 +5,7 @@ namespace Sentry.Serilog; /// /// /// -internal sealed class SentrySink : ILogEventSink, IDisposable +internal sealed partial class SentrySink : ILogEventSink, IDisposable { private readonly IDisposable? _sdkDisposable; private readonly SentrySerilogOptions _options; @@ -122,30 +122,33 @@ private void InnerEmit(LogEvent logEvent) } } - if (logEvent.Level < _options.MinimumBreadcrumbLevel) + if (logEvent.Level >= _options.MinimumBreadcrumbLevel) { - return; + Dictionary? data = null; + if (exception != null && !string.IsNullOrWhiteSpace(formatted)) + { + // Exception.Message won't be used as Breadcrumb message + // Avoid losing it by adding as data: + data = new Dictionary + { + { "exception_message", exception.Message } + }; + } + + hub.AddBreadcrumb( + _clock, + string.IsNullOrWhiteSpace(formatted) + ? exception?.Message ?? "" + : formatted, + context, + data: data, + level: logEvent.Level.ToBreadcrumbLevel()); } - Dictionary? data = null; - if (exception != null && !string.IsNullOrWhiteSpace(formatted)) + if (_options.Experimental.EnableLogs) { - // Exception.Message won't be used as Breadcrumb message - // Avoid losing it by adding as data: - data = new Dictionary - { - {"exception_message", exception.Message} - }; + CaptureStructuredLog(hub, logEvent, formatted, template); } - - hub.AddBreadcrumb( - _clock, - string.IsNullOrWhiteSpace(formatted) - ? exception?.Message ?? "" - : formatted, - context, - data: data, - level: logEvent.Level.ToBreadcrumbLevel()); } private static bool IsSentryContext(string context) => diff --git a/src/Sentry.Serilog/SentrySinkExtensions.cs b/src/Sentry.Serilog/SentrySinkExtensions.cs index 924cec9d84..06d80dd83a 100644 --- a/src/Sentry.Serilog/SentrySinkExtensions.cs +++ b/src/Sentry.Serilog/SentrySinkExtensions.cs @@ -17,6 +17,7 @@ public static class SentrySinkExtensions /// Minimum log level to record a breadcrumb. /// The Serilog format provider. /// The Serilog text formatter. + /// Whether to send structured logs. /// Whether to include default Personal Identifiable information. /// Whether to report the as the User affected in the event. /// Gets or sets the name of the server running the application. @@ -50,7 +51,8 @@ public static class SentrySinkExtensions /// "dsn": "https://MY-DSN@sentry.io", /// "minimumBreadcrumbLevel": "Verbose", /// "minimumEventLevel": "Error", - /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}"/// + /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}", + /// "experimentalEnableLogs": true, /// "sendDefaultPii": false, /// "isEnvironmentUser": false, /// "serverName": "MyServerName", @@ -86,6 +88,7 @@ public static LoggerConfiguration Sentry( LogEventLevel? minimumEventLevel = null, IFormatProvider? formatProvider = null, ITextFormatter? textFormatter = null, + bool? experimentalEnableLogs = null, bool? sendDefaultPii = null, bool? isEnvironmentUser = null, string? serverName = null, @@ -111,6 +114,7 @@ public static LoggerConfiguration Sentry( minimumBreadcrumbLevel, formatProvider, textFormatter, + experimentalEnableLogs, sendDefaultPii, isEnvironmentUser, serverName, @@ -143,6 +147,7 @@ public static LoggerConfiguration Sentry( /// Minimum log level to record a breadcrumb. /// The Serilog format provider. /// The Serilog text formatter. + /// Whether to send structured logs. /// /// This sample shows how each item may be set from within a configuration file: /// @@ -157,7 +162,8 @@ public static LoggerConfiguration Sentry( /// "Args": { /// "minimumEventLevel": "Error", /// "minimumBreadcrumbLevel": "Verbose", - /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}"/// + /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}", + /// "experimentalEnableLogs": true /// } /// } /// ] @@ -170,7 +176,8 @@ public static LoggerConfiguration Sentry( LogEventLevel? minimumEventLevel = null, LogEventLevel? minimumBreadcrumbLevel = null, IFormatProvider? formatProvider = null, - ITextFormatter? textFormatter = null + ITextFormatter? textFormatter = null, + bool? experimentalEnableLogs = null ) { return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o, @@ -178,7 +185,8 @@ public static LoggerConfiguration Sentry( minimumEventLevel, minimumBreadcrumbLevel, formatProvider, - textFormatter)); + textFormatter, + experimentalEnableLogs)); } internal static void ConfigureSentrySerilogOptions( @@ -188,6 +196,7 @@ internal static void ConfigureSentrySerilogOptions( LogEventLevel? minimumBreadcrumbLevel = null, IFormatProvider? formatProvider = null, ITextFormatter? textFormatter = null, + bool? experimentalEnableLogs = null, bool? sendDefaultPii = null, bool? isEnvironmentUser = null, string? serverName = null, @@ -232,6 +241,11 @@ internal static void ConfigureSentrySerilogOptions( sentrySerilogOptions.TextFormatter = textFormatter; } + if (experimentalEnableLogs.HasValue) + { + sentrySerilogOptions.Experimental.EnableLogs = experimentalEnableLogs.Value; + } + if (sendDefaultPii.HasValue) { sentrySerilogOptions.SendDefaultPii = sendDefaultPii.Value; @@ -354,7 +368,14 @@ public static LoggerConfiguration Sentry( sdkDisposable = SentrySdk.Init(options); } - var minimumOverall = (LogEventLevel)Math.Min((int)options.MinimumBreadcrumbLevel, (int)options.MinimumEventLevel); - return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable), minimumOverall); + if (options.Experimental.EnableLogs) + { + return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable)); + } + else + { + var minimumOverall = (LogEventLevel)Math.Min((int)options.MinimumBreadcrumbLevel, (int)options.MinimumEventLevel); + return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable), minimumOverall); + } } } diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index b506b9da6c..7e58fec173 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -9,6 +9,7 @@ namespace Sentry; /// This API is experimental and it may change in the future. /// [Experimental(DiagnosticId.ExperimentalFeature)] +[DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")] public sealed class SentryLog { private readonly Dictionary _attributes; diff --git a/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs b/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs index 4510240ceb..94d800be69 100644 --- a/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs +++ b/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs @@ -6,13 +6,23 @@ namespace Sentry.Serilog.Tests; public class SerilogAspNetSentrySdkTestFixture : AspNetSentrySdkTestFixture { protected List Events; + protected List Logs; protected override void ConfigureBuilder(WebHostBuilder builder) { Events = new List(); Configure = options => { - options.SetBeforeSend((@event, _) => { Events.Add(@event); return @event; }); + options.SetBeforeSend((@event, _) => + { + Events.Add(@event); + return @event; + }); + options.Experimental.SetBeforeSendLog(log => + { + Logs.Add(log); + return log; + }); }; ConfigureApp = app => From 7c73bf1edb636851581103c3783c8703ec4b7842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:42:11 +0200 Subject: [PATCH 02/19] test: and fix --- src/Sentry.Serilog/LogLevelExtensions.cs | 2 +- src/Sentry.Serilog/SentrySink.Structured.cs | 57 +++++----- ...piApprovalTests.Run.DotNet8_0.verified.txt | 3 +- ...piApprovalTests.Run.DotNet9_0.verified.txt | 3 +- .../ApiApprovalTests.Run.Net4_8.verified.txt | 3 +- .../AspNetCoreIntegrationTests.cs | 47 ++++++++ ...rationTests.StructuredLogging.verified.txt | 70 ++++++++++++ .../IntegrationTests.verify.cs | 40 +++++++ .../SentrySerilogOptionsTests.cs | 7 ++ .../SentrySerilogSinkExtensionsTests.cs | 6 +- .../SentrySinkTests.Structured.cs | 104 ++++++++++++++++++ test/Sentry.Serilog.Tests/SentrySinkTests.cs | 2 +- .../SerilogAspNetSentrySdkTestFixture.cs | 20 ++-- .../InMemorySentryStructuredLogger.cs | 3 +- test/Sentry.Testing/RecordingTransport.cs | 2 + ...piApprovalTests.Run.DotNet8_0.verified.txt | 1 + ...piApprovalTests.Run.DotNet9_0.verified.txt | 1 + .../ApiApprovalTests.Run.Net4_8.verified.txt | 1 + 18 files changed, 328 insertions(+), 44 deletions(-) create mode 100644 test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt create mode 100644 test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs diff --git a/src/Sentry.Serilog/LogLevelExtensions.cs b/src/Sentry.Serilog/LogLevelExtensions.cs index 3d57f908e8..07960179b2 100644 --- a/src/Sentry.Serilog/LogLevelExtensions.cs +++ b/src/Sentry.Serilog/LogLevelExtensions.cs @@ -47,7 +47,7 @@ public static SentryLogLevel ToSentryLogLevel(this LogEventLevel level) { return level switch { - LogEventLevel.Verbose => SentryLogLevel.Debug, + LogEventLevel.Verbose => SentryLogLevel.Trace, LogEventLevel.Debug => SentryLogLevel.Debug, LogEventLevel.Information => SentryLogLevel.Info, LogEventLevel.Warning => SentryLogLevel.Warning, diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index 490b924bee..654dc09c63 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -5,6 +5,8 @@ namespace Sentry.Serilog; internal sealed partial class SentrySink { + private static readonly SdkVersion Sdk = CreateSdkVersion(); + private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, string? template) { var traceHeader = hub.GetTraceHeader() ?? SentryTraceHeader.Empty; @@ -17,8 +19,7 @@ private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, ParentSpanId = traceHeader.SpanId, }; - var scope = hub.GetScope(); - log.SetDefaultAttributes(_options, scope?.Sdk ?? SdkVersion.Instance); + log.SetDefaultAttributes(_options, Sdk); foreach (var attribute in attributes) { @@ -46,14 +47,14 @@ private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEven { if (propertyTokens.Exists(prop => prop.PropertyName == property.Key)) { - if (TryGetLogEventProperty(property, out var parameter)) + foreach (var parameter in GetLogEventProperties(property)) { @params.Add(parameter); } } else { - if (TryGetLogEventProperty(property, out var attribute)) + foreach (var attribute in GetLogEventProperties(property)) { attributes.Add(new KeyValuePair($"property.{attribute.Key}", attribute.Value)); } @@ -63,14 +64,27 @@ private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEven parameters = @params.DrainToImmutable(); return; - static bool TryGetLogEventProperty(KeyValuePair property, out KeyValuePair value) + static IEnumerable> GetLogEventProperties(KeyValuePair property) { if (property.Value is ScalarValue scalarValue) { if (scalarValue.Value is not null) { - value = new KeyValuePair(property.Key, scalarValue.Value); - return true; + yield return new KeyValuePair(property.Key, scalarValue.Value); + } + } + else if (property.Value is SequenceValue sequenceValue) + { + if (sequenceValue.Elements.Count != 0) + { + yield return new KeyValuePair(property.Key, sequenceValue.ToString()); + } + } + else if (property.Value is DictionaryValue dictionaryValue) + { + if (dictionaryValue.Elements.Count != 0) + { + yield return new KeyValuePair(property.Key, dictionaryValue.ToString()); } } else if (property.Value is StructureValue structureValue) @@ -79,30 +93,23 @@ static bool TryGetLogEventProperty(KeyValuePair p { if (LogEventProperty.IsValidName(prop.Name)) { - if (prop.Value is ScalarValue scalarProperty) - { - if (scalarProperty.Value is not null) - { - value = new KeyValuePair($"{property.Key}.{prop.Name}", scalarProperty.Value); - return true; - } - } - else - { - value = new KeyValuePair($"{property.Key}.{prop.Name}", prop.Value); - return true; - } + yield return new KeyValuePair($"{property.Key}.{prop.Name}", prop.Value.ToString()); } } } else if (!property.Value.IsNull()) { - value = new KeyValuePair(property.Key, property.Value); - return true; + yield return new KeyValuePair(property.Key, property.Value); } - - value = default; - return false; } } + + private static SdkVersion CreateSdkVersion() + { + return new SdkVersion + { + Name = SdkName, + Version = NameAndVersion.Version, + }; + } } 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 1455bbc51b..86091444f4 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -21,7 +21,7 @@ namespace Serilog public static class SentrySinkExtensions { public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action configureOptions) { } - public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { } + public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, bool? experimentalEnableLogs = default) { } public static Serilog.LoggerConfiguration Sentry( this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, string dsn, @@ -29,6 +29,7 @@ namespace Serilog Serilog.Events.LogEventLevel? minimumEventLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, + bool? experimentalEnableLogs = default, bool? sendDefaultPii = default, bool? isEnvironmentUser = default, string? serverName = null, 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 1455bbc51b..86091444f4 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -21,7 +21,7 @@ namespace Serilog public static class SentrySinkExtensions { public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action configureOptions) { } - public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { } + public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, bool? experimentalEnableLogs = default) { } public static Serilog.LoggerConfiguration Sentry( this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, string dsn, @@ -29,6 +29,7 @@ namespace Serilog Serilog.Events.LogEventLevel? minimumEventLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, + bool? experimentalEnableLogs = default, bool? sendDefaultPii = default, bool? isEnvironmentUser = default, string? serverName = null, 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 1455bbc51b..86091444f4 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -21,7 +21,7 @@ namespace Serilog public static class SentrySinkExtensions { public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action configureOptions) { } - public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { } + public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, bool? experimentalEnableLogs = default) { } public static Serilog.LoggerConfiguration Sentry( this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, string dsn, @@ -29,6 +29,7 @@ namespace Serilog Serilog.Events.LogEventLevel? minimumEventLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, + bool? experimentalEnableLogs = default, bool? sendDefaultPii = default, bool? isEnvironmentUser = default, string? serverName = null, diff --git a/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs b/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs index 8088548272..5046d34ac8 100644 --- a/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs +++ b/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs @@ -1,4 +1,6 @@ #if NET6_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Sentry.AspNetCore.TestUtils; namespace Sentry.Serilog.Tests; @@ -22,5 +24,50 @@ public async Task UnhandledException_MarkedAsUnhandled() Assert.Contains(Events, e => e.Logger == "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware"); Assert.Collection(Events, @event => Assert.Collection(@event.SentryExceptions, x => Assert.False(x.Mechanism?.Handled))); } + + [Fact] + public async Task StructuredLogging_Disabled() + { + Assert.False(ExperimentalEnableLogs); + + var handler = new RequestHandler + { + Path = "/log", + Handler = context => + { + context.RequestServices.GetRequiredService>().LogInformation("Hello, World!"); + return Task.CompletedTask; + } + }; + + Handlers = new[] { handler }; + Build(); + await HttpClient.GetAsync(handler.Path); + + Assert.Empty(Logs); + } + + [Fact] + public async Task StructuredLogging_Enabled() + { + ExperimentalEnableLogs = true; + + var handler = new RequestHandler + { + Path = "/log", + Handler = context => + { + context.RequestServices.GetRequiredService>().LogInformation("Hello, World!"); + return Task.CompletedTask; + } + }; + + Handlers = new[] { handler }; + Build(); + await HttpClient.GetAsync(handler.Path); + + Assert.NotEmpty(Logs); + Assert.Contains(Logs, log => log.Level == SentryLogLevel.Info && log.Message == "Hello, World!"); + } } #endif diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt b/test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt new file mode 100644 index 0000000000..2eb81f0805 --- /dev/null +++ b/test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt @@ -0,0 +1,70 @@ +{ + envelopes: [ + { + Header: { + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + content_type: application/vnd.sentry.items.log+json, + item_count: 4, + type: log + }, + Payload: { + Source: { + Length: 4 + } + } + } + ] + } + ], + logs: [ + [ + { + Level: Debug, + Message: Debug message with a Scalar property: 42, + Template: Debug message with a Scalar property: {Scalar}, + Parameters: [ + { + Scalar: 42 + } + ] + }, + { + Level: Info, + Message: Information message with a Sequence property: [41, 42, 43], + Template: Information message with a Sequence property: {Sequence}, + Parameters: [ + { + Sequence: [41, 42, 43] + } + ] + }, + { + Level: Warning, + Message: Warning message with a Dictionary property: [("key": "value")], + Template: Warning message with a Dictionary property: {Dictionary}, + Parameters: [ + { + Dictionary: [("key": "value")] + } + ] + }, + { + Level: Error, + Message: Error message with a Structure property: [42, "42"], + Template: Error message with a Structure property: {Structure}, + Parameters: [ + { + Structure: [42, "42"] + } + ] + } + ] + ], + diagnostics: [] +} \ No newline at end of file diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs b/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs index 10d7b538bc..cde70dd56f 100644 --- a/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs +++ b/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs @@ -100,5 +100,45 @@ public Task LoggingInsideTheContextOfLogging() }) .IgnoreStandardSentryMembers(); } + + [Fact] + public Task StructuredLogging() + { + var transport = new RecordingTransport(); + + var configuration = new LoggerConfiguration(); + configuration.MinimumLevel.Debug(); + var diagnosticLogger = new InMemoryDiagnosticLogger(); + configuration.WriteTo.Sentry( + _ => + { + _.MinimumEventLevel = (LogEventLevel)int.MaxValue; + _.Experimental.EnableLogs = true; + _.Transport = transport; + _.DiagnosticLogger = diagnosticLogger; + _.Dsn = ValidDsn; + _.Debug = true; + _.Environment = "test-environment"; + _.Release = "test-release"; + }); + + Log.Logger = configuration.CreateLogger(); + + Log.Debug("Debug message with a Scalar property: {Scalar}", 42); + Log.Information("Information message with a Sequence property: {Sequence}", new object[] { new int[] { 41, 42, 43} }); + Log.Warning("Warning message with a Dictionary property: {Dictionary}", new Dictionary { {"key", "value"} }); + Log.Error("Error message with a Structure property: {Structure}", (Number: 42, Text: "42")); + + Log.CloseAndFlush(); + + var envelopes = transport.Envelopes; + var logs = transport.Payloads.OfType() + .Select(payload => payload.Source) + .OfType() + .Select(log => log.Items.ToArray()); + var diagnostics = diagnosticLogger.Entries.Where(_ => _.Level >= SentryLevel.Warning); + return Verify(new { envelopes, logs, diagnostics }) + .IgnoreStandardSentryMembers(); + } } #endif diff --git a/test/Sentry.Serilog.Tests/SentrySerilogOptionsTests.cs b/test/Sentry.Serilog.Tests/SentrySerilogOptionsTests.cs index f2a51b3bc7..fe0a3e3255 100644 --- a/test/Sentry.Serilog.Tests/SentrySerilogOptionsTests.cs +++ b/test/Sentry.Serilog.Tests/SentrySerilogOptionsTests.cs @@ -15,4 +15,11 @@ public void Ctor_MinimumEventLevel_Error() var sut = new SentrySerilogOptions(); Assert.Equal(LogEventLevel.Error, sut.MinimumEventLevel); } + + [Fact] + public void Ctor_EnableLogs_False() + { + var sut = new SentrySerilogOptions(); + Assert.False(sut.Experimental.EnableLogs); + } } diff --git a/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs b/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs index 57f5fcf9a5..907697ea9f 100644 --- a/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs +++ b/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs @@ -28,6 +28,7 @@ private class Fixture public bool InitializeSdk { get; } = false; public LogEventLevel MinimumEventLevel { get; } = LogEventLevel.Verbose; public LogEventLevel MinimumBreadcrumbLevel { get; } = LogEventLevel.Fatal; + public bool ExperimentalEnableLogs { get; } = true; public static SentrySerilogOptions GetSut() => new(); } @@ -92,7 +93,7 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan var sut = Fixture.GetSut(); SentrySinkExtensions.ConfigureSentrySerilogOptions(sut, _fixture.Dsn, _fixture.MinimumEventLevel, - _fixture.MinimumBreadcrumbLevel, null, null, _fixture.SendDefaultPii, + _fixture.MinimumBreadcrumbLevel, null, null, _fixture.ExperimentalEnableLogs, _fixture.SendDefaultPii, _fixture.IsEnvironmentUser, _fixture.ServerName, _fixture.AttachStackTrace, _fixture.MaxBreadcrumbs, _fixture.SampleRate, _fixture.Release, _fixture.Environment, _fixture.MaxQueueItems, _fixture.ShutdownTimeout, _fixture.DecompressionMethods, _fixture.RequestBodyCompressionLevel, @@ -100,6 +101,7 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan _fixture.ReportAssembliesMode, _fixture.DeduplicateMode); // Compare individual properties + Assert.Equal(_fixture.ExperimentalEnableLogs, sut.Experimental.EnableLogs); Assert.Equal(_fixture.SendDefaultPii, sut.SendDefaultPii); Assert.Equal(_fixture.IsEnvironmentUser, sut.IsEnvironmentUser); Assert.Equal(_fixture.ServerName, sut.ServerName); @@ -108,7 +110,7 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan Assert.Equal(_fixture.SampleRate, sut.SampleRate); Assert.Equal(_fixture.Release, sut.Release); Assert.Equal(_fixture.Environment, sut.Environment); - Assert.Equal(_fixture.Dsn, sut.Dsn!); + Assert.Equal(_fixture.Dsn, sut.Dsn); Assert.Equal(_fixture.MaxQueueItems, sut.MaxQueueItems); Assert.Equal(_fixture.ShutdownTimeout, sut.ShutdownTimeout); Assert.Equal(_fixture.DecompressionMethods, sut.DecompressionMethods); diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs new file mode 100644 index 0000000000..9a1c6bbeee --- /dev/null +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs @@ -0,0 +1,104 @@ +#nullable enable + +namespace Sentry.Serilog.Tests; + +public partial class SentrySinkTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Emit_StructuredLogging_IsEnabled(bool isEnabled) + { + InMemorySentryStructuredLogger capturer = new(); + _fixture.Hub.Logger.Returns(capturer); + _fixture.Options.Experimental.EnableLogs = isEnabled; + + var sut = _fixture.GetSut(); + var logger = new LoggerConfiguration().WriteTo.Sink(sut).MinimumLevel.Verbose().CreateLogger(); + + logger.Write(LogEventLevel.Information, "Message"); + + capturer.Logs.Should().HaveCount(isEnabled ? 1 : 0); + } + + [Theory] + [InlineData(LogEventLevel.Verbose, SentryLogLevel.Trace)] + [InlineData(LogEventLevel.Debug, SentryLogLevel.Debug)] + [InlineData(LogEventLevel.Information, SentryLogLevel.Info)] + [InlineData(LogEventLevel.Warning, SentryLogLevel.Warning)] + [InlineData(LogEventLevel.Error, SentryLogLevel.Error)] + [InlineData(LogEventLevel.Fatal, SentryLogLevel.Fatal)] + public void Emit_StructuredLogging_LogLevel(LogEventLevel level, SentryLogLevel expected) + { + InMemorySentryStructuredLogger capturer = new(); + _fixture.Hub.Logger.Returns(capturer); + _fixture.Options.Experimental.EnableLogs = true; + + var sut = _fixture.GetSut(); + var logger = new LoggerConfiguration().WriteTo.Sink(sut).MinimumLevel.Verbose().CreateLogger(); + + logger.Write(level, "Message"); + + capturer.Logs.Should().ContainSingle().Which.Level.Should().Be(expected); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Emit_StructuredLogging_LogEvent(bool withTraceHeader) + { + InMemorySentryStructuredLogger capturer = new(); + _fixture.Hub.Logger.Returns(capturer); + _fixture.Options.Experimental.EnableLogs = true; + _fixture.Options.Environment = "test-environment"; + _fixture.Options.Release = "test-release"; + + var traceHeader = new SentryTraceHeader(SentryId.Create(), SpanId.Create(), null); + _fixture.Hub.GetTraceHeader().Returns(withTraceHeader ? traceHeader : null); + + var sut = _fixture.GetSut(); + var logger = new LoggerConfiguration() + .WriteTo.Sink(sut) + .MinimumLevel.Verbose() + .Enrich.WithProperty("Scalar-Property", 42) + .Enrich.WithProperty("Sequence-Property", new[] { 41, 42, 43 }) + .Enrich.WithProperty("Dictionary-Property", new Dictionary { {"key", "value"} }) + .Enrich.WithProperty("Structure-Property", (Number: 42, Text: "42")) + .CreateLogger(); + + logger.Write(LogEventLevel.Information, + "Message with Scalar property {Scalar}, Sequence property: {Sequence}, Dictionary property: {Dictionary}, and Structure property: {Structure}.", + 42, new[] { 41, 42, 43 }, new Dictionary { {"key", "value"} }, (Number: 42, Text: "42")); + + var log = capturer.Logs.Should().ContainSingle().Which; + log.Timestamp.Should().BeOnOrBefore(DateTimeOffset.Now); + log.TraceId.Should().Be(withTraceHeader ? traceHeader.TraceId : SentryId.Empty); + log.Level.Should().Be(SentryLogLevel.Info); + log.Message.Should().Be("""Message with Scalar property 42, Sequence property: [41, 42, 43], Dictionary property: [("key": "value")], and Structure property: [42, "42"]."""); + log.Template.Should().Be("Message with Scalar property {Scalar}, Sequence property: {Sequence}, Dictionary property: {Dictionary}, and Structure property: {Structure}."); + log.Parameters.Should().HaveCount(4); + log.Parameters[0].Should().BeEquivalentTo(new KeyValuePair("Scalar", 42)); + 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(withTraceHeader ? traceHeader.SpanId : SpanId.Empty); + + log.TryGetAttribute("sentry.environment", out object? environment).Should().BeTrue(); + environment.Should().Be("test-environment"); + log.TryGetAttribute("sentry.release", out object? release).Should().BeTrue(); + release.Should().Be("test-release"); + log.TryGetAttribute("sentry.sdk.name", out object? sdkName).Should().BeTrue(); + sdkName.Should().Be(SentrySink.SdkName); + log.TryGetAttribute("sentry.sdk.version", out object? sdkVersion).Should().BeTrue(); + sdkVersion.Should().Be(SentrySink.NameAndVersion.Version); + + log.TryGetAttribute("property.Scalar-Property", out object? scalar).Should().BeTrue(); + scalar.Should().Be(42); + log.TryGetAttribute("property.Sequence-Property", out object? sequence).Should().BeTrue(); + sequence.Should().Be("[41, 42, 43]"); + log.TryGetAttribute("property.Dictionary-Property", out object? dictionary).Should().BeTrue(); + dictionary.Should().Be("""[("key": "value")]"""); + log.TryGetAttribute("property.Structure-Property", out object? structure).Should().BeTrue(); + structure.Should().Be("""[42, "42"]"""); + } +} diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.cs index ce1011aa2b..2fa7000d7c 100644 --- a/test/Sentry.Serilog.Tests/SentrySinkTests.cs +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.cs @@ -1,6 +1,6 @@ namespace Sentry.Serilog.Tests; -public class SentrySinkTests +public partial class SentrySinkTests { private class Fixture { diff --git a/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs b/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs index 94d800be69..83de6c6a0f 100644 --- a/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs +++ b/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs @@ -8,21 +8,19 @@ public class SerilogAspNetSentrySdkTestFixture : AspNetSentrySdkTestFixture protected List Events; protected List Logs; + protected bool ExperimentalEnableLogs { get; set; } = false; + protected override void ConfigureBuilder(WebHostBuilder builder) { Events = new List(); + Logs = new List(); + Configure = options => { - options.SetBeforeSend((@event, _) => - { - Events.Add(@event); - return @event; - }); - options.Experimental.SetBeforeSendLog(log => - { - Logs.Add(log); - return log; - }); + options.SetBeforeSend((@event, _) => { Events.Add(@event); return @event; }); + + options.Experimental.EnableLogs = ExperimentalEnableLogs; + options.Experimental.SetBeforeSendLog(log => { Logs.Add(log); return log; }); }; ConfigureApp = app => @@ -37,7 +35,7 @@ protected override void ConfigureBuilder(WebHostBuilder builder) builder.ConfigureLogging(loggingBuilder => { var logger = new LoggerConfiguration() - .WriteTo.Sentry(ValidDsn) + .WriteTo.Sentry(ValidDsn, experimentalEnableLogs: ExperimentalEnableLogs) .CreateLogger(); loggingBuilder.AddSerilog(logger); }); diff --git a/test/Sentry.Testing/InMemorySentryStructuredLogger.cs b/test/Sentry.Testing/InMemorySentryStructuredLogger.cs index 440b83cdc7..0dfde97564 100644 --- a/test/Sentry.Testing/InMemorySentryStructuredLogger.cs +++ b/test/Sentry.Testing/InMemorySentryStructuredLogger.cs @@ -5,6 +5,7 @@ namespace Sentry.Testing; public sealed class InMemorySentryStructuredLogger : SentryStructuredLogger { public List Entries { get; } = new(); + public List Logs { get; } = new(); /// private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action? configureLog) @@ -15,7 +16,7 @@ private protected override void CaptureLog(SentryLogLevel level, string template /// protected internal override void CaptureLog(SentryLog log) { - throw new NotSupportedException(); + Logs.Add(log); } /// diff --git a/test/Sentry.Testing/RecordingTransport.cs b/test/Sentry.Testing/RecordingTransport.cs index 386be50b9b..ba0566a88c 100644 --- a/test/Sentry.Testing/RecordingTransport.cs +++ b/test/Sentry.Testing/RecordingTransport.cs @@ -1,5 +1,7 @@ using ISerializable = Sentry.Protocol.Envelopes.ISerializable; +namespace Sentry.Testing; + public class RecordingTransport : ITransport { private List _envelopes = new(); diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index b94a74dc34..f1b176be0b 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -613,6 +613,7 @@ namespace Sentry Fatal = 4, } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + [System.Diagnostics.DebuggerDisplay("SentryLog \\{ Level = {Level}, Message = \'{Message}\' \\}")] [System.Runtime.CompilerServices.RequiredMember] public sealed class SentryLog { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index b94a74dc34..f1b176be0b 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -613,6 +613,7 @@ namespace Sentry Fatal = 4, } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + [System.Diagnostics.DebuggerDisplay("SentryLog \\{ Level = {Level}, Message = \'{Message}\' \\}")] [System.Runtime.CompilerServices.RequiredMember] public sealed class SentryLog { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index aa006ffe82..85df793bcd 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -599,6 +599,7 @@ namespace Sentry [System.Runtime.Serialization.EnumMember(Value="fatal")] Fatal = 4, } + [System.Diagnostics.DebuggerDisplay("SentryLog \\{ Level = {Level}, Message = \'{Message}\' \\}")] public sealed class SentryLog { public Sentry.SentryLogLevel Level { get; init; } From cca7f576c3bf947dff26b811e30bc9b3f66b56fd Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 21 Aug 2025 16:59:07 +0000 Subject: [PATCH 03/19] Format code --- src/Sentry.Serilog/SentrySink.Structured.cs | 2 +- test/Sentry.Serilog.Tests/IntegrationTests.verify.cs | 4 ++-- test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index 654dc09c63..ad351b1a75 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -1,5 +1,5 @@ -using Serilog.Parsing; using Sentry.Internal.Extensions; +using Serilog.Parsing; namespace Sentry.Serilog; diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs b/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs index cde70dd56f..aab8e7dd17 100644 --- a/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs +++ b/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs @@ -125,8 +125,8 @@ public Task StructuredLogging() Log.Logger = configuration.CreateLogger(); Log.Debug("Debug message with a Scalar property: {Scalar}", 42); - Log.Information("Information message with a Sequence property: {Sequence}", new object[] { new int[] { 41, 42, 43} }); - Log.Warning("Warning message with a Dictionary property: {Dictionary}", new Dictionary { {"key", "value"} }); + Log.Information("Information message with a Sequence property: {Sequence}", new object[] { new int[] { 41, 42, 43 } }); + Log.Warning("Warning message with a Dictionary property: {Dictionary}", new Dictionary { { "key", "value" } }); Log.Error("Error message with a Structure property: {Structure}", (Number: 42, Text: "42")); Log.CloseAndFlush(); diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs index 9a1c6bbeee..b9f4f6c567 100644 --- a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs @@ -62,13 +62,13 @@ public void Emit_StructuredLogging_LogEvent(bool withTraceHeader) .MinimumLevel.Verbose() .Enrich.WithProperty("Scalar-Property", 42) .Enrich.WithProperty("Sequence-Property", new[] { 41, 42, 43 }) - .Enrich.WithProperty("Dictionary-Property", new Dictionary { {"key", "value"} }) + .Enrich.WithProperty("Dictionary-Property", new Dictionary { { "key", "value" } }) .Enrich.WithProperty("Structure-Property", (Number: 42, Text: "42")) .CreateLogger(); logger.Write(LogEventLevel.Information, "Message with Scalar property {Scalar}, Sequence property: {Sequence}, Dictionary property: {Dictionary}, and Structure property: {Structure}.", - 42, new[] { 41, 42, 43 }, new Dictionary { {"key", "value"} }, (Number: 42, Text: "42")); + 42, new[] { 41, 42, 43 }, new Dictionary { { "key", "value" } }, (Number: 42, Text: "42")); var log = capturer.Logs.Should().ContainSingle().Which; log.Timestamp.Should().BeOnOrBefore(DateTimeOffset.Now); From 922185b59acba0a1e140a78e76e55ada2f18ebfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 21 Aug 2025 20:42:31 +0200 Subject: [PATCH 04/19] docs: update CHANGELOG.md --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed287feb39..b6ac799de5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Experimental _Structured Logs_: + - Add integration for `Serilog` ([#4462](https://github.com/getsentry/sentry-dotnet/pull/4462)) - Shorten the `key` names of `Microsoft.Extensions.Logging` attributes ([#4450](https://github.com/getsentry/sentry-dotnet/pull/4450)) ### Fixes @@ -17,8 +18,8 @@ ### Dependencies - Reapply "Bump Cocoa SDK from v8.39.0 to v8.46.0 (#4103)" ([#4442](https://github.com/getsentry/sentry-dotnet/pull/4442)) - - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8460) - - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.39.0...8.46.0) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8460) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.39.0...8.46.0) - Bump Native SDK from v0.9.1 to v0.10.0 ([#4436](https://github.com/getsentry/sentry-dotnet/pull/4436)) - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0100) - [diff](https://github.com/getsentry/sentry-native/compare/0.9.1...0.10.0) From 962e435cf011903b45de2322084a848143f4e0ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 21 Aug 2025 21:27:00 +0200 Subject: [PATCH 05/19] test: flush structured logger --- test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs | 2 ++ test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs b/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs index 5046d34ac8..6c99cedcf4 100644 --- a/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs +++ b/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs @@ -43,6 +43,7 @@ public async Task StructuredLogging_Disabled() Handlers = new[] { handler }; Build(); await HttpClient.GetAsync(handler.Path); + ServiceProvider.GetRequiredService().Logger.Flush(); Assert.Empty(Logs); } @@ -65,6 +66,7 @@ public async Task StructuredLogging_Enabled() Handlers = new[] { handler }; Build(); await HttpClient.GetAsync(handler.Path); + ServiceProvider.GetRequiredService().Logger.Flush(); Assert.NotEmpty(Logs); Assert.Contains(Logs, log => log.Level == SentryLogLevel.Info && log.Message == "Hello, World!"); diff --git a/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs b/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs index 83de6c6a0f..b7b1a6d764 100644 --- a/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs +++ b/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs @@ -8,7 +8,7 @@ public class SerilogAspNetSentrySdkTestFixture : AspNetSentrySdkTestFixture protected List Events; protected List Logs; - protected bool ExperimentalEnableLogs { get; set; } = false; + protected bool ExperimentalEnableLogs { get; set; } protected override void ConfigureBuilder(WebHostBuilder builder) { From 9cec5f8b29d50be7b0365e0c7291912f894faf68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:15:34 +0200 Subject: [PATCH 06/19] perf: change lookup from List to HashSet --- src/Sentry.Serilog/SentrySink.Structured.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index ad351b1a75..3022c7a3f3 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -31,12 +31,12 @@ private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEvent, out ImmutableArray> parameters, out List> attributes) { - var propertyTokens = new List(); + var propertyNames = new HashSet(); foreach (var token in logEvent.MessageTemplate.Tokens) { if (token is PropertyToken property) { - propertyTokens.Add(property); + propertyNames.Add(property.PropertyName); } } @@ -45,7 +45,7 @@ private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEven foreach (var property in logEvent.Properties) { - if (propertyTokens.Exists(prop => prop.PropertyName == property.Key)) + if (propertyNames.Contains(property.Key)) { foreach (var parameter in GetLogEventProperties(property)) { From cfe1703588aab0dacde3eb2718e2a7d4821af551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:51:30 +0200 Subject: [PATCH 07/19] ref(logs): reorder parameters --- src/Sentry.Serilog/SentrySinkExtensions.cs | 32 +++++++++---------- .../SentrySerilogOptionsTests.cs | 7 ---- .../SentrySerilogSinkExtensionsTests.cs | 6 ++-- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/Sentry.Serilog/SentrySinkExtensions.cs b/src/Sentry.Serilog/SentrySinkExtensions.cs index 06d80dd83a..4597232554 100644 --- a/src/Sentry.Serilog/SentrySinkExtensions.cs +++ b/src/Sentry.Serilog/SentrySinkExtensions.cs @@ -13,11 +13,10 @@ public static class SentrySinkExtensions /// /// The logger configuration . /// The Sentry DSN (required). - /// Minimum log level to send an event. /// Minimum log level to record a breadcrumb. + /// Minimum log level to send an event. /// The Serilog format provider. /// The Serilog text formatter. - /// Whether to send structured logs. /// Whether to include default Personal Identifiable information. /// Whether to report the as the User affected in the event. /// Gets or sets the name of the server running the application. @@ -36,6 +35,7 @@ public static class SentrySinkExtensions /// What mode to use for reporting referenced assemblies in each event sent to sentry. Defaults to /// What modes to use for event automatic de-duplication. /// Default tags to add to all events. + /// Whether to send structured logs. /// /// This sample shows how each item may be set from within a configuration file: /// @@ -52,7 +52,6 @@ public static class SentrySinkExtensions /// "minimumBreadcrumbLevel": "Verbose", /// "minimumEventLevel": "Error", /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}", - /// "experimentalEnableLogs": true, /// "sendDefaultPii": false, /// "isEnvironmentUser": false, /// "serverName": "MyServerName", @@ -73,7 +72,8 @@ public static class SentrySinkExtensions /// "defaultTags": { /// "key-1", "value-1", /// "key-2", "value-2" - /// } + /// }, + /// "experimentalEnableLogs": true /// } /// } /// ] @@ -88,7 +88,6 @@ public static LoggerConfiguration Sentry( LogEventLevel? minimumEventLevel = null, IFormatProvider? formatProvider = null, ITextFormatter? textFormatter = null, - bool? experimentalEnableLogs = null, bool? sendDefaultPii = null, bool? isEnvironmentUser = null, string? serverName = null, @@ -106,7 +105,8 @@ public static LoggerConfiguration Sentry( SentryLevel? diagnosticLevel = null, ReportAssembliesMode? reportAssembliesMode = null, DeduplicateMode? deduplicateMode = null, - Dictionary? defaultTags = null) + Dictionary? defaultTags = null, + bool? experimentalEnableLogs = null) { return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o, dsn, @@ -114,7 +114,6 @@ public static LoggerConfiguration Sentry( minimumBreadcrumbLevel, formatProvider, textFormatter, - experimentalEnableLogs, sendDefaultPii, isEnvironmentUser, serverName, @@ -132,7 +131,8 @@ public static LoggerConfiguration Sentry( diagnosticLevel, reportAssembliesMode, deduplicateMode, - defaultTags)); + defaultTags, + experimentalEnableLogs)); } /// @@ -186,7 +186,7 @@ public static LoggerConfiguration Sentry( minimumBreadcrumbLevel, formatProvider, textFormatter, - experimentalEnableLogs)); + experimentalEnableLogs: experimentalEnableLogs)); } internal static void ConfigureSentrySerilogOptions( @@ -196,7 +196,6 @@ internal static void ConfigureSentrySerilogOptions( LogEventLevel? minimumBreadcrumbLevel = null, IFormatProvider? formatProvider = null, ITextFormatter? textFormatter = null, - bool? experimentalEnableLogs = null, bool? sendDefaultPii = null, bool? isEnvironmentUser = null, string? serverName = null, @@ -214,7 +213,8 @@ internal static void ConfigureSentrySerilogOptions( SentryLevel? diagnosticLevel = null, ReportAssembliesMode? reportAssembliesMode = null, DeduplicateMode? deduplicateMode = null, - Dictionary? defaultTags = null) + Dictionary? defaultTags = null, + bool? experimentalEnableLogs = null) { if (dsn is not null) { @@ -241,11 +241,6 @@ internal static void ConfigureSentrySerilogOptions( sentrySerilogOptions.TextFormatter = textFormatter; } - if (experimentalEnableLogs.HasValue) - { - sentrySerilogOptions.Experimental.EnableLogs = experimentalEnableLogs.Value; - } - if (sendDefaultPii.HasValue) { sentrySerilogOptions.SendDefaultPii = sendDefaultPii.Value; @@ -331,6 +326,11 @@ internal static void ConfigureSentrySerilogOptions( sentrySerilogOptions.DeduplicateMode = deduplicateMode.Value; } + if (experimentalEnableLogs.HasValue) + { + sentrySerilogOptions.Experimental.EnableLogs = experimentalEnableLogs.Value; + } + // Serilog-specific items sentrySerilogOptions.InitializeSdk = dsn is not null; // Inferred from the Sentry overload that is used if (defaultTags?.Count > 0) diff --git a/test/Sentry.Serilog.Tests/SentrySerilogOptionsTests.cs b/test/Sentry.Serilog.Tests/SentrySerilogOptionsTests.cs index fe0a3e3255..f2a51b3bc7 100644 --- a/test/Sentry.Serilog.Tests/SentrySerilogOptionsTests.cs +++ b/test/Sentry.Serilog.Tests/SentrySerilogOptionsTests.cs @@ -15,11 +15,4 @@ public void Ctor_MinimumEventLevel_Error() var sut = new SentrySerilogOptions(); Assert.Equal(LogEventLevel.Error, sut.MinimumEventLevel); } - - [Fact] - public void Ctor_EnableLogs_False() - { - var sut = new SentrySerilogOptions(); - Assert.False(sut.Experimental.EnableLogs); - } } diff --git a/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs b/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs index 907697ea9f..c0cda5c45a 100644 --- a/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs +++ b/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs @@ -93,15 +93,14 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan var sut = Fixture.GetSut(); SentrySinkExtensions.ConfigureSentrySerilogOptions(sut, _fixture.Dsn, _fixture.MinimumEventLevel, - _fixture.MinimumBreadcrumbLevel, null, null, _fixture.ExperimentalEnableLogs, _fixture.SendDefaultPii, + _fixture.MinimumBreadcrumbLevel, null, null, _fixture.SendDefaultPii, _fixture.IsEnvironmentUser, _fixture.ServerName, _fixture.AttachStackTrace, _fixture.MaxBreadcrumbs, _fixture.SampleRate, _fixture.Release, _fixture.Environment, _fixture.MaxQueueItems, _fixture.ShutdownTimeout, _fixture.DecompressionMethods, _fixture.RequestBodyCompressionLevel, _fixture.RequestBodyCompressionBuffered, _fixture.Debug, _fixture.DiagnosticLevel, - _fixture.ReportAssembliesMode, _fixture.DeduplicateMode); + _fixture.ReportAssembliesMode, _fixture.DeduplicateMode, null, _fixture.ExperimentalEnableLogs); // Compare individual properties - Assert.Equal(_fixture.ExperimentalEnableLogs, sut.Experimental.EnableLogs); Assert.Equal(_fixture.SendDefaultPii, sut.SendDefaultPii); Assert.Equal(_fixture.IsEnvironmentUser, sut.IsEnvironmentUser); Assert.Equal(_fixture.ServerName, sut.ServerName); @@ -120,6 +119,7 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan Assert.Equal(_fixture.DiagnosticLevel, sut.DiagnosticLevel); Assert.Equal(_fixture.ReportAssembliesMode, sut.ReportAssembliesMode); Assert.Equal(_fixture.DeduplicateMode, sut.DeduplicateMode); + Assert.Equal(_fixture.ExperimentalEnableLogs, sut.Experimental.EnableLogs); Assert.True(sut.InitializeSdk); Assert.Equal(_fixture.MinimumEventLevel, sut.MinimumEventLevel); Assert.Equal(_fixture.MinimumBreadcrumbLevel, sut.MinimumBreadcrumbLevel); From b0ec5a3975ce774a6b2b3a14470c71022130664d Mon Sep 17 00:00:00 2001 From: Flash0ver <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:52:00 +0200 Subject: [PATCH 08/19] ref(logs): inline method --- src/Sentry.Serilog/SentrySink.Structured.cs | 11 ----------- src/Sentry.Serilog/SentrySink.cs | 6 ++++++ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index 3022c7a3f3..36d93474ed 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -5,8 +5,6 @@ namespace Sentry.Serilog; internal sealed partial class SentrySink { - private static readonly SdkVersion Sdk = CreateSdkVersion(); - private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, string? template) { var traceHeader = hub.GetTraceHeader() ?? SentryTraceHeader.Empty; @@ -103,13 +101,4 @@ static IEnumerable> GetLogEventProperties(KeyValueP } } } - - private static SdkVersion CreateSdkVersion() - { - return new SdkVersion - { - Name = SdkName, - Version = NameAndVersion.Version, - }; - } } diff --git a/src/Sentry.Serilog/SentrySink.cs b/src/Sentry.Serilog/SentrySink.cs index af3ae40a78..3df4b280a4 100644 --- a/src/Sentry.Serilog/SentrySink.cs +++ b/src/Sentry.Serilog/SentrySink.cs @@ -13,6 +13,12 @@ internal sealed partial class SentrySink : ILogEventSink, IDisposable internal static readonly SdkVersion NameAndVersion = typeof(SentrySink).Assembly.GetNameAndVersion(); + private static readonly SdkVersion Sdk = new() + { + Name = SdkName, + Version = NameAndVersion.Version, + }; + /// /// Serilog SDK name. /// From bf68d3586f7cb58a1fdb9eb85a8b2a8c275fe992 Mon Sep 17 00:00:00 2001 From: Flash0ver <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:53:48 +0200 Subject: [PATCH 09/19] test: update API approvals --- .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 4 ++-- .../ApiApprovalTests.Run.DotNet9_0.verified.txt | 4 ++-- .../ApiApprovalTests.Run.Net4_8.verified.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) 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 86091444f4..c0c0fc3e2e 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -29,7 +29,6 @@ namespace Serilog Serilog.Events.LogEventLevel? minimumEventLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, - bool? experimentalEnableLogs = default, bool? sendDefaultPii = default, bool? isEnvironmentUser = default, string? serverName = null, @@ -47,6 +46,7 @@ namespace Serilog Sentry.SentryLevel? diagnosticLevel = default, Sentry.ReportAssembliesMode? reportAssembliesMode = default, Sentry.DeduplicateMode? deduplicateMode = default, - System.Collections.Generic.Dictionary? defaultTags = null) { } + System.Collections.Generic.Dictionary? defaultTags = null, + bool? experimentalEnableLogs = default) { } } } \ No newline at end of file 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 86091444f4..c0c0fc3e2e 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -29,7 +29,6 @@ namespace Serilog Serilog.Events.LogEventLevel? minimumEventLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, - bool? experimentalEnableLogs = default, bool? sendDefaultPii = default, bool? isEnvironmentUser = default, string? serverName = null, @@ -47,6 +46,7 @@ namespace Serilog Sentry.SentryLevel? diagnosticLevel = default, Sentry.ReportAssembliesMode? reportAssembliesMode = default, Sentry.DeduplicateMode? deduplicateMode = default, - System.Collections.Generic.Dictionary? defaultTags = null) { } + System.Collections.Generic.Dictionary? defaultTags = null, + bool? experimentalEnableLogs = default) { } } } \ No newline at end of file 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 86091444f4..c0c0fc3e2e 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -29,7 +29,6 @@ namespace Serilog Serilog.Events.LogEventLevel? minimumEventLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, - bool? experimentalEnableLogs = default, bool? sendDefaultPii = default, bool? isEnvironmentUser = default, string? serverName = null, @@ -47,6 +46,7 @@ namespace Serilog Sentry.SentryLevel? diagnosticLevel = default, Sentry.ReportAssembliesMode? reportAssembliesMode = default, Sentry.DeduplicateMode? deduplicateMode = default, - System.Collections.Generic.Dictionary? defaultTags = null) { } + System.Collections.Generic.Dictionary? defaultTags = null, + bool? experimentalEnableLogs = default) { } } } \ No newline at end of file From 998c1052e49c8717f4fb7439483ba24dd896ea14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:25:42 +0200 Subject: [PATCH 10/19] test(logs): flush client as well --- test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs b/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs index 6c99cedcf4..760b5b84ff 100644 --- a/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs +++ b/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs @@ -43,7 +43,7 @@ public async Task StructuredLogging_Disabled() Handlers = new[] { handler }; Build(); await HttpClient.GetAsync(handler.Path); - ServiceProvider.GetRequiredService().Logger.Flush(); + await ServiceProvider.GetRequiredService().FlushAsync(); Assert.Empty(Logs); } @@ -66,7 +66,7 @@ public async Task StructuredLogging_Enabled() Handlers = new[] { handler }; Build(); await HttpClient.GetAsync(handler.Path); - ServiceProvider.GetRequiredService().Logger.Flush(); + await ServiceProvider.GetRequiredService().FlushAsync(); Assert.NotEmpty(Logs); Assert.Contains(Logs, log => log.Level == SentryLogLevel.Info && log.Message == "Hello, World!"); From 4f84ca7e0a27bd477ced5bac1e032b30090b444f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 10 Sep 2025 22:53:32 +0200 Subject: [PATCH 11/19] ref: remove option from overload not initializing the SDK --- src/Sentry.Serilog/SentrySinkExtensions.cs | 10 +++------- .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 2 +- .../ApiApprovalTests.Run.DotNet9_0.verified.txt | 2 +- .../ApiApprovalTests.Run.Net4_8.verified.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Sentry.Serilog/SentrySinkExtensions.cs b/src/Sentry.Serilog/SentrySinkExtensions.cs index 4597232554..6ca8e5e426 100644 --- a/src/Sentry.Serilog/SentrySinkExtensions.cs +++ b/src/Sentry.Serilog/SentrySinkExtensions.cs @@ -147,7 +147,6 @@ public static LoggerConfiguration Sentry( /// Minimum log level to record a breadcrumb. /// The Serilog format provider. /// The Serilog text formatter. - /// Whether to send structured logs. /// /// This sample shows how each item may be set from within a configuration file: /// @@ -162,8 +161,7 @@ public static LoggerConfiguration Sentry( /// "Args": { /// "minimumEventLevel": "Error", /// "minimumBreadcrumbLevel": "Verbose", - /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}", - /// "experimentalEnableLogs": true + /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}" /// } /// } /// ] @@ -176,8 +174,7 @@ public static LoggerConfiguration Sentry( LogEventLevel? minimumEventLevel = null, LogEventLevel? minimumBreadcrumbLevel = null, IFormatProvider? formatProvider = null, - ITextFormatter? textFormatter = null, - bool? experimentalEnableLogs = null + ITextFormatter? textFormatter = null ) { return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o, @@ -185,8 +182,7 @@ public static LoggerConfiguration Sentry( minimumEventLevel, minimumBreadcrumbLevel, formatProvider, - textFormatter, - experimentalEnableLogs: experimentalEnableLogs)); + textFormatter)); } internal static void ConfigureSentrySerilogOptions( 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 c0c0fc3e2e..f204ed0701 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -21,7 +21,7 @@ namespace Serilog public static class SentrySinkExtensions { public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action configureOptions) { } - public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, bool? experimentalEnableLogs = default) { } + public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { } public static Serilog.LoggerConfiguration Sentry( this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, string dsn, 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 c0c0fc3e2e..f204ed0701 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -21,7 +21,7 @@ namespace Serilog public static class SentrySinkExtensions { public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action configureOptions) { } - public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, bool? experimentalEnableLogs = default) { } + public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { } public static Serilog.LoggerConfiguration Sentry( this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, string dsn, 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 c0c0fc3e2e..f204ed0701 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -21,7 +21,7 @@ namespace Serilog public static class SentrySinkExtensions { public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action configureOptions) { } - public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, bool? experimentalEnableLogs = default) { } + public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { } public static Serilog.LoggerConfiguration Sentry( this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, string dsn, From ed13b351905dfb2d17cde8bab69f7b18cc484499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 10 Sep 2025 22:59:26 +0200 Subject: [PATCH 12/19] fix: Event and Breadcrumbs disabled but Logs enabled --- src/Sentry.Serilog/SentrySink.cs | 12 ++++++++++++ src/Sentry.Serilog/SentrySinkExtensions.cs | 10 +--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Sentry.Serilog/SentrySink.cs b/src/Sentry.Serilog/SentrySink.cs index 3df4b280a4..df01ed1c60 100644 --- a/src/Sentry.Serilog/SentrySink.cs +++ b/src/Sentry.Serilog/SentrySink.cs @@ -56,6 +56,11 @@ internal SentrySink( public void Emit(LogEvent logEvent) { + if (!IsEnabled(logEvent)) + { + return; + } + if (isReentrant.Value) { _options.DiagnosticLogger?.LogError($"Reentrant log event detected. Logging when inside the scope of another log event can cause a StackOverflowException. LogEventInfo.Message: {logEvent.MessageTemplate.Text}"); @@ -73,6 +78,13 @@ public void Emit(LogEvent logEvent) } } + private bool IsEnabled(LogEvent logEvent) + { + return logEvent.Level >= _options.MinimumEventLevel + || logEvent.Level >= _options.MinimumBreadcrumbLevel + || _options.Experimental.EnableLogs; + } + private void InnerEmit(LogEvent logEvent) { if (logEvent.TryGetSourceContext(out var context)) diff --git a/src/Sentry.Serilog/SentrySinkExtensions.cs b/src/Sentry.Serilog/SentrySinkExtensions.cs index 6ca8e5e426..e300ae1697 100644 --- a/src/Sentry.Serilog/SentrySinkExtensions.cs +++ b/src/Sentry.Serilog/SentrySinkExtensions.cs @@ -364,14 +364,6 @@ public static LoggerConfiguration Sentry( sdkDisposable = SentrySdk.Init(options); } - if (options.Experimental.EnableLogs) - { - return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable)); - } - else - { - var minimumOverall = (LogEventLevel)Math.Min((int)options.MinimumBreadcrumbLevel, (int)options.MinimumEventLevel); - return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable), minimumOverall); - } + return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable)); } } From 972efc4fbe802fbcba546764a69cd24622ea00c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 10 Sep 2025 23:49:32 +0200 Subject: [PATCH 13/19] fix: TraceId and ParentSpanId --- src/Sentry.Serilog/SentrySink.Structured.cs | 28 +++++++++++++++++-- .../SentrySinkTests.Structured.cs | 21 ++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index 36d93474ed..c91a697acf 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -7,14 +7,14 @@ internal sealed partial class SentrySink { private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, string? template) { - var traceHeader = hub.GetTraceHeader() ?? SentryTraceHeader.Empty; + GetTraceIdAndSpanId(hub, out var traceId, out var spanId); GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes); - SentryLog log = new(logEvent.Timestamp, traceHeader.TraceId, logEvent.Level.ToSentryLogLevel(), formatted) + SentryLog log = new(logEvent.Timestamp, traceId, logEvent.Level.ToSentryLogLevel(), formatted) { Template = template, Parameters = parameters, - ParentSpanId = traceHeader.SpanId, + ParentSpanId = spanId, }; log.SetDefaultAttributes(_options, Sdk); @@ -27,6 +27,28 @@ private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, 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/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs index b9f4f6c567..ad57f30975 100644 --- a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs @@ -43,9 +43,9 @@ public void Emit_StructuredLogging_LogLevel(LogEventLevel level, SentryLogLevel } [Theory] - [InlineData(false)] [InlineData(true)] - public void Emit_StructuredLogging_LogEvent(bool withTraceHeader) + [InlineData(false)] + public void Emit_StructuredLogging_LogEvent(bool withActiveSpan) { InMemorySentryStructuredLogger capturer = new(); _fixture.Hub.Logger.Returns(capturer); @@ -53,8 +53,17 @@ public void Emit_StructuredLogging_LogEvent(bool withTraceHeader) _fixture.Options.Environment = "test-environment"; _fixture.Options.Release = "test-release"; - var traceHeader = new SentryTraceHeader(SentryId.Create(), SpanId.Create(), null); - _fixture.Hub.GetTraceHeader().Returns(withTraceHeader ? traceHeader : null); + if (withActiveSpan) + { + var span = Substitute.For(); + span.TraceId.Returns(SentryId.Create()); + span.SpanId.Returns(SpanId.Create()); + _fixture.Hub.GetSpan().Returns(span); + } + else + { + _fixture.Hub.GetSpan().Returns((ISpan?)null); + } var sut = _fixture.GetSut(); var logger = new LoggerConfiguration() @@ -72,7 +81,7 @@ public void Emit_StructuredLogging_LogEvent(bool withTraceHeader) var log = capturer.Logs.Should().ContainSingle().Which; log.Timestamp.Should().BeOnOrBefore(DateTimeOffset.Now); - log.TraceId.Should().Be(withTraceHeader ? traceHeader.TraceId : SentryId.Empty); + log.TraceId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.TraceId : _fixture.Scope.PropagationContext.TraceId); log.Level.Should().Be(SentryLogLevel.Info); log.Message.Should().Be("""Message with Scalar property 42, Sequence property: [41, 42, 43], Dictionary property: [("key": "value")], and Structure property: [42, "42"]."""); log.Template.Should().Be("Message with Scalar property {Scalar}, Sequence property: {Sequence}, Dictionary property: {Dictionary}, and Structure property: {Structure}."); @@ -81,7 +90,7 @@ public void Emit_StructuredLogging_LogEvent(bool withTraceHeader) 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(withTraceHeader ? traceHeader.SpanId : SpanId.Empty); + log.ParentSpanId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.SpanId : _fixture.Scope.PropagationContext.SpanId); log.TryGetAttribute("sentry.environment", out object? environment).Should().BeTrue(); environment.Should().Be("test-environment"); From 02defb5706520ee36c35897ca6abe0af6a3e005d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 11 Sep 2025 00:48:25 +0200 Subject: [PATCH 14/19] fix: use Hub-Options over Sink-Options to allow SDK-Init via Serilog and other --- src/Sentry.Serilog/SentrySink.Structured.cs | 4 ++-- src/Sentry.Serilog/SentrySink.cs | 9 +++++--- .../SentrySinkTests.Structured.cs | 22 +++++++++++++++++++ test/Sentry.Serilog.Tests/SentrySinkTests.cs | 1 + 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index c91a697acf..6584afb934 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -5,7 +5,7 @@ namespace Sentry.Serilog; internal sealed partial class SentrySink { - private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, string? template) + private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEvent logEvent, string formatted, string? template) { GetTraceIdAndSpanId(hub, out var traceId, out var spanId); GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes); @@ -17,7 +17,7 @@ private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, ParentSpanId = spanId, }; - log.SetDefaultAttributes(_options, Sdk); + log.SetDefaultAttributes(options, Sdk); foreach (var attribute in attributes) { diff --git a/src/Sentry.Serilog/SentrySink.cs b/src/Sentry.Serilog/SentrySink.cs index df01ed1c60..fff765537f 100644 --- a/src/Sentry.Serilog/SentrySink.cs +++ b/src/Sentry.Serilog/SentrySink.cs @@ -80,9 +80,11 @@ public void Emit(LogEvent logEvent) private bool IsEnabled(LogEvent logEvent) { + var options = _hubAccessor()?.GetSentryOptions(); + return logEvent.Level >= _options.MinimumEventLevel || logEvent.Level >= _options.MinimumBreadcrumbLevel - || _options.Experimental.EnableLogs; + || options?.Experimental.EnableLogs is true; } private void InnerEmit(LogEvent logEvent) @@ -163,9 +165,10 @@ private void InnerEmit(LogEvent logEvent) level: logEvent.Level.ToBreadcrumbLevel()); } - if (_options.Experimental.EnableLogs) + var options = hub.GetSentryOptions(); + if (options?.Experimental.EnableLogs is true) { - CaptureStructuredLog(hub, logEvent, formatted, template); + CaptureStructuredLog(hub, options, logEvent, formatted, template); } } diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs index ad57f30975..b7cb36b76f 100644 --- a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs @@ -21,6 +21,28 @@ public void Emit_StructuredLogging_IsEnabled(bool isEnabled) capturer.Logs.Should().HaveCount(isEnabled ? 1 : 0); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Emit_StructuredLogging_UseHubOptionsOverSinkOptions(bool isEnabled) + { + InMemorySentryStructuredLogger capturer = new(); + _fixture.Hub.Logger.Returns(capturer); + _fixture.Options.Experimental.EnableLogs = true; + + if (!isEnabled) + { + SentryClientExtensions.SentryOptionsForTestingOnly = null; + } + + var sut = _fixture.GetSut(); + var logger = new LoggerConfiguration().WriteTo.Sink(sut).MinimumLevel.Verbose().CreateLogger(); + + logger.Write(LogEventLevel.Information, "Message"); + + capturer.Logs.Should().HaveCount(isEnabled ? 1 : 0); + } + [Theory] [InlineData(LogEventLevel.Verbose, SentryLogLevel.Trace)] [InlineData(LogEventLevel.Debug, SentryLogLevel.Debug)] diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.cs index 2fa7000d7c..0ed6e94139 100644 --- a/test/Sentry.Serilog.Tests/SentrySinkTests.cs +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.cs @@ -15,6 +15,7 @@ public Fixture() Hub.IsEnabled.Returns(true); HubAccessor = () => Hub; Hub.SubstituteConfigureScope(Scope); + SentryClientExtensions.SentryOptionsForTestingOnly = Options; } public SentrySink GetSut() From 7f886cc92be372168c54cfe6d14a7510a0098149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:47:12 +0200 Subject: [PATCH 15/19] docs: update CHANGELOG after release --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fba9568d3b..964bd9bff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ # Changelog +## Unreleased + +### Features + +- Add (experimental) _Structured Logs_ integration for `Serilog` ([#4462](https://github.com/getsentry/sentry-dotnet/pull/4462)) + ## 5.15.0 ### Features - Experimental _Structured Logs_: - - Add integration for `Serilog` ([#4462](https://github.com/getsentry/sentry-dotnet/pull/4462)) - Redesign SDK Logger APIs to allow usage of `params` ([#4451](https://github.com/getsentry/sentry-dotnet/pull/4451)) - Shorten the `key` names of `Microsoft.Extensions.Logging` attributes ([#4450](https://github.com/getsentry/sentry-dotnet/pull/4450)) From c29c704af8750f58d8c2f647ceb041ab35a11806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:38:33 +0200 Subject: [PATCH 16/19] docs: update CHANGELOG after merge --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 495b3b4caa..f2bb515671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add (experimental) _Structured Logs_ integration for `Serilog` ([#4462](https://github.com/getsentry/sentry-dotnet/pull/4462)) + ### Fixes - Fail when building Blazor WASM with Profiling. We don't support profiling in Blazor WebAssembly projects. ([#4512](https://github.com/getsentry/sentry-dotnet/pull/4512)) From 5136730782eb727420c6c74076be67c202a02007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:57:23 +0200 Subject: [PATCH 17/19] docs: add comment about options --- src/Sentry.Serilog/SentrySink.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Sentry.Serilog/SentrySink.cs b/src/Sentry.Serilog/SentrySink.cs index fff765537f..0a003f598a 100644 --- a/src/Sentry.Serilog/SentrySink.cs +++ b/src/Sentry.Serilog/SentrySink.cs @@ -165,6 +165,9 @@ private void InnerEmit(LogEvent logEvent) level: logEvent.Level.ToBreadcrumbLevel()); } + // Read the options from the Hub, rather than the Sink's Serilog-Options, because 'EnableLogs' is declared in the base 'SentryOptions', rather than the derived 'SentrySerilogOptions'. + // In cases where Sentry's Serilog-Sink is added without a DSN (i.e., without initializing the SDK) and the SDK is initialized differently (e.g., through ASP.NET Core), + // then the 'EnableLogs' option of this Sink's Serilog-Options is default, but the Hub's Sentry-Options have the actual user-defined value configured. var options = hub.GetSentryOptions(); if (options?.Experimental.EnableLogs is true) { From 91f50135a860bfdd28c073dc5ff0085479089915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:05:36 +0200 Subject: [PATCH 18/19] ref: remove redundant null-check Co-authored-by: James Crosswell --- src/Sentry.Serilog/SentrySink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry.Serilog/SentrySink.cs b/src/Sentry.Serilog/SentrySink.cs index 0a003f598a..46963afe3c 100644 --- a/src/Sentry.Serilog/SentrySink.cs +++ b/src/Sentry.Serilog/SentrySink.cs @@ -80,7 +80,7 @@ public void Emit(LogEvent logEvent) private bool IsEnabled(LogEvent logEvent) { - var options = _hubAccessor()?.GetSentryOptions(); + var options = _hubAccessor().GetSentryOptions(); return logEvent.Level >= _options.MinimumEventLevel || logEvent.Level >= _options.MinimumBreadcrumbLevel From cdf03180ec8776175f3a80127304d298c66f8558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:16:24 +0200 Subject: [PATCH 19/19] ref: remove another redundant null-check --- src/Sentry.Serilog/SentrySink.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Sentry.Serilog/SentrySink.cs b/src/Sentry.Serilog/SentrySink.cs index 46963afe3c..369d52a673 100644 --- a/src/Sentry.Serilog/SentrySink.cs +++ b/src/Sentry.Serilog/SentrySink.cs @@ -97,8 +97,7 @@ private void InnerEmit(LogEvent logEvent) } } - var hub = _hubAccessor(); - if (hub is null || !hub.IsEnabled) + if (_hubAccessor() is not { IsEnabled: true } hub) { return; }