Articles › ASP.NET Core
Structured Logging in ASP.NET Core with Serilog
Set up production-grade logging in ASP.NET Core: ILogger basics, log levels, structured logging, Serilog sinks, request logging, enrichers, correlation IDs and what never to log.
When something breaks in production, logs are often the only witness. Good logs answer what happened, to whom, when and why within minutes; bad logs are either silent or a wall of noise. This guide covers the built-in ILogger, structured logging, and a production-ready Serilog setup for ASP.NET Core.
ILogger basics
ASP.NET Core ships with a logging abstraction you can inject anywhere:
public class OrderService(ILogger<OrderService> logger)
{
public async Task PlaceAsync(Order order)
{
logger.LogInformation("Placing order {OrderId} for customer {CustomerId}", order.Id, order.CustomerId);
// ...
}
}
The generic type (OrderService) becomes the category, so you can filter logs per class or namespace.
Log levels
| Level | Use for | Example |
|---|---|---|
Trace |
extremely detailed diagnostics | raw payloads (never in production) |
Debug |
developer diagnostics | cache miss for key X |
Information |
normal business events | order placed, user registered |
Warning |
unexpected but handled | retrying payment gateway, slow query |
Error |
a failure in the current operation | payment failed with exception |
Critical |
the app can't continue | database unreachable at startup |
Configure minimum levels per category in appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
Structured logging: templates, not string concatenation
// ❌ String interpolation: the values are baked into a plain string
logger.LogInformation($"Order {order.Id} total {order.Total}");
// ✅ Message template: values are captured as named properties
logger.LogInformation("Order {OrderId} total {Total}", order.Id, order.Total);
With a structured sink, the second form stores OrderId and Total as separate fields. You can then query "all errors where OrderId = 4821" or "average Total of failed orders" — impossible with plain text. Templates are also faster, because formatting is skipped when the level is disabled.
High-performance logging with source generators
For hot paths, generate strongly typed logging methods at compile time:
public static partial class Log
{
[LoggerMessage(EventId = 1001, Level = LogLevel.Information,
Message = "Article {Slug} viewed in {Language}")]
public static partial void ArticleViewed(ILogger logger, string slug, string language);
}
Log.ArticleViewed(logger, slug, lang);
Why Serilog?
The built-in providers write to the console and debug output. Serilog adds rich sinks (files, Seq, Elasticsearch, Application Insights…), enrichers and an excellent structured-data model, while still plugging into ILogger.
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
Setting up Serilog
using Serilog;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger(); // captures startup errors
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, services, config) => config
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "Portfolio.Api"));
var app = builder.Build();
app.UseSerilogRequestLogging(); // one clean line per request
// ...
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
Configuration in appsettings.json:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "logs/app-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 14,
"formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact"
}
},
{ "Name": "Seq", "Args": { "serverUrl": "http://localhost:5341" } }
]
}
}
Rolling files keep disk usage bounded; JSON format keeps logs machine-readable; Seq gives you a free local UI to search structured logs.
Request logging
UseSerilogRequestLogging() replaces ASP.NET Core's many per-request lines with one summary:
HTTP GET /fa/articles/csharp-async-await-guide responded 200 in 18.3 ms
Add useful properties to that line:
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diag, http) =>
{
diag.Set("UserId", http.User.FindFirst("sub")?.Value ?? "anonymous");
diag.Set("ClientIp", http.Connection.RemoteIpAddress?.ToString());
};
});
Enrichment and correlation IDs
Use scopes to attach context to every log written during an operation:
using (logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = order.Id,
["CorrelationId"] = correlationId
}))
{
logger.LogInformation("Charging payment");
logger.LogInformation("Sending confirmation"); // both lines carry OrderId and CorrelationId
}
A correlation ID passed between services (via an X-Correlation-ID header) lets you follow one user action across your API, background workers and other services.
Logging exceptions correctly
try
{
await gateway.ChargeAsync(order);
}
catch (PaymentException ex)
{
// ✅ pass the exception as the first argument — stack trace is preserved
logger.LogError(ex, "Payment failed for order {OrderId}", order.Id);
throw;
}
Log each exception once, ideally where it's handled (often the global exception handler). Logging and rethrowing at every layer creates duplicate noise.
What never to log
- Passwords, tokens, API keys, session cookies
- Full credit card numbers, national IDs, sensitive personal data
- Entire request/response bodies by default
Use Serilog's destructuring policies or masking enrichers if sensitive fields might slip through, and remember privacy regulations apply to logs too.
Performance tips
- Don't log inside tight loops at
Informationlevel. - Use
IsEnabled(LogLevel.Debug)before building expensive debug data. - Prefer asynchronous/batched sinks (Seq, Elasticsearch sinks batch by default).
- Keep
EnableSensitiveDataLogging(EF Core) for development only.
Summary
Inject ILogger<T> everywhere, choose levels deliberately, and always use message templates so logs are structured. Add Serilog for rolling files, JSON output and a searchable UI like Seq, enable request logging, enrich logs with scopes and correlation IDs, log exceptions once with their stack traces, and never log secrets. Your future self — debugging at 2 a.m. — will thank you.
FAQ
Should I use Serilog or the built-in logging?
The built-in abstraction is enough for simple apps. Serilog adds sinks, enrichers and better structured output for production systems — and you still code against ILogger.
What's the difference between logs, metrics and traces?
Logs record discrete events; metrics are numeric measurements over time (requests/sec); traces follow a request across services. OpenTelemetry unifies all three.
How long should I keep logs?
Long enough to investigate incidents and meet legal requirements — commonly 14–90 days for application logs, longer for audit logs.