Skip to content

feat(nlog)!: the Sentry target no longer initializes the SDK - #5585

Open
jamescrosswell wants to merge 35 commits into
version7from
feat/no-init-from-logging-nlog-5245
Open

jamescrosswell wants to merge 35 commits into
version7from
feat/no-init-from-logging-nlog-5245

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

The NLog portion of #5245, stacked on #5573 (Serilog) and following the same design. The Sentry target for NLog now only configures the target; Sentry has to be initialized separately via SentrySdk.Init, UseSentry, etc.

Part of #5245

Breaking changes

  • SentryNLogOptions no longer derives from SentryOptions. It carries only target settings.
  • Removed from SentryNLogOptions and SentryTarget: InitializeSdk, Dsn/DsnLayout, Release/ReleaseLayout, Environment/EnvironmentLayout, ShutdownTimeoutSeconds, FlushTimeout/FlushTimeoutSeconds. Events now take release and environment from the options used to initialize Sentry, rather than per-target overrides.
  • When NLog flushes the target, Sentry is flushed using SentryOptions.FlushTimeout from the options used to initialize Sentry. That defaults to 2 seconds; the NLog target previously defaulted to 15. Set FlushTimeout in SentrySdk.Init to keep the old wait.
  • In NLog.config, the dsn, release, environment, initializeSdk, shutdownTimeoutSeconds and flushTimeoutSeconds target attributes are gone, as is setting arbitrary SentryOptions properties through the <options> element (e.g. <options attachStacktrace="true" />). With throwConfigExceptions="true" these now fail config loading.
  • dsn and initializeSdk specifically fail with migration guidance rather than "cannot assign unknown property" — see the tombstones below.
  • The three AddSentry overloads become one supported overload, AddSentry(Action<SentryNLogOptions>? optionsConfig = null, string targetName = "sentry"), plus two tombstones.
  • Events and structured logs no longer report sentry.dotnet.nlog as the SDK name. Sdk.Name identifies the integration that initialized Sentry, and the target identifies itself through the log origin (auto.log.nlog). See Metrics and SentrySdk.Logger logs emitted during a request carry no sentry.sdk.name/sentry.sdk.version on ASP.NET Core #5497.
  • SDK diagnostics are no longer written to NLog's InternalLogger.
  • An AddSentry(o => …) call that only sets target settings still compiles, and nothing in the API can catch it: on 6.x it initialized the SDK itself, taking the DSN from SENTRY_DSN or a [Dsn] assembly attribute. The target therefore warns at runtime — on the first log event at or above MinimumEventLevel, if Sentry is not initialized but a DSN can still be found, it writes one line to NLog's InternalLogger and to standard error, at most once per target. InternalLogger is off by default, so the standard-error line is the one that lands. The shared policy lives in Sentry.Internal.UninitializedSdkWarning, added in feat(serilog)!: the Sentry sink no longer initializes the SDK #5573. Previously, enabling NLog internal logging at any level made the target set the SDK's DiagnosticLogger to write there and turn on Debug. To see SDK diagnostics, set Debug (and optionally DiagnosticLogger) on the options used to initialize Sentry.

Before:

LogManager.Configuration = new LoggingConfiguration()
    .AddSentry("https://key@sentry.io/1", o => o.MinimumEventLevel = LogLevel.Error);

After:

using var _ = SentrySdk.Init(o => o.Dsn = "https://key@sentry.io/1");

LogManager.Configuration = new LoggingConfiguration()
    .AddSentry(o => o.MinimumEventLevel = LogLevel.Error);

Migration guard (tombstones)

Following the pattern established for Serilog in #5611, the v6 entry points that initialized the SDK are kept as tombstones rather than deleted: AddSentry(dsn, …), AddSentry(dsn, targetName, …), SentryTarget.Dsn and SentryTarget.InitializeSdk. Each is [Obsolete(…, error: true)] and throws NotSupportedException with migration guidance.

