Production logging requires careful configuration โ the wrong settings can flood storage with noise (every EF Core SQL statement, every HTTP framework log), hide real problems (log levels too high), or hurt performance (synchronous file writes on every request). Good production log configuration filters out noise, elevates important events, and keeps the logging pipeline non-blocking. Log hygiene โ knowing what to log, what to filter, and how to store logs efficiently โ is an operational skill as important as writing the logs in the first place.
Log Level Filtering for Production
// โโ appsettings.Production.json โ production log levels โโโโโโโโโโโโโโโโโโโ
// {
// "Serilog": {
// "MinimumLevel": {
// "Default": "Warning", // only warnings and above by default
// "Override": {
// "Microsoft": "Warning",
// "Microsoft.AspNetCore": "Warning",
// "Microsoft.Hosting.Lifetime": "Information", // startup/shutdown visible
// "Microsoft.EntityFrameworkCore": "Warning", // suppress EF SQL logs
// "BlogApp": "Information" // your code at Information
// }
// }
// }
// }
// โโ appsettings.Development.json โ verbose for local development โโโโโโโโโโโ
// {
// "Serilog": {
// "MinimumLevel": {
// "Default": "Debug",
// "Override": {
// "Microsoft.EntityFrameworkCore.Database.Command": "Information", // show SQL
// "BlogApp": "Trace" // maximum verbosity for your code
// }
// }
// }
// }
// โโ Programmatic filtering โ suppress specific sources โโโโโโโโโโโโโโโโโโโโ
builder.Host.UseSerilog((ctx, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.Filter.ByExcluding(Matching.FromSource("Microsoft.AspNetCore.StaticFiles"))
.Filter.ByExcluding(logEvent =>
logEvent.Properties.ContainsKey("RequestPath") &&
logEvent.Properties["RequestPath"].ToString().Contains("/health")));
/metrics, /favicon.ico, and static file requests that generate noise without value.Serilog.Sinks.Async wraps any sink in an asynchronous buffer: .WriteTo.Async(a => a.File("logs/app.log")). Log entries are queued to a background channel and written by a dedicated thread, keeping request threads free. For high-throughput APIs, this is essential โ synchronous file writes (especially over network shares or cloud storage) add measurable latency to every request.retainedFileCountLimit (daily rolling) or retainedFileSizeLimit (size-based rolling). A production server with unbounded log files is a time bomb โ disk-full conditions cause complete service outages, often at the worst possible time. Set retention to match your compliance requirements (typically 30โ90 days for most applications) and monitor disk usage as a health metric.Rolling File Sink with Retention
// โโ Serilog file sink with rolling, async, and retention โโโโโโโโโโโโโโโโโโ
builder.Host.UseSerilog((ctx, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.WriteTo.Async(a => a.Console(
new CompactJsonFormatter())) // JSON format for log aggregator ingestion
.WriteTo.Async(a => a.File(
formatter: new CompactJsonFormatter(),
path: "logs/blogapp-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30, // keep 30 days of logs
fileSizeLimitBytes: 100 * 1024 * 1024, // 100MB per file
rollOnFileSizeLimit: true)));
// โโ Seq sink for local development (structured log viewer) โโโโโโโโโโโโโโโโ
if (ctx.HostingEnvironment.IsDevelopment())
{
cfg.WriteTo.Seq("http://localhost:5341");
// dotnet tool install --global seqcli
// seqcli run (or docker run -p 5341:5341 datalust/seq)
}
Logging Health Check
// โโ Verify logging pipeline is operational at startup โโโโโโโโโโโโโโโโโโโโโ
public class LoggingHealthCheck(ILogger<LoggingHealthCheck> logger) : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct = default)
{
try
{
logger.LogInformation("Logging health check: pipeline operational.");
return Task.FromResult(HealthCheckResult.Healthy("Logging pipeline operational."));
}
catch (Exception ex)
{
return Task.FromResult(
HealthCheckResult.Unhealthy("Logging pipeline failure.", ex));
}
}
}
// builder.Services.AddHealthChecks().AddCheck<LoggingHealthCheck>("logging");
Common Mistakes
Mistake 1 โ No retention limit on rolling file sink (disk fills up)
โ Wrong โ unlimited logs accumulate and fill the disk, causing service outage:
.WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day) // no retainedFileCountLimit!
โ
Correct โ always set retainedFileCountLimit and fileSizeLimitBytes.
Mistake 2 โ Synchronous sinks in production (blocking request threads)
โ Wrong โ direct file/network sink writes block request threads during heavy load.
โ
Correct โ wrap all production sinks in .WriteTo.Async(a => a.SinkName(...)).