Skip to content

Generic host: ClearProviders() after UseSentry silently disables the ILogger integration, or the SDK entirely #5755

Closed

Description

@jamescrosswell

Problem

UseSentry in the new Sentry.Extensions.Hosting package does not initialise the SDK directly. It registers services, and initialisation happens the first time Func<IHub> is resolved. Two things resolve it:

  1. SentryHostedService — its constructor does _ = services.GetRequiredService<Func<IHub>>(). Runs when the host starts.
  2. SentryLoggerProvider — its factory calls GetRequiredService<IHub>(), so it initialises Sentry as a side effect whenever ILoggerFactory is first materialised.

ILoggingBuilder.ClearProviders() is services.RemoveAll<ILoggerProvider>(). Called after UseSentry, it removes both of Sentry's logger providers — and with them trigger 2 and the whole ILogger → Sentry bridge. The Func<IHub> registration and the IHostedService registration are not ILoggerProviders, so they survive. The result is a partial, silent failure.

Happy path — ClearProviders() before UseSentry

var builder = Host.CreateApplicationBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConsole();

builder.UseSentry(options => options.Dsn = "...");  // registered after the clear

var host = builder.Build();
await host.RunAsync();

Sentry's logger providers survive, so logger.LogError(...) reaches Sentry and the SDK has two independent initialisation triggers.

Unhappy path 1 — ClearProviders() after UseSentry, host started

var builder = Host.CreateApplicationBuilder(args);

builder.UseSentry(options => options.Dsn = "...");  // registers the logger providers

builder.Logging.ClearProviders();                   // ...and this removes them
builder.Logging.AddConsole();

var host = builder.Build();
await host.RunAsync();

The SDK initialises, because SentryHostedService still runs at host start. Unhandled exceptions and explicit SentrySdk.CaptureException work as expected. But the ILogger integration is gone: logger.LogError(...) produces no event, no breadcrumbs and no structured logs. Nothing in the output indicates this.

Unhappy path 2 — same, but the host is never started

var builder = Host.CreateApplicationBuilder(args);

builder.UseSentry(options => options.Dsn = "...");

builder.Logging.ClearProviders();
builder.Logging.AddConsole();

var host = builder.Build();   // no Sentry logger provider -> nothing resolves Func<IHub>
// no StartAsync/RunAsync -> SentryHostedService is never constructed

try
{
    await DoWork(host.Services);
}
catch (Exception e)
{
    SentrySdk.CaptureException(e);   // no-op: the hub was never initialised
    Environment.Exit(1);
}

Both triggers are gone, so the SDK never initialises and nothing is reported at all. Building a host and resolving services off it without starting it is a normal shape for CLI-style and scheduled-job apps.

Why this is easy to hit

In 6.x, the initialisation registration and the logger provider registration were the same call on the same ILoggingBuilder:

.ConfigureLogging(logging =>
{
    logging.ClearProviders();
    logging.AddSentry(options => options.Dsn = "...");   // naturally lands after the clear
})

Clear-then-add is the obvious way to write that, so Sentry's registration came last and survived.

v7 splits the two across levels — UseSentry on the host builder, ClearProviders() on the logging builder — which inverts the natural order. The 7.0.0 upgrade notes say to "initialize Sentry first with SentrySdk.Init, or UseSentry", and samples/Sentry.Samples.GenericHost puts builder.UseSentry(...) at the top of the chain. Someone upgrading a worker service that clears providers, and following that guidance, lands on unhappy path 1.

Impact

Order Host started 6.x 7.0.0-rc.2
ClearProviders() → Sentry either works works
Sentry → ClearProviders() started SDK never initialises SDK initialises; ILogger bridge silently gone
Sentry → ClearProviders() never SDK never initialises SDK never initialises

The middle row is the regression that worries me: an app that captured logger.LogError events in 6.x stops doing so, with no error, no warning and no change the author associates with Sentry.

test/Sentry.Extensions.Hosting.Tests/SentryHostedServiceTests.cs has StartAsync_LoggerProvidersCleared_SdkRunsBeforeAppHostedServiceStarts, which covers exactly this configuration and asserts that the SDK starts — but not that the logging integration survived the clear, which is the part that breaks.

Possible improvements

  1. Warn when it happens. In SentryHostedService.StartAsync, check whether a SentryLoggerProvider still resolves, and log a warning when UseSentry ran but the providers are gone. That turns both unhappy paths into diagnosable ones, and fits beside the existing warning for a web app missing the ASP.NET Core integration.
  2. Make initialisation independent of logger providers and of host start. Host's constructor resolves IOptions<HostOptions>, so an IConfigureOptions<HostOptions> registered by UseSentry would fire during Build() in both cases above. It works, but it leans on a hosting implementation detail and would fire even when a host is built only to inspect its services — worth weighing against simply documenting that the SDK starts when the host starts.
  3. Re-register the providers defensively, so a later ClearProviders() cannot leave UseSentry half-applied.
  4. Document the ordering constraint on the generic host page, and state that initialisation happens at host start — so an app that never starts its host knows to call SentrySdk.Init or start the host.

Found by source inspection against the 7.0.0-rc.2 tag while dogfooding the release; happy to put up a runnable repro for all three cases if that would help.

Activity

  1. linear-code commented on Oct 20, 2026

    @linear-code
  2. added
    .NETPull requests that update .net code
    BugSomething isn't working
    on Oct 20, 2026
  3. jamescrosswell commented on Oct 20, 2026

    @jamescrosswell
    CollaboratorAuthor

    Reproduced against Sentry.Extensions.Hosting 7.0.0-rc.2 on .NET 9. One program, three scenarios, BeforeSend returning null so nothing leaves the process.

    var events = new List<string>();
    
    void Configure(SentryGenericHostOptions o)
    {
        o.Dsn = "https://abc123@o1.ingest.sentry.io/1";
        o.AutoSessionTracking = false;
        o.SetBeforeSend((e, _) =>
        {
            events.Add($"event environment={e.Environment}");
            return null;
        });
    }
    
    var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings
    {
        EnvironmentName = "Staging",
        DisableDefaults = true,
    });
    
    switch (scenario)
    {
        case "happy":
            builder.Logging.ClearProviders();
            builder.UseSentry(Configure);
            break;
        case "unhappy1":
        case "unhappy2":
            builder.UseSentry(Configure);
            builder.Logging.ClearProviders();
            break;
    }
    
    var host = builder.Build();
    Console.WriteLine($"IsEnabled after Build = {SentrySdk.IsEnabled}");
    
    if (scenario != "unhappy2")
    {
        await host.StartAsync();
        Console.WriteLine($"IsEnabled after Start = {SentrySdk.IsEnabled}");
    }
    
    host.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Repro").LogError("probe via ILogger");
    SentrySdk.CaptureException(new InvalidOperationException("probe via CaptureException"));
    SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)).GetAwaiter().GetResult();
    
    Console.WriteLine($"events captured = {events.Count}");

    Output:

    [happy] HostEnvironment=Staging
    [happy] IsEnabled after Build  = True
    [happy] IsEnabled after Start  = True
    [happy] events captured = 2
    [happy]   event environment=staging
    [happy]   event environment=staging
    
    [unhappy1] HostEnvironment=Staging
    [unhappy1] IsEnabled after Build  = False
    [unhappy1] IsEnabled after Start  = True
    [unhappy1] events captured = 1
    [unhappy1]   event environment=staging
    
    [unhappy2] HostEnvironment=Staging
    [unhappy2] IsEnabled after Build  = False
    [unhappy2] events captured = 0
    

    Reading the three:

    • happy — both triggers present. IsEnabled is already true after Build(), because materialising ILoggerFactory constructs SentryLoggerProvider, which resolves IHub. Both probes are captured.
    • unhappy1 — IsEnabled is false after Build() and true after StartAsync(), so SentryHostedService is doing the initialising. Only one of the two probes is captured: SentrySdk.CaptureException works, and logger.LogError(...) produces nothing. That is the silent half — the SDK looks healthy and the ILogger integration is gone.
    • unhappy2 — identical configuration, host never started. Nothing is captured at all; SentrySdk.CaptureException is a no-op.

    Also confirms the environment bridge is working as intended in all cases where the SDK runs: EnvironmentName = "Staging" on the host comes out as environment=staging on the event, with no SENTRY_ENVIRONMENT set.

    Happy to turn the unhappy1 case into a failing test against SentryHostedServiceTests if you'd like it alongside the existing StartAsync_LoggerProvidersCleared_SdkRunsBeforeAppHostedServiceStarts — asserting that a SentryLoggerProvider still resolves, which is the assertion that would have caught this.

  4. self-assigned this
    on Oct 21, 2026
  5. added this to the 7.0.0 milestone on Oct 21, 2026
  6. ric-oliv commented on Oct 21, 2026

    @ric-oliv
    Member

    Thanks for the repro, James! I ran the same scenarios against the 7.0.0-rc.2 packages on net10, with both IHostApplicationBuilder and IHostBuilder, and got the same results. I also tried a few more setups, and two of them change how I'd fix this.

    Setup SDK on after Build() Events captured
    Hosting rc.2, UseSentry then ClearProviders(), host started no, it starts at StartAsync only CaptureException
    Sentry.AspNetCore 6.12.1, UseSentry then ClearProviders() no, it starts at StartAsync only CaptureException
    Sentry.AspNetCore rc.2, same order no, it starts at StartAsync only CaptureException
    Hosting rc.2 with Services.AddSerilog(), no ClearProviders(), host never started no none

    Unhappy path 1 isn't new

    Sentry.AspNetCore already behaves this way in 6.x. In 6.12.1 it drops the ILogger integration exactly like the generic host does in rc.2. That makes sense, since ClearProviders() removes every provider registered before it, ours included. So the generic host now matches ASP.NET Core. The regression only shows up during migration. Moving from Logging.AddSentry() to UseSentry tends to put the Sentry call above ClearProviders().

    Sometimes removing our provider is the right result

    NLog's getting-started guide calls builder.Logging.ClearProviders() and then builder.Host.UseNLog(). In v7 those apps also need UseSentry or SentrySdk.Init, because the NLog target no longer starts the SDK. I tried that shape with AddNLog() and the Sentry NLog target:

    // UseSentry first: each error reaches Sentry once, through the NLog target
    builder.UseSentry(Configure);
    builder.Logging.ClearProviders();
    builder.Logging.AddNLog();
    
    // UseSentry last: LogError("message only") reaches Sentry twice
    builder.Logging.ClearProviders();
    builder.Logging.AddNLog();
    builder.UseSentry(Configure);

    With UseSentry last, the error with an exception arrives once, because duplicate detection drops the second copy. The message-only error arrives twice. So re-registering our providers defensively, option 3, would cause duplicates for these apps. A warning on the host logger would also fire on a setup that's working as intended.

    Unhappy path 2 isn't about ClearProviders()

    Services.AddSerilog() replaces the logger factory, so our providers never get created there either. If that app builds its host and never starts it, the SDK never starts. That happens with UseSentry in its usual place and no ClearProviders() call at all. It's the gap we accepted in #5739, where the SDK starts with the host when logging is replaced. One small thing on option 1 too. The hosted service never runs in path 2, so a warning there can't cover it.

    What I'd suggest

    • Document it on the generic host page and in the 7.0 migration guide. Tell people to call ClearProviders() before UseSentry, explain when the SDK starts, and show NLog and log4net users the setup that avoids duplicates.
    • Add the check from option 1, but log it through the SDK's diagnostic logger. Then it shows up with Debug = true when someone asks why their LogError calls are missing, and it doesn't bother the NLog apps.
    • Look at option 2 on its own. I checked it, and an IConfigureOptions<HostOptions> hook does start the SDK during Build() on both builders. It would close path 2 and the Serilog case. It still relies on Host reading HostOptions in its constructor, and I haven't tried it on the Microsoft.Extensions.Hosting 3.1 floor we use for netstandard2.0. I'd rather decide it separately than squeeze it into this issue.
    • Skip option 3 because of the duplicates.

    About the test, I'd rather not assert that a SentryLoggerProvider still resolves after ClearProviders(), since the NLog setup shows that removing it can be correct. What do you think about a test for the diagnostic message instead? I'm happy to pick up the docs and the warning if that sounds good to you.

  7. jamescrosswell commented on Oct 22, 2026

    @jamescrosswell
    CollaboratorAuthor

    OK agreed on the docs, agreed on the option 1 check via the diagnostic logger, agreed that option 2 deserves its own issue, and agreed on dropping option 3 — defensive re-registration does cause duplicates for the NLog apps.

    However, I'm wondering if we should actually take a different tack and reverse one of our earlier design decisions.

    Alternative - reintroduce builder.Logging.AddSentry()

    What if UseSentry stopped registering the ILogger bridge, and the bridge stayed where the other logging integrations live?

    builder.UseSentry(o => o.Dsn = "...");   // SDK, hub, options. No ILoggerProvider.
    builder.Logging.AddSentry();             // the ILogger -> Sentry bridge

    That gives us:

    • UseSentry becomes order-independent, because nothing a logging call does can remove the SDK. Path 2 via ClearProviders stops existing, and the AddSerilog case is unaffected by it too (though it still needs option 2 to initialise during Build()).
    • The NLog case needs no mechanism at all — those apps just don't call Logging.AddSentry(). No ordering trick, no MinimumEventLevel: None, no heuristic for detecting a configured target, no warning that fires on correct setups.
    • The residual ordering is conventional. AddSentry() vs ClearProviders() is two calls on the same builder, three lines apart, behaving exactly like AddConsole(). Nobody is surprised that clearing providers after adding one removes it.
    • HostLoggerProvidersMarker and RedundantWithHostIntegration go away, since nothing is competing for the bridge any more.

    It would also simplify how we explain this to customers. Separating the concerns makes it one rule — structured logs come from whatever logging integration you added, configured through that integration — and EnableLogs can come off all four host options rather than needing an explanation in the docs about when it matters and when it doesn't.

    It feels like a cleaner design but it's not without some complications...

    • AspNetCore, MAUI, Blazor WASM and the generic host all register providers from UseSentry today.
    • AspNetCore and MAUI register subclasses with framework-specific filters, including AddFilter<SentryAspNetCoreStructuredLoggerProvider>("Sentry.AspNetCore.SentryMiddleware", LogLevel.None), which is there to stop a feedback loop. builder.Logging.AddSentry() would have to pick the right provider based on the registered options type... and it has to be done without circular dependencies. That's possible, I think - just might require some trickery.
    • It flips a default that's wider than logs. EnableLogs only ever gated structured logs; creating events and breadcrumbs from logs has always happened automatically without needing to be enabled explicitly. If we go down this path, an app that only calls UseSentry would stop turning logger.LogError into issues... quite a big call.

    Worth desiding up front whethere we want to go down this route or try to patch up the design we already have .

  8. jamescrosswell commented on Oct 22, 2026

    @jamescrosswell
    CollaboratorAuthor

    Alternatively, something to complete/complement version of your option 1...

    The natural fix for unhappy path 1 — "my LogError calls stopped reaching Sentry, let me add the logging integration back" — does not work:

    UseSentry -> clear                                   | events=0 | providers=[]
    UseSentry -> clear -> Logging.AddSentry()            | events=0 | providers=[NullLoggerProvider, NullLoggerProvider]
    clear -> Logging.AddSentry() only (no UseSentry)     | events=1 | providers=[SentryLoggerProvider, SentryStructuredLoggerProvider]
    

    HostLoggerProvidersMarker is registered with TryAddSingleton and isn't an ILoggerProvider, so ClearProviders() removes the providers but leaves the marker behind. Logging.AddSentry() then sees the marker, concludes it would be duplicating providers that no longer exist, installs NullLoggerProvider twice, and logs RedundantWithHostIntegration — telling the user their fix is unnecessary.

    So today the only remedy is reordering the calls. That makes the docs change harder than it looks: there's nothing else we can tell someone who has already hit it.

    Potential fix

    LoggingBuilderExtensions uses HubAdapter.Instance rather than GetRequiredService<IHub>() per #103, so it never captures a hub. That means the recovered bridge forwards to the current hub but initialises nothing, unlike the host integration's own provider.

    So fixing the marker alone leaves an app in unhappy path 2 with a perfectly good bridge to a disabled SDK.

    Part 1 — initialisation stops depending on logger providers

    You mentioned you'd checked that an IConfigureOptions<HostOptions> hook starts the SDK during Build() on both builders. That's the piece:

    internal sealed class SentrySdkInitializer(IServiceProvider services) : IConfigureOptions<HostOptions>
    {
        // Nothing to configure. This exists because Host's constructor resolves IOptions<HostOptions>,
        // which runs every IConfigureOptions<HostOptions> — a hook that fires during Build() whatever
        // the app did to its logging.
        public void Configure(HostOptions options) => _ = services.GetRequiredService<Func<IHub>>();
    }

    Resolving Func<IHub> is already what initialises the SDK. This only pulls it forward to Build() instead of leaving it to whichever of SentryHostedService or SentryLoggerProvider gets there first. SentryHostedService stays as it is, since it's what registers ApplicationStopped -> SentrySdk.Close for started hosts.

    This closes unhappy path 2 and your AddSerilog case, neither of which depends on a logger provider any more.

    Part 2 — the marker shares the lifetime of what it stands for

    internal sealed class HostLoggerProvidersMarker : ILoggerProvider
    {
        public ILogger CreateLogger(string categoryName) => NullLogger.Instance;
        public void Dispose() { }
    }
    
    // at each of the five host integrations, instead of TryAddSingleton<HostLoggerProvidersMarker>()
    services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, HostLoggerProvidersMarker>());

    and Logging.AddSentry() tests the service collection instead of resolving the marker:

    var services = builder.Services;   // captured, so the check sees every later change
    
    builder.Services.AddSingleton<ILoggerProvider>(c =>
        !HostLoggerProvidersMarker.IsRegistered(services)
            ? new SentryLoggerProvider(c.GetRequiredService<IOptions<SentryLoggingOptions>>(), HubAdapter.Instance)
            : /* warn + */ NullLoggerProvider.Instance);

    The capture is load-bearing. A check performed while AddSentry runs would break the AddSentry()-before-UseSentry ordering and send everything twice — which LoggingAddSentry_BeforeUseSentry_DoesNotSendErrorsTwice already pins. Deferring to resolution keeps that order-independent while still noticing the clear.

    Effect

    Setup Today With both parts
    ClearProviders() then UseSentry works works
    UseSentry then ClearProviders(), host started SDK on, ILogger dead, AddSentry() cannot fix it SDK on, ILogger dead, AddSentry() fixes it
    UseSentry then ClearProviders(), host never started nothing captured SDK on, AddSentry() restores ILogger
    Services.AddSerilog(), host never started nothing captured SDK on

    Your NLog and log4net apps are untouched throughout: they don't call Logging.AddSentry(), so nothing re-registers our provider and there are no duplicates. No detection heuristics and no opt-out option needed.

    I prototyped part 2 against version7 to check the mechanism — the two existing DoesNotSendErrorsTwice tests keep passing, and the recovery case passes once the host is started. I haven't put a PR up, since this is still State: Needs Discussion and it's your call which direction we take.

    Two things this gives your plan

    • Your diagnostic warning can cover path 2. You noted the hosted service never runs there. With part 1, initialisation happens at Build(), so the check can live there instead and cover both paths. It should test the descriptors rather than the resolved providers, since the order in which Host's constructor resolves IOptions<HostOptions> versus ILogger<Host> isn't something to depend on.
    • The docs get a second remedy. Instead of only "call ClearProviders() before UseSentry", they can say "or add builder.Logging.AddSentry() after it" — which is the advice someone who has already shipped the wrong order actually needs.

    What it still doesn't do

    Unhappy path 1's default is unchanged. UseSentry then ClearProviders() still silently leaves ILogger disconnected; we'd have made the SDK reliable, the failure diagnosable and the fix effective, but someone still has to notice.

    So still worth considering the 'reintroduce builder.Logging.AddSentry()' solution above...

  9. jamescrosswell commented on Oct 27, 2026

    @jamescrosswell
    CollaboratorAuthor

    Closing this — all three parts of the original report are addressed on version7:

    Original symptom Fixed by
    UseSentry then ClearProviders() on a host that is never started captured nothing at all #5772 — SentrySdkInitializer starts the SDK during Build() via IConfigureOptions<HostOptions>, so initialisation no longer depends on Sentry's logger providers surviving. Also covers a replaced logger factory, e.g. Services.AddSerilog().
    Logging.AddSentry() couldn't recover the ILogger integration — it saw the leftover marker and installed NullLoggerProvider twice #5776 — HostLoggerProviders replaces the marker and keeps the descriptors it registered, so AddSentry() rebuilds the host's own providers (keeping the host's options and its filter rules, including the one behind #1284).
    The failure was silent #5789 — with Debug = true the SDK warns when the providers were removed and not added back.

    Option 4 was considered and decided against

    I proposed going further: have UseSentry stop registering the ILogger bridge altogether and move it to an explicit builder.Logging.AddSentry(). That would have made the ordering question disappear rather than be mitigated — nothing a logging-level call does could undo a host-level one, and ClearProviders() before or after UseSentry would stop mattering.

    We're not doing it, for two reasons:

    • It changes the default far beyond this bug. EnableLogs only ever gated structured logs; the ILogger → event and breadcrumb bridge has always been on with UseSentry alone. Under option 4 an app that calls only UseSentry would stop turning logger.LogError into issues, across all four host integrations. That trades one silent surprise for another, with a much larger blast radius.
    • It's breaking, and 7.0 is in RC. It would have to be 7.0 or 8.0, and it isn't worth holding the major for.

    The mitigations above give recoverability and a diagnostic without touching the default, which is the right trade for now. If we revisit it, the cost to weigh is that AddSentry() would need to select the correct host-specific provider per integration — the problem #5776 solved a different way.

    Not covered

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

Metadata

Metadata

Assignees

Labels

.NETPull requests that update .net codeBugSomething isn't workingNext MajorChanges scheduled for the next Major release.

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions