Serilog Sinks — Routing Logs to the Right Destinations
Configure Serilog sinks: Console for development, Seq for structured querying, Application Insights for Azure, file rolling sinks, sub-loggers for routing by level, and async sink wrapper for performance.
What Sinks Are
Sinks: where Serilog writes log events.
Console: stdout — visible in Docker/Kubernetes logs
File: rolling file — local disk, useful for audit logs
Seq: structured log server — queryable, dev/staging
Application Insights: Azure telemetry — production, Azure-hosted apps
Elasticsearch: distributed search — large-scale production
EventHub / Kafka: streaming — high-volume log pipelines
A single Serilog configuration can write to multiple sinks simultaneously.
Route different log levels to different sinks via sub-loggers.Console and Seq Setup
// NuGet: Serilog.AspNetCore, Serilog.Sinks.Console, Serilog.Sinks.Seq
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}",
theme: AnsiConsoleTheme.Code)
.WriteTo.Seq(
serverUrl: "http://seq-server:5341",
restrictedToMinimumLevel: LogEventLevel.Information)
.CreateLogger();
builder.Host.UseSerilog();Application Insights Sink
// NuGet: Serilog.Sinks.ApplicationInsights
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.ApplicationInsights(
telemetryConfiguration: TelemetryConfiguration.CreateDefault(),
telemetryConverter: TelemetryConverter.Traces)
.CreateLogger();
// Or configure via appsettings.json (Serilog.Settings.Configuration)
// appsettings.json:
{
"Serilog": {
"WriteTo": [
{
"Name": "ApplicationInsights",
"Args": {
"connectionString": "InstrumentationKey=...",
"telemetryConverter": "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights"
}
}
]
}
}Rolling File Sink for Audit Logs
// NuGet: Serilog.Sinks.File
Log.Logger = new LoggerConfiguration()
.WriteTo.File(
path: "logs/clinical-audit-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 90, // keep 90 days
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}",
shared: false, // don't share file across processes
buffered: false) // flush immediately (audit logs need this)
.CreateLogger();
// For clinical audit trails: retainedFileCountLimit should match regulatory requirements.
// HIPAA: 6 years. NHSDigital: 8 years. Check your jurisdiction.Sub-Loggers for Routing
// Write Warnings/Errors to Application Insights, everything else to Console only
// Sub-loggers let you route different levels to different sinks
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(e =>
e.Level >= LogEventLevel.Warning)
.WriteTo.ApplicationInsights(
TelemetryConfiguration.CreateDefault(),
TelemetryConverter.Traces))
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(e =>
e.Properties.ContainsKey("AuditEvent"))
.WriteTo.File(
"logs/audit-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 365))
.CreateLogger();
// Only Warning+ go to Application Insights (cost control)
// Only audit-tagged events go to the audit fileAsync Sink Wrapper
// NuGet: Serilog.Sinks.Async
// Wraps any sink in an async queue — fire-and-forget from the calling thread
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a =>
{
a.Console();
a.Seq("http://seq-server:5341");
},
bufferSize: 10000) // buffer up to 10,000 events
.CreateLogger();
// Without async: every log call blocks the thread until the sink writes
// With async: the call returns immediately; background thread drains the queue
// Use for: file sinks, network sinks (Seq, Elasticsearch) in production
// Audit sinks: do NOT use async — you want guaranteed writes even on crashReading Sink Configuration from appsettings
// NuGet: Serilog.Settings.Configuration
// Allows changing log levels and sinks without recompiling
// appsettings.json
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"Enrich": ["FromLogContext", "WithMachineName"],
"WriteTo": [
{ "Name": "Console" },
{ "Name": "Seq", "Args": { "serverUrl": "http://seq:5341" } }
]
}
}
// Program.cs
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.CreateLogger();Production issue I've seen: A team wrote all logs synchronously to a remote Seq server. Under traffic spikes, each HTTP request blocked for 15-30ms waiting for the log sink to flush over the network. The Serilog write was on the hot path — every log call blocked the request thread. Adding
.WriteTo.Async(a => a.Seq(...))decoupled logging from the request thread and brought 95th-percentile response time down from 85ms to 45ms. Async sinks are not optional for network sinks in production.
Key Takeaway
Use Console in development, Seq for staging (structured querying), and Application Insights for production Azure environments. Use sub-loggers to route Warnings/Errors to alerting sinks and audit events to file. Always wrap network sinks (Seq, Elasticsearch, Application Insights) in
WriteTo.Async()— synchronous network writes block request threads. Do not use async for regulatory audit file sinks — you need guaranteed writes even on crash.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.