Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
36fde27
feat: Serilog sink no longer initializes the SDK
jamescrosswell Sep 14, 2026
5385388
Accept API verifier changes
getsentry-bot Sep 14, 2026
f7f48a5
Merge remote-tracking branch 'origin/version7' into feat/no-init-from…
jamescrosswell Sep 14, 2026
9f8ec8c
Merge remote-tracking branch 'origin/feat/no-init-from-logging-5245' …
jamescrosswell Sep 14, 2026
c72c26e
Tweak comments in the samples
jamescrosswell Sep 17, 2026
0aada87
docs: drop unused Sentry settings from the Serilog sample appsettings
jamescrosswell Sep 21, 2026
5acfa5e
fix(serilog): make the UseSerilog warning check atomic
jamescrosswell Sep 21, 2026
3307c30
refactor: the Serilog sink no longer sets the SDK name
jamescrosswell Sep 22, 2026
72d27f9
feat(serilog): configuring a DSN on the sink now fails with a migrati…
ric-oliv Sep 22, 2026
9d27112
test(serilog): pin the DSN tombstone against Serilog.Settings.Configu…
jamescrosswell Sep 22, 2026
ed73567
docs(serilog): reword the DSN migration error
jamescrosswell Sep 22, 2026
a1bf334
fix(serilog): the Sentry sink registers the Serilog scope event proce…
ric-oliv Sep 23, 2026
7b88c80
fix(serilog): register the scope event processor atomically
jamescrosswell Sep 23, 2026
53f4144
refactor(serilog): use the Lock shim for the registration lock
jamescrosswell Sep 23, 2026
bbf2ffd
Merge remote-tracking branch 'origin/version7' into feat/no-init-from…
jamescrosswell Sep 23, 2026
fbbaa39
docs: samples are exempt from the no-comments rule
jamescrosswell Sep 23, 2026
5f4f1d1
Apply suggestion from @jamescrosswell
jamescrosswell Sep 23, 2026
2925072
feat(serilog): warn at runtime when the sink drops events because Sen…
jamescrosswell Sep 28, 2026
656118c
Update src/Sentry.Serilog/SentryOptionExtensions.cs
jamescrosswell Sep 29, 2026
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ Do not add comments that restate what the code already says. In particular, don'
members as being exposed for tests (`// Exposed for tests`) — that's already apparent from
the member being `internal` and from tests being its only callers.

**This applies to the library, not to `samples/`.** Samples are documentation: people read them
to learn how to wire Sentry up, and copy them as a starting point. Comment them concisely but clearly,
including the configuration files — `appsettings.json` and the like carry `//` comments in this
repo even though JSON doesn't officially support them, and that has never caused a problem.
When a sample changes, update its comments rather than dropping them.

## Adding New Options (AOT Compatibility)

`SentryOptions` is **not** bound directly from configuration. Instead, a parallel `BindableSentryOptions` class (`src/Sentry/BindableSentryOptions.cs`) exists for AOT-safe configuration binding.
Expand Down
23 changes: 12 additions & 11 deletions samples/Sentry.Samples.AspNetCore.Serilog/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Sentry.Serilog;
using Serilog;
using Serilog.Events;

Expand All @@ -14,28 +15,28 @@ public static WebApplication BuildWebApp(string[] args)
c.Enrich.FromLogContext()
.MinimumLevel.Debug()
.WriteTo.Console()
// Add Sentry integration with Serilog
// Configure Serilog to send logs to Sentry. This only configures the sink - Sentry is initialised below.
.WriteTo.Sentry(s =>
{
// Sets the minimum log level required to add a log message as breadcrumb
s.MinimumBreadcrumbLevel = LogEventLevel.Debug;
// Set the minimum level for messages to be sent out as events to Sentry
s.MinimumEventLevel = LogEventLevel.Error;
// When configuring Sentry's Serilog integration in combination with other integrations that
// initialize the Sentry SDK (like ASP.NET Core or MAUI) we need to tell it not to reinitialize
// Sentry... we just want it to set up the Serilog sink
s.InitializeSdk = false;
}));

