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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Sentry.Serilog/SentrySerilogOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,16 @@ public class SentrySerilogOptions : SentryOptions
/// Optional <see cref="ITextFormatter"/>
/// </summary>
public ITextFormatter? TextFormatter { get; set; }

/// <summary>
/// The minimum level for events passed through the sink. Ignored when <see cref="LevelSwitch"/> is specified.
/// </summary>
/// <seealso href="https://github.com/serilog/serilog/wiki/Configuration-Basics#overriding-per-sink"/>
public LogEventLevel RestrictedToMinimumLevel { get; set; } = LevelAlias.Minimum;

/// <summary>
/// A switch allowing the pass-through minimum level to be changed at runtime.
/// </summary>
/// <seealso href="https://github.com/serilog/serilog/wiki/Configuration-Basics#overriding-per-sink"/>
public LoggingLevelSwitch? LevelSwitch { get; set; }
}
30 changes: 23 additions & 7 deletions src/Sentry.Serilog/SentrySinkExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public static class SentrySinkExtensions
/// <param name="deduplicateMode">What modes to use for event automatic de-duplication. <seealso cref="SentryOptions.DeduplicateMode"/></param>
/// <param name="defaultTags">Default tags to add to all events. <seealso cref="SentryOptions.DefaultTags"/></param>
/// <param name="enableLogs">Whether to send structured logs. <seealso cref="SentryOptions.EnableLogs"/></param>
/// <param name="restrictedToMinimumLevel">The minimum level for events passed through the sink. Ignored when <paramref name="levelSwitch"/> is specified. <seealso cref="SentrySerilogOptions.RestrictedToMinimumLevel"/></param>
/// <param name="levelSwitch">A switch allowing the pass-through minimum level to be changed at runtime. <seealso cref="SentrySerilogOptions.LevelSwitch"/></param>
/// <returns><see cref="LoggerConfiguration"/></returns>
/// <example>This sample shows how each item may be set from within a configuration file:
/// <code>
Expand Down Expand Up @@ -106,7 +108,9 @@ public static LoggerConfiguration Sentry(
ReportAssembliesMode? reportAssembliesMode = null,
DeduplicateMode? deduplicateMode = null,
Dictionary<string, string>? defaultTags = null,
bool? enableLogs = null)
bool? enableLogs = null,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
LoggingLevelSwitch? levelSwitch = null)
{
return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o,
dsn,
Expand All @@ -132,7 +136,9 @@ public static LoggerConfiguration Sentry(
reportAssembliesMode,
deduplicateMode,
defaultTags,
enableLogs));
enableLogs,
restrictedToMinimumLevel,
levelSwitch));
}

/// <summary>
Expand All @@ -147,6 +153,8 @@ public static LoggerConfiguration Sentry(
/// <param name="minimumBreadcrumbLevel">Minimum log level to record a breadcrumb. <seealso cref="SentrySerilogOptions.MinimumBreadcrumbLevel"/></param>
/// <param name="formatProvider">The Serilog format provider. <seealso cref="IFormatProvider"/></param>
/// <param name="textFormatter">The Serilog text formatter. <seealso cref="ITextFormatter"/></param>
/// <param name="restrictedToMinimumLevel">The minimum level for events passed through the sink. Ignored when <paramref name="levelSwitch"/> is specified. <seealso cref="SentrySerilogOptions.RestrictedToMinimumLevel"/></param>
/// <param name="levelSwitch">A switch allowing the pass-through minimum level to be changed at runtime. <seealso cref="SentrySerilogOptions.LevelSwitch"/></param>
/// <returns><see cref="LoggerConfiguration"/></returns>
/// <example>This sample shows how each item may be set from within a configuration file:
/// <code>
Expand Down Expand Up @@ -174,15 +182,18 @@ public static LoggerConfiguration Sentry(
LogEventLevel? minimumEventLevel = null,
LogEventLevel? minimumBreadcrumbLevel = null,
IFormatProvider? formatProvider = null,
ITextFormatter? textFormatter = null
)
ITextFormatter? textFormatter = null,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
LoggingLevelSwitch? levelSwitch = null)
{
return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o,
null,
minimumEventLevel,
minimumBreadcrumbLevel,
formatProvider,
textFormatter));
textFormatter,
restrictedToMinimumLevel: restrictedToMinimumLevel,
levelSwitch: levelSwitch));
}

