All articles

Structured Logging in ASP.NET Core with Serilog and Seq

Wire Serilog into ASP.NET Core with console, rolling-file, and Seq sinks: request logging, enrichment, levels, and the named-placeholder habit that makes logs queryable.

0 · log in to like, save & follow Share on LinkedIn Share on X
Structured Logging in ASP.NET Core with Serilog and Seq

Console.WriteLine debugging dies the day your app runs on a server you can't watch. Structured logging is the production replacement: every event carries named properties you can query — "show me all errors for OrderId 13" — instead of grepping text. This post wires Serilog into an ASP.NET Core app on .NET 10 with console, rolling-file, and Seq sinks, request logging, and the discipline that makes logs queryable, with output captured from a real run.

Structured Logging in ASP.NET Core with Serilog and Seq

Structured vs stringly

The difference is one habit:

// unstructured — a sentence
logger.LogInformation($"Fetching order {id} for {email}");

// structured — an event with properties
logger.LogInformation("Fetching order {OrderId} for {Customer}", id, email);

Both print the same text. But the second stores OrderId=7 and Customer=asha@example.com as first-class properties — filterable, groupable, chartable in any structured backend. Interpolation throws that away at the moment of logging, permanently. Never interpolate into log messages.

Wiring Serilog

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Seq
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSerilog((_, cfg) =>
{
    cfg.MinimumLevel.Information()
       .Enrich.FromLogContext()
       .WriteTo.Console()
       .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day);
    if (Environment.GetEnvironmentVariable("SEQ_URL") is { Length: > 0 } seq)
        cfg.WriteTo.Seq(seq);
});

var app = builder.Build();
app.UseSerilogRequestLogging();   // one structured event per request
  • Console for docker logs and development.
  • Rolling file (app-20260726.log, daily, retainedFileCountLimit: 14) as the on-box safety net.
  • Seq — activated by environment variable so local runs work without it. Sinks are additive; events go to all of them.

UseSerilogRequestLogging replaces ASP.NET Core's chatty per-request infrastructure logs with one event per request carrying method, path, status, and elapsed time — the single most useful line in the config.

Verified output

Two requests against the demo API (/orders/7 succeeds, /orders/13 simulates corruption):

[11:19:30 INF] Fetching order 7 for asha@example.com
[11:19:30 INF] HTTP GET /orders/7 responded 200 in 11.9637 ms
[11:19:30 INF] Fetching order 13 for asha@example.com
[11:19:30 ERR] Order 13 is corrupted

The controller code stays on ILogger<T> — Serilog plugs in underneath, so libraries and framework code all flow through the same pipeline without knowing Serilog exists.

Seq: the queryable half

Seq is a log server built for structured events, free for development and single-user deployments:

docker run -d --name seq -p 5341:80 -e ACCEPT_EULA=Y datalust/seq
SEQ_URL=http://localhost:5341 dotnet run

In the Seq UI those same events become queryable data: OrderId = 13 finds every event about that order across requests; StatusCode >= 500 && Elapsed > 1000 finds slow failures; select count(*) from stream group by Customer charts noisy customers. This is the payoff of the named-placeholder habit — questions you didn't anticipate, answered without a deploy. (The same events flow equally well to Elasticsearch, Grafana Loki, or Application Insights — sinks are one line each; the discipline transfers.)

Levels and noise control

cfg.MinimumLevel.Information()
   .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning);

The override silences framework chatter while your events stay at Information. The level ladder in practice: Debug for local diagnosis, Information for business events worth counting, Warning for handled-but-odd, Error for failed operations, Fatal for "the process is going down". If a log line fires thousands of times an hour and no one would ever query it, it's Debug, not Information — log volume is a real bill in hosted backends.

Enrichment adds ambient context to every event: Enrich.FromLogContext() plus LogContext.PushProperty("TenantId", id) in a middleware stamps a whole request's events; Enrich.WithProperty("Application", "checkout-api") distinguishes services sharing one Seq.

Production notes

  • Bootstrap logging: initialize a minimal Log.Logger before CreateBuilder and wrap startup in try/catch/Log.CloseAndFlush() — otherwise the one crash you most need logged (startup failure) is the one that isn't.
  • CloseAndFlush on shutdown — sinks buffer; skipping it drops the final batch, which always contains the interesting events.
  • Configuration from appsettings (cfg.ReadFrom.Configuration(...)) lets operations change levels per namespace without redeploying.
  • Serilog and OpenTelemetry are complements, not competitors: OTel owns traces and metrics; Serilog is a first-class log pipeline that can stamp events with the active trace id, joining the three signals.

The runnable demo API is in the companion repository — start Seq in Docker, set SEQ_URL, and click through your own structured events.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.