Plain text logs work fine until you have more than a handful of requests a day, and then every incident starts with someone grepping through gigabytes of unstructured text trying to reconstruct what happened. Structured logging fixes this by treating log entries as data — queryable fields, not sentences. Serilog is still the most common way to get there in .NET, and it works the same way in .NET 10 as it always has.
- Log with message templates and named properties, not string interpolation, so each field stays queryable.
- Bootstrap a two-stage logger: a minimal one that catches startup failures, then the fully configured one built from
appsettings.json. - Enrich every log line with request-scoped context (correlation ID, environment) once, centrally, instead of repeating it at every call site.
- Route dev logs to the console and production logs to a queryable sink (Seq, Elasticsearch, or an OpenTelemetry collector) — console logs alone don't scale past local development.
Message templates, not string interpolation
This is the one habit that makes or breaks structured logging. It's tempting to write _logger.LogInformation($"Order {orderId} submitted") — but that collapses the order ID into the message text, losing it as a queryable field. Use the template form instead:
// Wrong: values baked into the string, not queryable
_logger.LogInformation($"Order {orderId} submitted for {customerId}");
// Right: named properties, indexed as structured fields
_logger.LogInformation("Order {OrderId} submitted for {CustomerId}", orderId, customerId);
With the second form, a sink like Seq or Elasticsearch indexes OrderId and CustomerId as their own fields — you can filter on OrderId = '8f14e45f-...' directly instead of running a regex over message text.
Bootstrapping Serilog
Configure a minimal logger first, before the host is built, so startup failures (a bad connection string, a missing config value) get logged instead of silently crashing before logging is available.
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext());
var app = builder.Build();
app.UseSerilogRequestLogging(); // one structured log line per HTTP request
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
Configuration-driven sinks
Keep sink configuration in appsettings.json so dev, staging, and production can point at different destinations without a code change.
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": { "Microsoft.AspNetCore": "Warning" }
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "Seq",
"Args": { "serverUrl": "http://seq:5341" }
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithEnvironmentName"]
}
}
Enriching with request-scoped context
Push a correlation ID onto every log line for the duration of a request, once, in middleware — not repeated as a parameter at every call site downstream.
app.Use(async (context, next) =>
{
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString();
using (LogContext.PushProperty("CorrelationId", correlationId))
{
context.Response.Headers["X-Correlation-Id"] = correlationId;
await next();
}
});
Every log statement emitted further down the pipeline — in controllers, handlers, repositories — automatically carries CorrelationId without threading it through method signatures.
Where OpenTelemetry fits
Serilog remains the most ergonomic way to write structured logs in .NET code. Where it increasingly plugs in is as a log provider feeding an OpenTelemetry pipeline (via the Serilog.Sinks.OpenTelemetry package), so logs, traces, and metrics land in the same collector and correlate by trace ID. You don't have to choose one over the other — keep writing logs the Serilog way and let OpenTelemetry handle export and correlation across signals.