ObsoleteAttribute has no runtime effect, so NLog still finds these by reflection when binding NLog.config, and the setter throws — which NLog surfaces as an NLogConfigurationException carrying our message. Code callers get a compile error instead of the silent behaviour change.

Without them, a stale dsn="…" in NLog.config produces only NLog's generic "cannot assign unknown property", and with throwConfigExceptions unset that is a warning to InternalLogger — off by default. The app starts, the target attaches, Sentry is never initialized and nothing is reported.

Covered by SentryTargetConfigurationBindingTests: XML config carrying dsn or initializeSdk fails with our message, target-only settings still load, and both overloads throw when invoked by name (as a config provider does) and are obsolete-as-error.

Notes for review

  • targetName moved to the last parameter on purpose. Keeping a (string targetName, Action<SentryNLogOptions>) overload would let existing AddSentry(dsn, o => …) calls keep compiling, with the DSN silently used as the target name and Sentry never initialized. With targetName last, every old DSN-taking call fails to compile instead.
  • Why the flush timeout moved to the SDK options. NLog flushes its targets on LogManager.Flush(), on LogManager.Shutdown() (which NLog also calls itself on process exit, since AutoShutdown is on by default), when the configuration is replaced (including autoReload), and from wrappers such as AutoFlushTargetWrapper. NLog doesn't pass its own timeout down to targets, so the target has to pick one, and the SDK-wide flush it triggers covers events from every integration, not only NLog. Previously the target owned the SDK, so this flush on shutdown was how an NLog-only app got its last events sent. Now the app initializes and disposes Sentry itself, and disposing the handle from SentrySdk.Init flushes on its own. The NLog-triggered flush is still useful for explicit flushes, config reloads and auto-flush wrappers, but it's an SDK operation, so it uses the SDK's setting instead of a target-level duplicate.
  • Why NLogDiagnosticLogger is deleted. Routing SDK diagnostics to InternalLogger was SDK configuration applied at target initialization. The target's options are no longer SentryOptions, and applying it to the live hub options instead would mean the target silently reconfiguring (and switching on Debug for) an SDK the user initialized. With no remaining callers, the internal class was dead code.
  • Unlike Serilog, NLog needs no UseNLog(): tags, user and properties are all applied by the target to the events it creates.
  • The IntegrationTests.Simple snapshot changes are only a stack-frame line/column shift from restructuring the test. Net4_8 got the same edit by hand, and ApiApprovalTests.Run.Net4_8 is a copy of the regenerated DotNet10_0 one, which it matched byte-for-byte beforehand. Neither can regenerate on macOS.
  • The integration tests previously never disposed the SDK the target initialized; they now dispose it via using before verifying.

🤖 Generated with Claude Code

jamescrosswell and others added 6 commits September 14, 2026 12:42
The Sentry sink for Serilog now only configures the sink. Sentry must be
initialized separately (SentrySdk.Init, UseSentry, etc).

- SentrySerilogOptions no longer derives from SentryOptions and only
  carries sink settings; InitializeSdk is removed
- Remove the WriteTo.Sentry(string dsn, ...) overload
- Rename ApplySerilogScopeToEvents() to UseSerilog(), make it idempotent
- The sink logs a one-time diagnostic warning when UseSerilog() was not
  called on the options used to initialize Sentry

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Sentry target for NLog now only configures the target. Sentry must be
initialized separately (SentrySdk.Init, UseSentry, etc).

- SentryNLogOptions no longer derives from SentryOptions and only carries
  target settings; FlushTimeout moves onto it directly
- Remove InitializeSdk, Dsn/DsnLayout, Release/ReleaseLayout,
  Environment/EnvironmentLayout and ShutdownTimeoutSeconds. Events take
  release and environment from the SDK options
- Collapse the AddSentry overloads into
  AddSentry(optionsConfig, targetName); the dsn overloads are removed
- The target no longer routes SDK diagnostics to NLog's InternalLogger

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jamescrosswell jamescrosswell added Breaking Change Binary/Source/Behavioral Breaking Changes. NLog labels Sep 17, 2026
@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.21053% with 3 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (version7@15b78ac). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/Sentry.NLog/SentryTarget.cs 81.25% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             version7    #5585   +/-   ##
===========================================
  Coverage            ?   74.99%           