// Add Sentry integration
// It can be defined via configuration (including `appsettings.json`)
// or coded explicitly, via parameter like:
// .UseSentry("dsn") or .UseSentry(o => o.Dsn = ""; o.Release = "1.0"; ...)
// Add the Sentry integration.
// Most options can be defined via binding configuration (including `appsettings.json` as we do here)
// or coded explicitly, in the options callback below (as we do with the DSN and Serilog log context)
builder.WebHost.UseSentry(o =>
{
#if !SENTRY_DSN_DEFINED_IN_ENV
builder.WebHost.UseSentry(SamplesShared.Dsn);
o.Dsn = SamplesShared.Dsn;
#else
builder.WebHost.UseSentry(EnvironmentVariables.Dsn);
o.Dsn = EnvironmentVariables.Dsn;
#endif
// Apply properties from the Serilog LogContext to Sentry events
o.UseSerilog();
});

// The App:
var webApplication = builder.Build();
Expand Down
Comment thread
jamescrosswell marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
// Example configuration via JSON. Only a subset of the settings below:
// All Sentry settings can also be configured via code or environment variables:
"Sentry": {
// The DSN can also be set via environment variable
//"Dsn": "TODO: Configure your DSN here and uncomment this line",
// Enable Sentry tracing features
"EnableTracing": true,
// This sample sets the DSN in Program.cs. You can set it here instead, or via the SENTRY_DSN environment variable.
// Opt-in for payload submission
"MaxRequestBodySize": "Always",
// Sends Cookies, User Id when one is logged on and user IP address to sentry. It's turned off by default.
Expand Down
31 changes: 19 additions & 12 deletions samples/Sentry.Samples.Serilog/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Sentry.Serilog;
using Serilog;
using Serilog.Context;
using Serilog.Events;
Expand All @@ -7,29 +8,35 @@ internal static class Program
{
private static void Main()
{
// Initialise Sentry SDK itself
using var _ = SentrySdk.Init(options =>
{
#if !SENTRY_DSN_DEFINED_IN_ENV
// A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable.
// See https://docs.sentry.io/product/sentry-basics/dsn-explainer/
options.Dsn = SamplesShared.Dsn;
#endif

options.AttachStacktrace = true;
// send PII like the username of the user logged in to the device
options.SendDefaultPii = true;
// Apply properties from the Serilog LogContext (like MyTaskId below) to Sentry events
options.UseSerilog();
});

Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug()
.WriteTo.Console()
// Other overloads exist, for example, configure the SDK with only the DSN or no parameters at all.
// Configure Serilog to send logs to Sentry
.WriteTo.Sentry(options =>
{
#if !SENTRY_DSN_DEFINED_IN_ENV
// A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable.
// See https://docs.sentry.io/product/sentry-basics/dsn-explainer/
options.Dsn = SamplesShared.Dsn;
#endif

// Debug and higher are stored as breadcrumbs (default os Information)
options.MinimumBreadcrumbLevel = LogEventLevel.Debug;
// Error and higher is sent as event (default is Error)
// Error and higher are sent as events (default is Error)
options.MinimumEventLevel = LogEventLevel.Error;
options.AttachStacktrace = 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.
options.TextFormatter = new MessageTemplateTextFormatter("[{MyTaskId}] {Message}");
// Other configuration
})
.CreateLogger();

Expand Down
37 changes: 28 additions & 9 deletions src/Sentry.Serilog/SentryOptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,36 @@ namespace Sentry.Serilog;
/// </summary>
public static class SentryOptionExtensions
{
private static readonly Lock Sync = new();

/// <summary>
/// Ensures Serilog scope properties get applied to Sentry events. If you are not initialising Sentry when
/// configuring the Sentry sink for Serilog then you should call this method in the options callback for whichever
/// Sentry integration you are using to initialise Sentry.
/// Enables the Serilog integration, so that properties from the Serilog <c>LogContext</c> get applied to all Sentry
/// events.
/// </summary>
/// <param name="options"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T ApplySerilogScopeToEvents<T>(this T options) where T : SentryOptions
/// <remarks>
/// Optional: the Sentry sink does this for you once it starts logging,
/// so only events captured before then miss the tags.
/// Call this in the options callback when initializing Sentry to include them from the first event.
/// Calling it more than once has no additional effect.
/// </remarks>
Comment thread
jamescrosswell marked this conversation as resolved.
/// <param name="options">The options used to initialise Sentry.</param>
public static void UseSerilog(this SentryOptions options) => options.TryUseSerilog();

// Sinks sharing one set of options can reach this concurrently, so the check and the add have to be atomic.
internal static bool TryUseSerilog(this SentryOptions options)
{
options.AddEventProcessor(new SerilogScopeEventProcessor(options));
return options;
lock (Sync)
{
if (options.HasSerilogScopeEventProcessor())
{
return false;
}

options.AddEventProcessor(new SerilogScopeEventProcessor(options));
return true;
}
}

internal static bool HasSerilogScopeEventProcessor(this SentryOptions options)
=> options.EventProcessors.Any(processor => processor.Type == typeof(SerilogScopeEventProcessor));
}
14 changes: 6 additions & 8 deletions src/Sentry.Serilog/SentrySerilogOptions.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
namespace Sentry.Serilog;

/// <summary>
/// Sentry Options for Serilog logging
/// Options for the Sentry sink for Serilog.
/// </summary>
/// <inheritdoc />
public class SentrySerilogOptions : SentryOptions
/// <remarks>
/// These options only configure the sink. The Sentry SDK itself is configured and initialised separately, using
/// <c>SentrySdk.Init</c> or another Sentry integration (such as ASP.NET Core or MAUI).
/// </remarks>
public class SentrySerilogOptions
{
/// <summary>
/// Whether to initialize this SDK through this integration
/// </summary>
public bool InitializeSdk { get; set; } = true;

/// <summary>
/// Minimum log level to send an event.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry.Serilog/SentrySink.Structured.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEve
};

var scope = hub.GetScope();
log.SetDefaultAttributes(options, scope, Sdk);
log.SetDefaultAttributes(options, scope);
log.SetOrigin("auto.log.serilog");

foreach (var attribute in attributes)
Expand Down
110 changes: 59 additions & 51 deletions src/Sentry.Serilog/SentrySink.cs
Original file line number Diff line number Diff line change
@@ -1,91 +1,97 @@
using Serilog.Debugging;

namespace Sentry.Serilog;

/// <summary>
/// Sentry Sink for Serilog
/// </summary>
/// <inheritdoc cref="IDisposable" />
/// <inheritdoc cref="ILogEventSink" />
internal sealed partial class SentrySink : ILogEventSink, IDisposable
internal sealed partial class SentrySink : ILogEventSink
{
private readonly IDisposable? _sdkDisposable;
private readonly SentrySerilogOptions _options;

internal static readonly SdkVersion NameAndVersion
= typeof(SentrySink).Assembly.GetNameAndVersion();

private static readonly SdkVersion Sdk = new()
{
Name = SdkName,
Version = NameAndVersion.Version,
};

/// <summary>
/// Serilog SDK name.
/// </summary>
public const string SdkName = "sentry.dotnet.serilog";

private static readonly string ProtocolPackageName = "nuget:" + NameAndVersion.Name;

private readonly Func<IHub> _hubAccessor;
private readonly ISystemClock _clock;
private readonly UninitializedSdkWarning _uninitializedSdkWarning;

public SentrySink(
SentrySerilogOptions options,
IDisposable? sdkDisposable)
private int _registeredScopeEventProcessor;

internal const string UninitializedSdkMessage =
"Sentry: the Serilog sink dropped a log event because Sentry is not initialized, but a DSN was found " +
"in the environment or in an assembly attribute. The sink no longer initializes the SDK: call " +
"SentrySdk.Init (or UseSentry via one of the integrations) at startup and UseSerilog() on those options. " +
"See https://docs.sentry.io/platforms/dotnet/guides/serilog/";

public SentrySink(SentrySerilogOptions options)
: this(
options,
() => HubAdapter.Instance,
sdkDisposable,
SystemClock.Clock)
{
}

internal SentrySink(
SentrySerilogOptions options,
Func<IHub> hubAccessor,
IDisposable? sdkDisposable,
ISystemClock clock)
ISystemClock clock,
UninitializedSdkWarning? uninitializedSdkWarning = null)
{
_options = options;
_hubAccessor = hubAccessor;
_clock = clock;
_sdkDisposable = sdkDisposable;
_uninitializedSdkWarning = uninitializedSdkWarning
?? new UninitializedSdkWarning(message => SelfLog.WriteLine("{0}", message));

if (hubAccessor() is { IsEnabled: true } hub && hub.GetSentryOptions() is { } sentryOptions)
{
EnsureSerilogScopeEventProcessor(sentryOptions);
}
}

private static AsyncLocal<bool> isReentrant = new();

public void Emit(LogEvent logEvent)
{
// Must precede the reentrancy check below to avoid an infinite recursion
logEvent.TryGetSourceContext(out var context);
if (SentrySdkNamespaces.IsSentrySdk(context))
{
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}");
_hubAccessor()?.GetSentryOptions()?.DiagnosticLogger?.LogError($"Reentrant log event detected. Logging when inside the scope of another log event can cause a StackOverflowException. LogEventInfo.Message: {logEvent.MessageTemplate.Text}");
return;
}

isReentrant.Value = true;
try
{
InnerEmit(logEvent);
InnerEmit(logEvent, context);
}
finally
{
isReentrant.Value = false;
}
}

private void InnerEmit(LogEvent logEvent)
private void InnerEmit(LogEvent logEvent, string? context)
{
if (logEvent.TryGetSourceContext(out var context))
if (_hubAccessor() is not { IsEnabled: true } hub)
{
if (SentrySdkNamespaces.IsSentrySdk(context))
if (logEvent.Level >= _options.MinimumEventLevel)
{
return;
_uninitializedSdkWarning.WarnOnce(UninitializedSdkMessage);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shutdown logs warn SDK never initialized

Low Severity

A disabled hub after Close or disposing the Init handle is treated the same as never initializing. If SENTRY_DSN (or a Dsn attribute) is still present, a later event-level log can write the one-time stderr warning telling the app to call SentrySdk.Init, even when Sentry was initialized correctly and then shut down.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2925072. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Highly unlikely... that implies the SDK has been initialised, the app has been running and the SDK closed all before a single log is made (since the warning only ever fires once - it will only be triggered by the first log message if the SDK is not initialised at that time).


return;
}

if (_hubAccessor() is not { IsEnabled: true } hub)
var options = hub.GetSentryOptions();
if (options is not null)
{
return;
EnsureSerilogScopeEventProcessor(options);
}

var exception = logEvent.Exception;
Expand All @@ -106,17 +112,6 @@ private void InnerEmit(LogEvent logEvent)
Level = logEvent.Level.ToSentryLevel()
};

if (evt.Sdk is { } sdk)
{
sdk.Name = SdkName;
sdk.Version = NameAndVersion.Version;

if (NameAndVersion.Version is { } version)
{
sdk.AddPackage(ProtocolPackageName, version);
}
}

evt.SetExtras(GetLoggingEventProperties(logEvent));

hub.CaptureEvent(evt);
Expand Down Expand Up @@ -151,16 +146,31 @@ private void InnerEmit(LogEvent logEvent)
level: logEvent.Level.ToBreadcrumbLevel());
}

// Read the options from the Hub, rather than the Sink's Serilog-Options. 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), only the Hub's Sentry-Options have the actual user-defined values configured.
var options = hub.GetSentryOptions();
if (options is not null)
{
CaptureStructuredLog(hub, options, logEvent, formatted, template);
}
}

private void EnsureSerilogScopeEventProcessor(SentryOptions options)
{
if (Interlocked.Exchange(ref _registeredScopeEventProcessor, 1) != 0)
{
return;
}

if (!options.TryUseSerilog())
{
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
sentry[bot] marked this conversation as resolved.
return;
}

options.LogWarning(
"The Sentry sink for Serilog registered the Serilog scope event processor automatically, because " +
"UseSerilog() was not called on the options used to initialise Sentry. Events captured before the sink " +
"received its first log event will not have properties from the Serilog LogContext applied. Call " +
"UseSerilog() when initialising Sentry to apply them to every event.");
}

private string FormatLogEvent(LogEvent logEvent)
{
if (_options.TextFormatter is { } formatter)
Expand Down Expand Up @@ -188,6 +198,4 @@ private string FormatLogEvent(LogEvent logEvent)
}
}
}

public void Dispose() => _sdkDisposable?.Dispose();
}
Loading
Loading