From 4af5667afecf678582c05041d34e1960653e5df2 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 28 Oct 2026 12:14:48 +1300 Subject: [PATCH 1/4] docs: add an ASP.NET Core NLog sample and stop advising AddSentry() when another integration handles ILogger The sample routes ILogger through NLog with ReplaceLoggerFactory, so Sentry's own ILogger providers stand aside and each entry reaches Sentry once. The ClearProviders() warning and the AddSentry() remarks told apps to add Sentry's providers back, which sends every entry twice when NLog, Serilog or log4net already forward ILogger entries to Sentry. Refs #5763 Co-Authored-By: Claude Opus 5.5 --- .generated.NoMobile.slnx | 1 + Sentry-CI-Build-Linux-NoMobile.slnf | 1 + Sentry-CI-Build-Linux.slnf | 1 + Sentry-CI-Build-Windows-arm64.slnf | 1 + Sentry-CI-Build-Windows.slnf | 1 + Sentry-CI-Build-macOS.slnf | 1 + Sentry.slnx | 1 + SentryAspNetCore.slnf | 1 + SentryNoMobile.slnf | 1 + .../Sentry.Samples.AspNetCore.NLog/Program.cs | 42 +++++++++++++++++++ .../Properties/launchSettings.json | 12 ++++++ .../Sentry.Samples.AspNetCore.NLog.csproj | 16 +++++++ .../LoggingBuilderExtensions.cs | 4 ++ .../SentryLoggingOptions.cs | 8 ++-- 14 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 samples/Sentry.Samples.AspNetCore.NLog/Program.cs create mode 100644 samples/Sentry.Samples.AspNetCore.NLog/Properties/launchSettings.json create mode 100644 samples/Sentry.Samples.AspNetCore.NLog/Sentry.Samples.AspNetCore.NLog.csproj diff --git a/.generated.NoMobile.slnx b/.generated.NoMobile.slnx index 4a47f6ecab..4742240f65 100644 --- a/.generated.NoMobile.slnx +++ b/.generated.NoMobile.slnx @@ -89,6 +89,7 @@ + diff --git a/Sentry-CI-Build-Linux-NoMobile.slnf b/Sentry-CI-Build-Linux-NoMobile.slnf index ce4e8725db..1e150ccacc 100644 --- a/Sentry-CI-Build-Linux-NoMobile.slnf +++ b/Sentry-CI-Build-Linux-NoMobile.slnf @@ -9,6 +9,7 @@ "samples\\Sentry.Samples.AspNetCore.Blazor.Wasm\\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "samples\\Sentry.Samples.AspNetCore.Grpc\\Sentry.Samples.AspNetCore.Grpc.csproj", "samples\\Sentry.Samples.AspNetCore.Mvc\\Sentry.Samples.AspNetCore.Mvc.csproj", + "samples\\Sentry.Samples.AspNetCore.NLog\\Sentry.Samples.AspNetCore.NLog.csproj", "samples\\Sentry.Samples.AspNetCore.Serilog\\Sentry.Samples.AspNetCore.Serilog.csproj", "samples\\Sentry.Samples.AspNetCore.WebAPI.Profiling\\Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj", "samples\\Sentry.Samples.Aws.Lambda.AspNetCoreServer\\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", diff --git a/Sentry-CI-Build-Linux.slnf b/Sentry-CI-Build-Linux.slnf index 843d20b98f..51e1b0f221 100644 --- a/Sentry-CI-Build-Linux.slnf +++ b/Sentry-CI-Build-Linux.slnf @@ -10,6 +10,7 @@ "samples\\Sentry.Samples.AspNetCore.Blazor.Wasm\\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "samples\\Sentry.Samples.AspNetCore.Grpc\\Sentry.Samples.AspNetCore.Grpc.csproj", "samples\\Sentry.Samples.AspNetCore.Mvc\\Sentry.Samples.AspNetCore.Mvc.csproj", + "samples\\Sentry.Samples.AspNetCore.NLog\\Sentry.Samples.AspNetCore.NLog.csproj", "samples\\Sentry.Samples.AspNetCore.Serilog\\Sentry.Samples.AspNetCore.Serilog.csproj", "samples\\Sentry.Samples.AspNetCore.WebAPI.Profiling\\Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj", "samples\\Sentry.Samples.Aws.Lambda.AspNetCoreServer\\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", diff --git a/Sentry-CI-Build-Windows-arm64.slnf b/Sentry-CI-Build-Windows-arm64.slnf index 164adcf65d..e57d3e4d8b 100644 --- a/Sentry-CI-Build-Windows-arm64.slnf +++ b/Sentry-CI-Build-Windows-arm64.slnf @@ -11,6 +11,7 @@ "samples\\Sentry.Samples.AspNetCore.Blazor.Wasm\\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "samples\\Sentry.Samples.AspNetCore.Grpc\\Sentry.Samples.AspNetCore.Grpc.csproj", "samples\\Sentry.Samples.AspNetCore.Mvc\\Sentry.Samples.AspNetCore.Mvc.csproj", + "samples\\Sentry.Samples.AspNetCore.NLog\\Sentry.Samples.AspNetCore.NLog.csproj", "samples\\Sentry.Samples.AspNetCore.Serilog\\Sentry.Samples.AspNetCore.Serilog.csproj", "samples\\Sentry.Samples.AspNetCore.WebAPI.Profiling\\Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj", "samples\\Sentry.Samples.Aws.Lambda.AspNetCoreServer\\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", diff --git a/Sentry-CI-Build-Windows.slnf b/Sentry-CI-Build-Windows.slnf index b14e45d2c4..66699079f9 100644 --- a/Sentry-CI-Build-Windows.slnf +++ b/Sentry-CI-Build-Windows.slnf @@ -11,6 +11,7 @@ "samples\\Sentry.Samples.AspNetCore.Blazor.Wasm\\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "samples\\Sentry.Samples.AspNetCore.Grpc\\Sentry.Samples.AspNetCore.Grpc.csproj", "samples\\Sentry.Samples.AspNetCore.Mvc\\Sentry.Samples.AspNetCore.Mvc.csproj", + "samples\\Sentry.Samples.AspNetCore.NLog\\Sentry.Samples.AspNetCore.NLog.csproj", "samples\\Sentry.Samples.AspNetCore.Serilog\\Sentry.Samples.AspNetCore.Serilog.csproj", "samples\\Sentry.Samples.AspNetCore.WebAPI.Profiling\\Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj", "samples\\Sentry.Samples.Aws.Lambda.AspNetCoreServer\\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf index bce6cf8fec..5a418b6c8c 100644 --- a/Sentry-CI-Build-macOS.slnf +++ b/Sentry-CI-Build-macOS.slnf @@ -12,6 +12,7 @@ "samples\\Sentry.Samples.AspNetCore.Blazor.Wasm\\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "samples\\Sentry.Samples.AspNetCore.Grpc\\Sentry.Samples.AspNetCore.Grpc.csproj", "samples\\Sentry.Samples.AspNetCore.Mvc\\Sentry.Samples.AspNetCore.Mvc.csproj", + "samples\\Sentry.Samples.AspNetCore.NLog\\Sentry.Samples.AspNetCore.NLog.csproj", "samples\\Sentry.Samples.AspNetCore.Serilog\\Sentry.Samples.AspNetCore.Serilog.csproj", "samples\\Sentry.Samples.AspNetCore.WebAPI.Profiling\\Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj", "samples\\Sentry.Samples.Aws.Lambda.AspNetCoreServer\\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", diff --git a/Sentry.slnx b/Sentry.slnx index 4a47f6ecab..4742240f65 100644 --- a/Sentry.slnx +++ b/Sentry.slnx @@ -89,6 +89,7 @@ + diff --git a/SentryAspNetCore.slnf b/SentryAspNetCore.slnf index ccd8904461..b64b079722 100644 --- a/SentryAspNetCore.slnf +++ b/SentryAspNetCore.slnf @@ -7,6 +7,7 @@ "samples\\Sentry.Samples.AspNetCore.Blazor.Wasm\\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "samples\\Sentry.Samples.AspNetCore.Grpc\\Sentry.Samples.AspNetCore.Grpc.csproj", "samples\\Sentry.Samples.AspNetCore.Mvc\\Sentry.Samples.AspNetCore.Mvc.csproj", + "samples\\Sentry.Samples.AspNetCore.NLog\\Sentry.Samples.AspNetCore.NLog.csproj", "samples\\Sentry.Samples.AspNetCore.Serilog\\Sentry.Samples.AspNetCore.Serilog.csproj", "samples\\Sentry.Samples.AspNetCore.WebAPI.Profiling\\Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj", "samples\\Sentry.Samples.Aws.Lambda.AspNetCoreServer\\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", diff --git a/SentryNoMobile.slnf b/SentryNoMobile.slnf index 206615ce08..9e2060f47b 100644 --- a/SentryNoMobile.slnf +++ b/SentryNoMobile.slnf @@ -9,6 +9,7 @@ "samples\\Sentry.Samples.AspNetCore.Blazor.Wasm\\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "samples\\Sentry.Samples.AspNetCore.Grpc\\Sentry.Samples.AspNetCore.Grpc.csproj", "samples\\Sentry.Samples.AspNetCore.Mvc\\Sentry.Samples.AspNetCore.Mvc.csproj", + "samples\\Sentry.Samples.AspNetCore.NLog\\Sentry.Samples.AspNetCore.NLog.csproj", "samples\\Sentry.Samples.AspNetCore.Serilog\\Sentry.Samples.AspNetCore.Serilog.csproj", "samples\\Sentry.Samples.AspNetCore.WebAPI.Profiling\\Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj", "samples\\Sentry.Samples.Aws.Lambda.AspNetCoreServer\\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", diff --git a/samples/Sentry.Samples.AspNetCore.NLog/Program.cs b/samples/Sentry.Samples.AspNetCore.NLog/Program.cs new file mode 100644 index 0000000000..0be5b9eed1 --- /dev/null +++ b/samples/Sentry.Samples.AspNetCore.NLog/Program.cs @@ -0,0 +1,42 @@ +using NLog; +using NLog.Config; +using NLog.Targets; +using NLog.Web; + +// Configure NLog to send logs to Sentry. This only adds the Sentry target: the SDK is initialized by UseSentry below. +var nlogConfig = new LoggingConfiguration(); +nlogConfig.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, new ColoredConsoleTarget("console")); +nlogConfig.AddSentry(); +LogManager.Configuration = nlogConfig; + +var builder = WebApplication.CreateBuilder(args); + +// Send ILogger entries through NLog. ReplaceLoggerFactory stops the ILogger providers that UseSentry adds from also +// capturing every entry, so each one reaches Sentry once, through the NLog target, whatever order these calls are in. +builder.Host.UseNLog(new NLogAspNetCoreOptions { ReplaceLoggerFactory = true }); + +builder.UseSentry(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; +#else + options.Dsn = EnvironmentVariables.Dsn; +#endif +}); + +var app = builder.Build(); + +app.MapGet("/", (ILogger logger) => +{ + logger.LogInformation("Stored as a breadcrumb"); + logger.LogError("Sent to Sentry as a single event"); + return "Logged an error through ILogger. Check Sentry for the event."; +}); + +app.MapGet("/throw", () => { throw new Exception("An exception thrown from the ASP.NET Core pipeline"); }); + +app.Run(); + +LogManager.Shutdown(); diff --git a/samples/Sentry.Samples.AspNetCore.NLog/Properties/launchSettings.json b/samples/Sentry.Samples.AspNetCore.NLog/Properties/launchSettings.json new file mode 100644 index 0000000000..a13893590a --- /dev/null +++ b/samples/Sentry.Samples.AspNetCore.NLog/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Sentry.Samples.AspNetCore.NLog": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5164" + } + } +} diff --git a/samples/Sentry.Samples.AspNetCore.NLog/Sentry.Samples.AspNetCore.NLog.csproj b/samples/Sentry.Samples.AspNetCore.NLog/Sentry.Samples.AspNetCore.NLog.csproj new file mode 100644 index 0000000000..629da0c982 --- /dev/null +++ b/samples/Sentry.Samples.AspNetCore.NLog/Sentry.Samples.AspNetCore.NLog.csproj @@ -0,0 +1,16 @@ + + + + net11.0 + + + + + + + + + + + + diff --git a/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs b/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs index 7543721fbf..90d8765ac5 100644 --- a/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs +++ b/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs @@ -24,6 +24,8 @@ public static class LoggingBuilderExtensions /// In an app that calls UseSentry, which already adds the logging integration, this has no effect unless the /// app removed Sentry's logger providers, for example with ClearProviders(). It then adds back the providers /// that UseSentry added, which use the host's Sentry options. + /// Don't call it to add them back when ILogger entries already reach Sentry through another integration, such + /// as NLog, Serilog or log4net: each entry would then be sent twice. /// /// The builder. public static ILoggingBuilder AddSentry(this ILoggingBuilder builder) @@ -48,6 +50,8 @@ public static ILoggingBuilder AddSentry(this ILoggingBuilder builder, string dsn /// app removed Sentry's logger providers, for example with ClearProviders(). It then adds back the providers /// that UseSentry added, which use the host's Sentry options, so is /// ignored. + /// Don't call it to add them back when ILogger entries already reach Sentry through another integration, such + /// as NLog, Serilog or log4net: each entry would then be sent twice. /// /// The builder. /// The options configuration. diff --git a/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs b/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs index 817656d5c6..8a720210be 100644 --- a/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs +++ b/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs @@ -28,10 +28,10 @@ public class SentryLoggingOptions "logging through the host's Sentry options."; internal const string HostLoggerProvidersRemoved = - "Sentry's logger providers were removed after UseSentry added them, for example by ClearProviders(), so log " + - "entries from ILogger don't reach Sentry. Call AddSentry() on the logging builder after ClearProviders(), " + - "for example builder.Logging.AddSentry(), to add them back. If you send logs to Sentry through another " + - "integration, such as NLog, Serilog or log4net, you can ignore this."; + "Sentry's logger providers were removed after UseSentry added them, for example by ClearProviders(). If " + + "ILogger entries reach Sentry through another integration, such as NLog, Serilog or log4net, this is " + + "expected: don't add them back, or each entry is sent twice. Otherwise ILogger entries don't reach Sentry: " + + "call AddSentry() on the logging builder after ClearProviders(), for example builder.Logging.AddSentry()."; /// /// Not supported. The logging integration no longer initializes the SDK. From e87deb5d0c1bdcf6807f3d75d7cf92c1fc9dbaad Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 28 Oct 2026 15:01:36 +1300 Subject: [PATCH 2/4] docs: show breadcrumb, event and structured log levels in the ASP.NET Core NLog sample Co-Authored-By: Claude Opus 5.5 --- .../Sentry.Samples.AspNetCore.NLog/Program.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/samples/Sentry.Samples.AspNetCore.NLog/Program.cs b/samples/Sentry.Samples.AspNetCore.NLog/Program.cs index 0be5b9eed1..ca75222ccf 100644 --- a/samples/Sentry.Samples.AspNetCore.NLog/Program.cs +++ b/samples/Sentry.Samples.AspNetCore.NLog/Program.cs @@ -2,11 +2,22 @@ using NLog.Config; using NLog.Targets; using NLog.Web; +using Sentry.NLog; // Configure NLog to send logs to Sentry. This only adds the Sentry target: the SDK is initialized by UseSentry below. +var sentryTarget = new SentryTarget(new SentryNLogOptions +{ + // Warn and higher are added as breadcrumbs (default is Info) + MinimumBreadcrumbLevel = NLog.LogLevel.Warn, + // Error and higher are sent as events (default is Error) + MinimumEventLevel = NLog.LogLevel.Error, +}); + var nlogConfig = new LoggingConfiguration(); nlogConfig.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, new ColoredConsoleTarget("console")); -nlogConfig.AddSentry(); +// Every entry that reaches the Sentry target is sent as a structured log, so this rule sets the minimum level for logs. +// Info and higher are sent as logs. Breadcrumbs and events can only use entries that pass this rule. +nlogConfig.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, sentryTarget); LogManager.Configuration = nlogConfig; var builder = WebApplication.CreateBuilder(args); @@ -30,9 +41,11 @@ app.MapGet("/", (ILogger logger) => { - logger.LogInformation("Stored as a breadcrumb"); - logger.LogError("Sent to Sentry as a single event"); - return "Logged an error through ILogger. Check Sentry for the event."; + logger.LogDebug("Below the Sentry rule's minimum level, so not sent to Sentry"); + logger.LogInformation("Sent as a log"); + logger.LogWarning("Sent as a log and added as a breadcrumb"); + logger.LogError("Sent as a log and as an event, which includes the breadcrumb above"); + return "Logged through ILogger at each level. Check Sentry for the logs and the event."; }); app.MapGet("/throw", () => { throw new Exception("An exception thrown from the ASP.NET Core pipeline"); }); From 33b6032248c2c86fd216b9d744480fef539fbe7e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 28 Oct 2026 15:18:39 +1300 Subject: [PATCH 3/4] docs: simplify the Logging.AddSentry() remarks Co-Authored-By: Claude Opus 5.5 --- .../LoggingBuilderExtensions.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs b/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs index 90d8765ac5..155e2b3ba3 100644 --- a/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs +++ b/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs @@ -21,11 +21,9 @@ public static class LoggingBuilderExtensions /// Adds the Sentry logging integration. /// /// - /// In an app that calls UseSentry, which already adds the logging integration, this has no effect unless the - /// app removed Sentry's logger providers, for example with ClearProviders(). It then adds back the providers - /// that UseSentry added, which use the host's Sentry options. - /// Don't call it to add them back when ILogger entries already reach Sentry through another integration, such - /// as NLog, Serilog or log4net: each entry would then be sent twice. + /// Apps that call UseSentry don't need this, because UseSentry already adds the logging integration. + /// If your app calls ClearProviders() after calling UseSentry that would also remove Sentry's logging + /// integration - in which case this method can be used to add Sentry's providers back again. /// /// The builder. public static ILoggingBuilder AddSentry(this ILoggingBuilder builder) @@ -46,12 +44,10 @@ public static ILoggingBuilder AddSentry(this ILoggingBuilder builder, string dsn /// Adds the Sentry logging integration. /// /// - /// In an app that calls UseSentry, which already adds the logging integration, this has no effect unless the - /// app removed Sentry's logger providers, for example with ClearProviders(). It then adds back the providers - /// that UseSentry added, which use the host's Sentry options, so is - /// ignored. - /// Don't call it to add them back when ILogger entries already reach Sentry through another integration, such - /// as NLog, Serilog or log4net: each entry would then be sent twice. + /// Apps that call UseSentry don't need this, because UseSentry already adds the logging integration. + /// If your app calls ClearProviders() after calling UseSentry that would also remove Sentry's logging + /// integration - in which case this method can be used to add Sentry's providers back again. The providers it adds + /// back use the host's Sentry options, so is ignored. /// /// The builder. /// The options configuration. From 69d16a21832a21737daef03726f79eb5574b4498 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 28 Oct 2026 15:21:52 +1300 Subject: [PATCH 4/4] Apply suggestion from @jamescrosswell --- src/Sentry.Extensions.Logging/SentryLoggingOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs b/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs index 8a720210be..48284d6d7e 100644 --- a/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs +++ b/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs @@ -30,8 +30,8 @@ public class SentryLoggingOptions internal const string HostLoggerProvidersRemoved = "Sentry's logger providers were removed after UseSentry added them, for example by ClearProviders(). If " + "ILogger entries reach Sentry through another integration, such as NLog, Serilog or log4net, this is " + - "expected: don't add them back, or each entry is sent twice. Otherwise ILogger entries don't reach Sentry: " + - "call AddSentry() on the logging builder after ClearProviders(), for example builder.Logging.AddSentry()."; + "expected (it prevents duplicates being sent to Sentry). Otherwise if ILogger entries don't reach Sentry, you can " + + "call AddSentry() on the logging builder after ClearProviders()."; /// /// Not supported. The logging integration no longer initializes the SDK.