===========================================
  Files               ?      515           
  Lines               ?    18849           
  Branches            ?     3658           
===========================================
  Hits                ?    14135           
  Misses              ?     3854           
  Partials            ?      860           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread samples/Sentry.Samples.NLog/Program.cs Outdated
Comment thread samples/Sentry.Samples.NLog/README.md Outdated
Comment thread samples/Sentry.Samples.NLog/README.md Outdated
Comment thread src/Sentry.NLog/SentryNLogOptions.cs Outdated
jamescrosswell and others added 2 commits September 17, 2026 15:29
Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>
Remove SentryTarget.FlushTimeoutSeconds and SentryNLogOptions.FlushTimeout.
When NLog flushes the target, the hub is now flushed with the FlushTimeout
from the options used to initialize Sentry, since the target no longer
owns the SDK.

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jamescrosswell and others added 9 commits September 22, 2026 09:30
The sample sets the DSN in code via UseSentry, so the commented-out Dsn
entry is misleading. EnableTracing is declared on BindableSentryOptions
but never applied, so setting it has no effect.

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Emit can run concurrently, so the check-then-set on the warned flag could
let more than one thread log the warning.

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sdk.Name should identify the integration that initialised the hub, which after
this change can no longer be a logging integration. The sink identifies itself
through the log origin (auto.log.serilog) instead.
See #5497.

Events are no longer stamped with sentry.dotnet.serilog, and structured logs no
longer carry it as sentry.sdk.name; both now report the SDK that initialised
Sentry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sdk.Name should identify the integration that initialised the hub, which after
this change can no longer be a logging integration. The target identifies itself
through the log origin (auto.log.nlog) instead.
See #5497.

Events are no longer stamped with sentry.dotnet.nlog, and structured logs no
longer carry it as sentry.sdk.name; both now report the SDK that initialised
Sentry. With no remaining callers, Constants is deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on error

Serilog configuration providers bind sink arguments by parameter name, so
removing the dsn-first overload made them drop `dsn` silently: the sink still
binds, Sentry is never initialized, and nothing is reported. Keeping the
overload as an [Obsolete(error: true)] tombstone that throws makes both
Serilog.Settings.Configuration (appsettings.json) and Serilog.Settings.AppSettings
(app.config) fail loudly with migration guidance, while code callers get a
compile error instead of a type mismatch on the second argument.

The overload mirrors the surviving overload's parameters plus `dsn`. With only
`string dsn` it loses Serilog's overload ranking whenever a configuration
supplies two or more of the surviving arguments, which would restore the silent
behaviour.

Part of #5245

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ration

The migration guard works only because of Serilog's overload ranking, and
nothing exercised that path. These tests bind a sink from IConfiguration
the way a provider does, so a Serilog change that stops selecting the
tombstone fails here rather than silently dropping the DSN again.

Verified they fail without the tombstone overload. Selection behaves the
same on Serilog.Settings.Configuration 3.4.0 (Serilog 2.12) and 10.0.1
(Serilog 4.3); 3.4.0 is referenced to avoid bumping Serilog in the tests.

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jamescrosswell and others added 10 commits September 23, 2026 10:47
…n error

