Serilog is the most widely used structured logging library for .NET — it integrates with ASP.NET Core’s ILogger abstraction, outputs structured JSON to multiple destinations (sinks) simultaneously, and enriches every log entry with contextual properties. While ASP.NET Core’s built-in logging is functional, Serilog provides a richer feature set for production: sink variety (Seq, Application Insights, Elasticsearch, S3), enrichers (machine name, environment, process ID), and better control over output formats. Setting up Serilog is the first infrastructure task in most production ASP.NET Core projects.
Serilog Setup
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.Console
// dotnet add package Serilog.Sinks.File
// dotnet add package Serilog.Enrichers.Environment
// dotnet add package Serilog.Enrichers.Process
// ── Program.cs — configure Serilog before building the host ───────────────
// Bootstrap logger for startup errors (before host builds)
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
// Replace default logging with Serilog
builder.Host.UseSerilog((context, services, config) =>
{
config
// Read base settings from appsettings.json "Serilog" section
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services) // allows sinks to use DI services
// Enrichers — add properties to every log entry
.Enrich.FromLogContext() // reads from BeginScope
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProcessId()
// Sinks — where logs are written
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} ({SourceContext}){NewLine}{Exception}")
.WriteTo.File(
path: "logs/blogapp-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}");
});
// ... register services
var app = builder.Build();
// ... configure pipeline
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly.");
}
finally
{
Log.CloseAndFlush(); // ensures all buffered log entries are written
}
Log.Logger = new LoggerConfiguration()...) is a minimal logger created before the host builds. It captures any errors that occur during startup, before Serilog’s full configuration is loaded. The try/catch/finally around the entire startup ensures that if the host itself fails to build (misconfiguration, missing service registration), the fatal error is logged and all buffered entries are flushed. Without this pattern, host startup failures leave no log trace.appsettings.json using .ReadFrom.Configuration() rather than hard-coding all configuration in Program.cs. This allows changing log levels, adding sinks, and adjusting output formats without redeploying — just update the configuration file or environment variable and restart. The Serilog section in appsettings.json mirrors the fluent API structure and supports all the same options.Log.CloseAndFlush() in the finally block. Serilog buffers log entries for performance and flushes asynchronously. If the application exits without flushing, the last log entries — often the most important ones during a crash — are lost. The finally block ensures flushing even when an exception causes an abnormal exit. In Kubernetes and container environments, application instances are killed without warning; CloseAndFlush() is the safeguard.Serilog Configuration in appsettings.json
// ── appsettings.json Serilog section ──────────────────────────────────────
// {
// "Serilog": {
// "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
// "MinimumLevel": {
// "Default": "Information",
// "Override": {
// "Microsoft": "Warning",
// "Microsoft.AspNetCore": "Warning",
// "Microsoft.EntityFrameworkCore": "Warning",
// "BlogApp": "Debug"
// }
// },
// "WriteTo": [
// {
// "Name": "Console",
// "Args": {
// "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
// }
// },
// {
// "Name": "File",
// "Args": {
// "path": "logs/blogapp-.log",
// "rollingInterval": "Day",
// "retainedFileCountLimit": 7
// }
// }
// ],
// "Enrich": ["FromLogContext", "WithMachineName", "WithEnvironmentName"]
// }
// }
Common Mistakes
Mistake 1 — Not calling Log.CloseAndFlush() (lost log entries on exit)
❌ Wrong — last entries may be buffered and never written:
app.Run();
// Application exits — buffered entries lost!
✅ Correct — wrap in try/finally and call Log.CloseAndFlush() in finally.
Mistake 2 — Omitting .Enrich.FromLogContext() (log scopes not captured)
❌ Wrong — without this enricher, BeginScope properties are not added to Serilog entries.
✅ Correct — always include .Enrich.FromLogContext() to capture ILogger scope properties.