internal static void ConfigureSentrySerilogOptions(
Expand Down Expand Up @@ -210,7 +221,9 @@ internal static void ConfigureSentrySerilogOptions(
ReportAssembliesMode? reportAssembliesMode = null,
DeduplicateMode? deduplicateMode = null,
Dictionary<string, string>? defaultTags = null,
bool? enableLogs = null)
bool? enableLogs = null,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
LoggingLevelSwitch? levelSwitch = null)
{
if (dsn is not null)
{
Expand Down Expand Up @@ -327,6 +340,9 @@ internal static void ConfigureSentrySerilogOptions(
sentrySerilogOptions.EnableLogs = enableLogs.Value;
}

sentrySerilogOptions.RestrictedToMinimumLevel = restrictedToMinimumLevel;
sentrySerilogOptions.LevelSwitch = levelSwitch;

// Serilog-specific items
sentrySerilogOptions.InitializeSdk = dsn is not null; // Inferred from the Sentry overload that is used
if (defaultTags?.Count > 0)
Expand Down Expand Up @@ -364,6 +380,6 @@ public static LoggerConfiguration Sentry(
sdkDisposable = SentrySdk.Init(options);
}

return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable));
return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable), options.RestrictedToMinimumLevel, options.LevelSwitch);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ namespace Sentry.Serilog
public SentrySerilogOptions() { }
public System.IFormatProvider? FormatProvider { get; set; }
public bool InitializeSdk { get; set; }
public Serilog.Core.LoggingLevelSwitch? LevelSwitch { get; set; }
public Serilog.Events.LogEventLevel MinimumBreadcrumbLevel { get; set; }
public Serilog.Events.LogEventLevel MinimumEventLevel { get; set; }
public Serilog.Events.LogEventLevel RestrictedToMinimumLevel { get; set; }
public Serilog.Formatting.ITextFormatter? TextFormatter { get; set; }
}
}
Expand All @@ -21,7 +23,7 @@ namespace Serilog
public static class SentrySinkExtensions
{
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action<Sentry.Serilog.SentrySerilogOptions> 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, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
public static Serilog.LoggerConfiguration Sentry(
this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration,
string dsn,
Expand All @@ -47,6 +49,8 @@ namespace Serilog
Sentry.ReportAssembliesMode? reportAssembliesMode = default,
Sentry.DeduplicateMode? deduplicateMode = default,
System.Collections.Generic.Dictionary<string, string>? defaultTags = null,
bool? enableLogs = default) { }
bool? enableLogs = default,
Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0,
Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ namespace Sentry.Serilog
public SentrySerilogOptions() { }
public System.IFormatProvider? FormatProvider { get; set; }
public bool InitializeSdk { get; set; }
public Serilog.Core.LoggingLevelSwitch? LevelSwitch { get; set; }
public Serilog.Events.LogEventLevel MinimumBreadcrumbLevel { get; set; }
public Serilog.Events.LogEventLevel MinimumEventLevel { get; set; }
public Serilog.Events.LogEventLevel RestrictedToMinimumLevel { get; set; }
public Serilog.Formatting.ITextFormatter? TextFormatter { get; set; }
}
}
Expand All @@ -21,7 +23,7 @@ namespace Serilog
public static class SentrySinkExtensions
{
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action<Sentry.Serilog.SentrySerilogOptions> 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, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
public static Serilog.LoggerConfiguration Sentry(
this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration,
string dsn,
Expand All @@ -47,6 +49,8 @@ namespace Serilog
Sentry.ReportAssembliesMode? reportAssembliesMode = default,
Sentry.DeduplicateMode? deduplicateMode = default,
System.Collections.Generic.Dictionary<string, string>? defaultTags = null,
bool? enableLogs = default) { }
bool? enableLogs = default,
Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0,
Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@
public SentrySerilogOptions() { }
public System.IFormatProvider? FormatProvider { get; set; }
public bool InitializeSdk { get; set; }
public Serilog.Core.LoggingLevelSwitch? LevelSwitch { get; set; }
public Serilog.Events.LogEventLevel MinimumBreadcrumbLevel { get; set; }
public Serilog.Events.LogEventLevel MinimumEventLevel { get; set; }
public Serilog.Events.LogEventLevel RestrictedToMinimumLevel { get; set; }
public Serilog.Formatting.ITextFormatter? TextFormatter { get; set; }
}
}
namespace Serilog
{
public static class SentrySinkExtensions
{

Check warning on line 24 in test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt

View check run for this annotation

@sentry/warden / warden: code-review

Adding optional parameters to public Sentry() overloads is a binary-breaking change

Adding new optional parameters (`restrictedToMinimumLevel`, `levelSwitch`) to the existing public `Sentry()` extension methods changes their metadata signatures. While source-compatible, this is binary-breaking in .NET: consumers compiled against a previous version of Sentry.Serilog will get a MissingMethodException at runtime until they recompile. For a public library this is an API contract change that warrants senior engineer review and, ideally, preservation of the prior signatures via additional overloads instead of in-place parameter additions.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding optional parameters to public Sentry() overloads is a binary-breaking change

Adding new optional parameters (restrictedToMinimumLevel, levelSwitch) to the existing public Sentry() extension methods changes their metadata signatures. While source-compatible, this is binary-breaking in .NET: consumers compiled against a previous version of Sentry.Serilog will get a MissingMethodException at runtime until they recompile. For a public library this is an API contract change that warrants senior engineer review and, ideally, preservation of the prior signatures via additional overloads instead of in-place parameter additions.

Verification

Read src/Sentry.Serilog/SentrySinkExtensions.cs and confirmed the two affected overloads (lines 86-113 and 180-187) had new optional parameters appended rather than being introduced as new overloads. Verified that the API approval snapshot reflects the same signature change at lines 24 and 49-53 of the .verified.txt hunk. .NET's binding to optional parameters is resolved at the call site at compile time, so adding optional parameters changes the method's metadata token and breaks binary compatibility, even though source compiles unchanged.

Identified by Warden code-review · PB4-XBA

Copy link
Copy Markdown
Collaborator Author

@jamescrosswell jamescrosswell May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Flash0ver I'd rather go with a binary breaking change here (it's source compatible). The signatures get really messy if we try to add overloads (there are domino effects).

I don't think there are many people swapping out the Sentry dll without recompiling their apps when they upgrade (I'd venture to say probably none)... and accessing functionality like this via reflection also seems highly improbable.