Mirrors the Serilog guard (#5611). The v6 AddSentry(dsn, ...) overloads and
the SentryTarget.Dsn / InitializeSdk properties come back as tombstones:
obsolete-as-error for code callers, throwing NotSupportedException so
NLog.config bindings fail loudly with migration guidance instead of
reporting an unknown property.

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ssor automatically (#5612)

* fix: make the SentryOptions processor collections thread safe

SentryClient enumerates these collections lazily for the whole duration of a capture, and
AddEventProcessor is documented as supporting registration after the SDK is initialised.
They were plain Lists, so appending to one while a capture was in flight threw
InvalidOperationException - which the SDK catches and logs at Debug, silently dropping the
event.

Swap them for ConcurrentBagLite, which snapshots on enumeration. Scope.EventProcessors
already uses it for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(serilog): register the Serilog scope event processor automatically

The sink no longer initialises the SDK, so integrators have to call UseSerilog() on the
options used to initialise Sentry. Forgetting it was only reported as a warning gated behind
Debug and DiagnosticLevel, so in practice it was silent.

The sink now registers SerilogScopeEventProcessor itself: at construction when Sentry is
already initialised, otherwise on the first log event. The sink and the processor live in the
same assembly, so no reflection is needed and this stays AOT safe. UseSerilog() is still the
better option - it applies from the first event rather than from the first log line - and the
warning now says so.

Also fixes a feedback loop this exposed. Emit answered a reentrant log event with another
diagnostic, which Serilog routed straight back into the sink, each message embedding the
last. With DiagnosticLevel at Info that produced 55 MB of logs in 17 seconds and the app
stopped serving requests. The SDK-namespace filter that breaks the cycle now runs before the
reentrancy check instead of after it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Removed unnecessary comments

Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>
Sinks sharing one set of SentryOptions can reach registration concurrently -
each sink's guard is per-instance - so the check and the add have to happen
under a lock, not as check-then-act. The sink now learns from the result
whether it was the one that registered, which is what the warning reports.

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jamescrosswell and others added 2 commits September 24, 2026 10:59
Restores the DSN comment dropped from the Serilog sample's appsettings.json,
pointing at where this sample actually sets it, and records in AGENTS.md that
"prefer no comments" covers the library rather than samples - including their
JSON configuration files.

Part of #5245

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jamescrosswell
jamescrosswell removed this pull request from stack #5603 September 27, 2026 22:03
jamescrosswell and others added 3 commits September 29, 2026 12:12
…try is not initialized

The tombstoned overloads catch everyone who passes a DSN to the sink, but they cannot see
the `WriteTo.Sentry(o => ...)` callback that only sets sink options and gets its DSN from
SENTRY_DSN or a [Dsn] assembly attribute. On 6.x that overload initialized the SDK itself;
now it compiles, nothing calls Init, and the sink drops everything silently.

Warn once, on the first event at or above MinimumEventLevel, when the hub is disabled and a
DSN can still be found. There is no DiagnosticLogger to write to in that state, so the
warning goes to Serilog's SelfLog and to standard error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ry is not initialized

Mirrors the Serilog sink: the tombstoned Dsn/InitializeSdk properties cannot see an
AddSentry(o => ...) call that only sets target options and gets its DSN from SENTRY_DSN or a
[Dsn] assembly attribute, so warn once on the first event at or above MinimumEventLevel when
the hub is disabled and a DSN can still be found. The warning goes to NLog's InternalLogger
and to standard error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added risk: high PR risk score: high and removed risk: medium PR risk score: medium labels Sep 28, 2026
@jamescrosswell jamescrosswell changed the title feat: NLog target no longer initializes the SDK feat(nlog)!: the Sentry target no longer initializes the SDK Sep 29, 2026
Base automatically changed from feat/no-init-from-logging-5245 to version7 September 29, 2026 22:12
…-logging-nlog-5245

version7 now has main merged into it, and #5573 has landed there, so this picks both up.

All four conflicts took version7's side, which is strictly newer in each case:

- AGENTS.md, samples/Sentry.Samples.AspNetCore.Serilog/appsettings.json and
  test/Sentry.Serilog.Tests/SentrySinkTests.cs were resolved when main was merged into
  version7; this branch only carried the pre-merge side of them.
- src/Sentry.Serilog/SentryOptionExtensions.cs: the UseSerilog() doc comment here still said
  the sink "cannot do this for you", which stopped being true when #5612 made the sink
  register the scope processor itself. version7 has the corrected wording.

The merge also brings #5523's breadcrumb hint to the NLog target (hint: exception.ToHint())
and its test, which don't overlap with anything in this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Breaking Change Binary/Source/Behavioral Breaking Changes. NLog risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants