Logging to sentry only sent before app.Run using .NET serilog experimental logging
Package
Sentry
.NET Flavor
.NET Core
.NET Version
9.0.301
OS
Windows
OS Version
25H2
Development Environment
Rider 2024 (Windows)
SDK Version
5.16.0
Self-Hosted Sentry Version
No response
Workload Versions
aspire 8.2.2/8.0.100 VS 17.14.36212.18
UseSentry or SentrySdk.Init call
builder.WebHost.UseSentry(options =>
{
options.Dsn = config.Dsn;
options.Release = config.CommitHash;
options.Environment = config.Environment;
options.TracesSampleRate = 1.0d;
options.SampleRate = 1.0f;
options.SendDefaultPii = true; // sending info about users
options.MaxRequestBodySize = RequestSize.Medium; // includes request body
options.MinimumEventLevel = LogLevel.Warning;
options.CaptureFailedRequests = false;
options.AddExceptionFilterForType<OperationCanceledException>();
options.SetBeforeSend(sentryEvent =>
{
// This happens when the client terminates a request. We can ignore this as there's not much we can about it.
if (sentryEvent.Exception is BadHttpRequestException bre && bre.Message.Contains("Unexpected end of request content"))
{
return null;
}
return sentryEvent;
});
options.UseOpenTelemetry();
});
And also this in the serilog pipeline
private static LoggerConfiguration LogToSentry(LoggerConfiguration loggerConfig, SentryConfiguration config)
{
return loggerConfig.WriteTo.Sentry(options =>
{
options.Dsn = config.Dsn;
options.MinimumBreadcrumbLevel = LogEventLevel.Information;
options.MinimumEventLevel = LogEventLevel.Warning;
options.CaptureFailedRequests = false;
// This doesn't work at this moment. But soon enough, it will start working.
options.Experimental.EnableLogs = true;
options.Experimental.SetBeforeSendLog(log =>
{
// TODO: Theoretically, this should already be filtered out by the logger configuration. Needs testing. Serilog wasn't supported when I wrote this.
if (log.TryGetAttribute(ObservabilityConstants.IsSpamJobAttribute, out var isSpamJob) && isSpamJob is true)
{
return log.Level >= SentryLogLevel.Warning
? log
: null;
}
return log;
});
});
}
Which is called this way:
var logging = new LoggerConfiguration()
.ReadFrom.Configuration(configForSerilogOnly)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.AzureApp(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}");
if (config.Observability.HoneycombApiKey is { } honeycombKey)
{
logging = LogToHoneycomb(logging, config, honeycombKey);
}
if (config.Observability.Sentry is { } sentryConfig)
{
logging = LogToSentry(logging, sentryConfig);
}
if (config.Observability.SeqApiKey is { } seqApiKey)
{
logging = LogToSeq(logging, serviceName: config.Observability.ServiceName, apiKey: seqApiKey);
}
Log.Logger = logging.CreateLogger();
builder.Host.UseSerilog(Log.Logger);
Steps to Reproduce
- Run the application
- Observe both logging and tracing get send to all the collectors during startup
- (sentry + seq locally, sentry + honeycomb in the cloud)
- Mostly the EF core migrations are producing logs, but feel free to add logging to the program.cs class
- app.Run() is called and server starts
- Call some request
- Observe traces are still sent everywhere correctly
- Observe logs are no longer reaching sentry
Expected Result
Logging works even after app.run is called.
Actual Result
Logging is only sent to sentry before app.Run is called. Afterwards, the logs go to console, honeycomb, seq, but not sentry.
I've tried logging manually in Program.cs using Log.Information("21") as well as using
using var scope2 = app.Services.CreateScope();
var logger = scope2.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("21");
And it works in the program.cs file. But not in any request, nor background service.
Thank you, @KaliCZ, for using our new integration so quickly, and for the detailed report.
At first glance, this could be related to initializing 2 Hubs, a bit of a quirk of how Serilog and the Serilog SDK works in conjunction with ASP.NET Core and the ASP.NET Core SDK.
I need a bit more time to take a closer look at it later on,
but could you give it a try to also EnableLogs in the ASP.NET Core UseSentry, and not just in the Serilog WriteTo.Sentry.
Let me know if that works for you. If it does, we probably need more docs around combining ASP.NET Core with Serilog ... I also have a off-the-top-of-my-head idea for an improvement that could make your scenario work as-is. If it does not, I'll need to look more into it.
@Flash0ver We're talking about the builder.WebHost.UseSentry(options => call, right? Because I'm on 5.16 and I don't see any EnableLogs or anything of that kind on the options class.
There is options.ApplySerilogScopeToEvents(); and then there is options.ExperimentalLogging, but AFAICS this only configures the min level which is Trace by default. So I'm not exactly sure what I should add.
Oh ... I may have missed something.
You're in an .NET Aspire app ... but you are using the Sentry.AspNetCore package, right?
Then there should be
builder.WebHost.UseSentry(options =>
{
//..
options.Experimental.EnableLogs = true;
options.Experimental.SetBeforeSendLog(static log => log);
//..
}
5.16.0 has removed Experimental from SentrySdk.Logger.Log.., as we're happy with the shape of the APIs.
But we kept the Experimental on the SentryOptions (and derived) for now, as we are going to have some behavioral changes planned for 5.17.0. With (or shortly after) that, we will re-evaluate the state and plan to remove Experimental for Structured Logging entirely, to also commit to behavioral compatibility in a SemVer fashion.
Unless I dramatically misunderstand something here 🤔.
@Flash0ver just chiming in as I am trying to setup the exact same thing. I tried a few different combinations and as soon as I call this:
builder.Host.UseSerilog(Log.Logger);
Logs stop getting sent to sentry. I configured both the UseSentry() and the WriteTo.Sentry calls:
builder.WebHost.UseSentry(opts =>
{
#pragma warning disable SENTRY0001
opts.Experimental.EnableLogs = includeSentryLogging;
opts.SendDefaultPii = true;
opts.SampleRate = 1.0F;
opts.TracesSampleRate = 1.0F;
#pragma warning restore SENTRY0001
});
LoggerConfiguration logConfig = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Information)
.MinimumLevel.Override("Hangfire", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithProperty("App", appName)
.WriteTo.Console(theme:AnsiConsoleTheme.Literate, outputTemplate:$"[{appName}][{{Timestamp:HH:mm:ss}} {{Level:u3}}] {{Message:lj}}{{NewLine}}{{Exception}}")
.WriteTo.Sentry(opts =>
{
opts.Experimental.EnableLogs = true;
opts.SendDefaultPii = true;
opts.SampleRate = 1.0F;
opts.TracesSampleRate = 1.0F;
opts.Dsn = [redacted]
});
I tried playing with the order too - initializing Sentry before Serilog, removing the WriteTo.Sentry etc - same results!
Happy to give network traces, debug logs, anything needed. I am very keen to use Sentry Logs - so nice having everything in one spot!
I see ... so this seems indeed to be related to a potential "double-Hub" issue.
When using ASP.NET Core ( Microsoft.Extensions.Logging + M.E.DependencyInjection + M.E.Configuration) and Serilog, we tested for scenarios where the Hub is initialized through either Serilog or ASP.NET Core, as well as both ... but we apparently missed a scenario.
@KaliCZ, @odinnix, thanks for listing your setup/init. I'll come back tomorrow with more insights and/or questions for reproduction + diagnostics.
@KaliCZ if you've already initialised Sentry via the WebHost:
builder.WebHost.UseSentry(options =>
{
options.Dsn = config.Dsn;
... then I wouldn't try to reinitialise it via Serilog. Instead, use this overload when telling Serilog to send events to Sentry (this overload configures the Sentry sink without trying to re-initialise Sentry): https://github.com/getsentry/sentry-dotnet/blob/aae73422bd34e427ff2e03928e9cea30e924f8ae/src/Sentry.Serilog/SentrySinkExtensions.cs#L168-L175
Then they should play together properly.
I can confirm, that logging works after replacing
loggerConfig.WriteTo.Sentry(options =>
{
options.Dsn = config.Dsn;
....
with
loggerConfig.WriteTo.Sentry(
minimumBreadcrumbLevel: LogEventLevel.Information,
minimumEventLevel: LogEventLevel.Warning
)
It's not that intuitive that this is a solution, I'm guessing you'll think about how to prevent people from making this mistake.
The bigger issue I see though is that now I have a loggerConfiguration that has half the sentry logging configuration and also the sentry initialization that has the other half:
builder.WebHost.UseSentry(options =>
{
...
options.Experimental.EnableLogs = true;
options.CaptureFailedRequests = false;
options.Experimental.SetBeforeSendLog(log =>
{
if (log.TryGetAttribute(ObservabilityConstants.IsSpamJobAttribute, out var isSpamJob) && isSpamJob is true)
{
return log.Level >= SentryLogLevel.Warning
? log
: null;
}
return log;
});
Which for me means that it's not that obvious that I can get rid of the IsSpamJob filtering above, because it's already defined in the loggerConfiguration. (but sure, if you think about it, sentry is picking up the resulting logs after the logging.Filter)
You could obviously put the logging parameters into the big sentry initialization. Even though I'd prefer having the separate configurations
- separate place for handling logging with the LoggerConfiguration
- separate place for handling tracing with the TracerProviderBuilder
- separate place possibly for metrics into sentry in the future
- separate place for handling exceptions
But I guess it's not that simple, when sentrySDK is doing stuff like breadcrumbs etc.
It's not that intuitive that this is a solution
Yeah, it's tricky.
Some people are only using Serilog (e.g. in a Console application). In that case, we want them to be able to configure Sentry with a single call (via the Sink extension).
Other people are only using ASP.NET Core and we want them to be able to configure Sentry in a single call (via the HostBuilder extension).
If people are using both ASP.NET Core and Serilog, however, they need to be able to configure a Sentry sink on Serilog... but without initialising Sentry (since that will be done via the HostBuilder extension).
However, we don't know, when the Sink is being added for Serilog, whether Serilog is being used in isolation or in combination with some other integration (like our ASP.NET Core integration).
We'd love to find a more intuitive solution for this... we just haven't come up with one yet.
I'm still not able to get logs flowing with Serilog.
Startup:
Log.Logger.Information("Adding Sentry");
builder.WebHost.UseSentry(opts =>
{
#pragma warning disable SENTRY0001
opts.Experimental.EnableLogs = true;
opts.SendDefaultPii = true;
opts.SampleRate = 1.0F;
opts.TracesSampleRate = 1.0F;
#pragma warning restore SENTRY0001
});
LoggerConfiguration logConfig = DefaultLogger.GetDefaultLogger(appName, overrideLevel, includeEfLogs);
logConfig.ReadFrom.Configuration(config);
Log.Logger = logConfig.CreateLogger();
builder.Services.AddSingleton(Log.Logger);
builder.Host.UseSerilog(Log.Logger);
Removed the init from the Serilog config:
public static LoggerConfiguration GetDefaultLogger(string appName, LogEventLevel logLevel, bool includeEfLogs)
{
bool isProduction = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT").ToLowerSafe() == "production";
LoggerConfiguration logConfig = new LoggerConfiguration()
.Filter.ByExcluding(e => e.Properties.ContainsKey("RequestPath") && e.Properties["RequestPath"].ToString().ToLower().Contains("health"))
.Filter.ByExcluding(e => e.Properties.ContainsKey("RequestPath") && e.Properties["RequestPath"].ToString().ToLower().Contains("hangfire"))
.Filter.ByExcluding(e => e.Properties.ContainsKey("RequestPath") && e.Properties["RequestPath"].ToString().ToLower().Contains("negotiate"))
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", logLevel)
.MinimumLevel.Override("Hangfire", logLevel)
.MinimumLevel.Override("Microsoft", logLevel)
.MinimumLevel.Override("Microsoft.AspNetCore", logLevel)
.MinimumLevel.Override("Microsoft.AspNetCore.Server.Kestrel", logLevel)
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", logLevel)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", logLevel)
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", logLevel)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", includeEfLogs?LogEventLevel.Information:logLevel)
.MinimumLevel.Override("Serilog.AspNetCore.RequestLoggingMiddleware", isProduction?LogEventLevel.Warning:LogEventLevel.Information)
.Enrich.FromLogContext()
.Enrich.WithProperty("App", appName)
.WriteTo.Console(theme:AnsiConsoleTheme.Literate, outputTemplate:$"[{appName}][{{Timestamp:HH:mm:ss}} {{Level:u3}}] {{Message:lj}}{{NewLine}}{{Exception}}")
.WriteTo.Sentry();
return logConfig;
#endregion
}
stdout:
[Sample][09:23:27 INF] Sentry trace header is null. Creating new Sentry Propagation Context.
[Sample][09:23:27 INF] Started transaction with span ID '47560418c75a45e5' and trace ID '16ff7048f8274479b1918f4bf8f4fdbf'.
[Sample][09:23:27 INF] hello at 2025-10-03 9:23:27 AM
[Sample][09:23:27 WRN] warning at 2025-10-03 9:23:27 AM
[Sample][09:23:27 ERR] error at 2025-10-03 9:23:27 AM
[Sample][09:23:27 INF] Capturing event.
[Sample][09:23:27 INF] Envelope queued up: '7ec40c1b27fc459e8c661a2b40a7324b'
[Sample][09:23:27 INF] Capturing transaction.
[Sample][09:23:27 INF] Envelope queued up: 'f75c682ce3c34215bebd0902c129dbc0'
[Sample][09:23:27 INF] 200 Handled /Dev for anonymous in 47.7443 ms
[Sample][09:23:27 INF] HttpTransport: Envelope '7ec40c1b27fc459e8c661a2b40a7324b' successfully sent.
[Sample][09:23:27 INF] HttpTransport: Envelope 'f75c682ce3c34215bebd0902c129dbc0' successfully sent.
I got this working.
For anyone that lands here, make sure Sentry.Serilog package is updated :)
@odinnix, have you made changes to the startup?
E.g. setting the Dsn in the ASP.NET Core config/options rather than Serilog config/options?
(since the absence of a DSN is the canonical way of not initializing a Sentry SDK)