How do you feel about that?

public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action<Sentry.Serilog.SentrySerilogOptions> 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, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
public static Serilog.LoggerConfiguration Sentry(
this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration,
string dsn,
Expand All @@ -47,6 +49,8 @@
Sentry.ReportAssembliesMode? reportAssembliesMode = default,
Sentry.DeduplicateMode? deduplicateMode = default,
System.Collections.Generic.Dictionary<string, string>? defaultTags = null,
bool? enableLogs = default) { }
bool? enableLogs = default,
Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0,
Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ namespace Sentry.Serilog
public SentrySerilogOptions() { }
public System.IFormatProvider? FormatProvider { get; set; }
public bool InitializeSdk { get; set; }
public Serilog.Core.LoggingLevelSwitch? LevelSwitch { get; set; }
public Serilog.Events.LogEventLevel MinimumBreadcrumbLevel { get; set; }
public Serilog.Events.LogEventLevel MinimumEventLevel { get; set; }
public Serilog.Events.LogEventLevel RestrictedToMinimumLevel { get; set; }
public Serilog.Formatting.ITextFormatter? TextFormatter { get; set; }
}
}
Expand All @@ -21,7 +23,7 @@ namespace Serilog
public static class SentrySinkExtensions
{
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action<Sentry.Serilog.SentrySerilogOptions> 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, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
public static Serilog.LoggerConfiguration Sentry(
this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration,
string dsn,
Expand All @@ -47,6 +49,8 @@ namespace Serilog
Sentry.ReportAssembliesMode? reportAssembliesMode = default,
Sentry.DeduplicateMode? deduplicateMode = default,
System.Collections.Generic.Dictionary<string, string>? defaultTags = null,
bool? enableLogs = default) { }
bool? enableLogs = default,
Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0,
Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
}
}
1 change: 1 addition & 0 deletions test/Sentry.Serilog.Tests/Sentry.Serilog.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ProjectReference Include="..\..\src\Sentry.Serilog\Sentry.Serilog.csproj" />

<Using Include="Serilog" />
<Using Include="Serilog.Core" />
<Using Include="Serilog.Events" />
<Using Include="Serilog.Context" />
<Using Include="Serilog.Formatting.Display" />
Expand Down
51 changes: 50 additions & 1 deletion test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ private class Fixture
public bool InitializeSdk { get; } = false;
public LogEventLevel MinimumEventLevel { get; } = LogEventLevel.Verbose;
public LogEventLevel MinimumBreadcrumbLevel { get; } = LogEventLevel.Fatal;
public LogEventLevel RestrictedToMinimumLevel { get; } = LogEventLevel.Warning;
public LoggingLevelSwitch LevelSwitch { get; } = new(LogEventLevel.Error);

public static SentrySerilogOptions GetSut() => new();
}
Expand Down Expand Up @@ -98,7 +100,8 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan
_fixture.SampleRate, _fixture.Release, _fixture.Environment, _fixture.MaxQueueItems,
_fixture.ShutdownTimeout, _fixture.DecompressionMethods, _fixture.RequestBodyCompressionLevel,
_fixture.RequestBodyCompressionBuffered, _fixture.Debug, _fixture.DiagnosticLevel,
_fixture.ReportAssembliesMode, _fixture.DeduplicateMode, null, _fixture.EnableLogs);
_fixture.ReportAssembliesMode, _fixture.DeduplicateMode, null, _fixture.EnableLogs,
_fixture.RestrictedToMinimumLevel, _fixture.LevelSwitch);

// Compare individual properties
Assert.Equal(_fixture.SendDefaultPii, sut.SendDefaultPii);
Expand All @@ -123,6 +126,52 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan
Assert.True(sut.InitializeSdk);
Assert.Equal(_fixture.MinimumEventLevel, sut.MinimumEventLevel);
Assert.Equal(_fixture.MinimumBreadcrumbLevel, sut.MinimumBreadcrumbLevel);
Assert.Equal(_fixture.RestrictedToMinimumLevel, sut.RestrictedToMinimumLevel);
Assert.Same(_fixture.LevelSwitch, sut.LevelSwitch);
}

[Fact]
public void Sentry_WithRestrictedToMinimumLevel_ConfigureOptions_FiltersLogsBelow()
{
// Arrange
var hub = Substitute.For<IHub>();
hub.IsEnabled.Returns(true);
var options = new SentrySerilogOptions
{
InitializeSdk = false,
MinimumBreadcrumbLevel = LogEventLevel.Verbose,
MinimumEventLevel = LogEventLevel.Verbose,
RestrictedToMinimumLevel = LogEventLevel.Error,
};
var sink = new SentrySink(options, () => hub, null, new MockClock());
using var logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.Sink(sink, options.RestrictedToMinimumLevel, options.LevelSwitch)
.CreateLogger();

// Act
logger.Warning("Below threshold");
logger.Error("At threshold");

// Assert: Warning is filtered by Serilog before reaching the sink; only Error gets through
hub.Received(1).CaptureEvent(Arg.Any<SentryEvent>());
hub.DidNotReceive().CaptureEvent(Arg.Is<SentryEvent>(e =>
e.Message.Message == "Below threshold"));
}

[Fact]
public void Sentry_WithRestrictedToMinimumLevel_NoDsn_ParameterIsAccepted()
{
// Verify the no-DSN overload accepts restrictedToMinimumLevel without throwing
var ex = Record.Exception(() =>
new LoggerConfiguration()
.WriteTo.Sentry(
minimumBreadcrumbLevel: LogEventLevel.Verbose,
minimumEventLevel: LogEventLevel.Error,
restrictedToMinimumLevel: LogEventLevel.Warning)
.CreateLogger());

Assert.Null(ex);
}

private static void AssertEqualDeep(object expected, object actual)
Expand Down
Loading