Serilog Logging in ASP.NET Core - Ultimate 2025 Guide

Logging via Serilog in ASP.NET Core

Still chasing cryptic stack traces at 2 a.m.? Imagine if your logs were more like a black‑box flight recorder than a dusty, unreadable diary. Serilog turns everyday logging into structured, queryable insights – so you can sleep at night (or at least fix production issues before the third cup of coffee).

Why Bother Logging at All?

  • Post‑mortem clarity ⇢ reproduce bugs in minutes, not hours.
  • Operational health ⇢ spot performance bottlenecks before users complain.
  • Security audits ⇢ prove that data actually stayed inside the castle walls.

Personal note: In my first large‑scale fintech project, we lost two days tracing an intermittent 500 error. A single structured log entry – {TransactionId} and ElapsedMs – would have narrowed the search to one microservice in seconds.

Why Serilog Instead of ILogger‑Only?

FeatureMicrosoft.Extensions.Logging (out of box)Serilog
Structured eventsLimited (state object)First‑class JSON, templates
Rich sink ecosystemFew (Console, Debug)100+ (Seq, Elasticsearch, Datadog, etc.)
EnrichersManualPlug‑and‑play (MachineName, ThreadId, CorrelationId)
Runtime level switchNoYes (Serilog.Expressions)
Message templatesPlain stringsNamed tokens ({OrderId})

Serilog isn’t a replacement for the built‑in logging abstractions; it integrates seamlessly via Serilog.AspNetCore, so you still inject ILogger<T> yet enjoy Serilog’s superpowers under the hood.

Getting Started in 90 Seconds

// Program.cs (minimal hosting)
using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .Enrich.FromLogContext()
    .CreateBootstrapLogger(); // Early‑startup safety

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((ctx, services, cfg) => cfg
    .ReadFrom.Configuration(ctx.Configuration)        // appsettings.json
    .ReadFrom.Services(services)                      // DI enrichers
    .WriteTo.Console(new RenderedCompactJsonFormatter()));

builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

appsettings.json snippet

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
    "WriteTo": [
      {
        "Name": "Console",
        "Args": { "formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact" }
      }
    ]
  }
}

Tip: Use the Serilog.Settings.Configuration package to move all tuning knobs to your config file – zero redeploys to bump levels.

Structured Logging

Message Templates vs. String Interpolation

Bad (loses structure):

_logger.LogInformation($"User {user.Id} logged in at {DateTime.UtcNow}");

Good (preserves named fields):

_logger.LogInformation("User {UserId} logged in", user.Id);

Resulting JSON (Console sink with compact formatter):

{"@t":"2025-07-26T10:13:45.123Z","@mt":"User {UserId} logged in","UserId":42,"SourceContext":"MyApp.AuthController"}

Searchable, filterable, Grafana‑friendly.

Destructuring Complex Objects

_logger.LogWarning("Failed payment {@Payment}", payment);

The @ tells Serilog to serialize the object rather than call ToString() – extremely handy when debugging nested DTOs.

Enrichers

  • Serilog.Enrichers.ClientInfo ⇢ browser, OS, device
  • Serilog.Enrichers.CorrelationId ⇢ trace a request across microservices
  • Custom enricher (e.g., tenant ID):
class TenantEnricher : ILogEventEnricher
{
    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propFactory)
        => logEvent.AddPropertyIfAbsent(propFactory.CreateProperty("TenantId", TenantProvider.Current));
}

Add with .Enrich.With<TenantEnricher>() in LoggerConfiguration.

Choosing the Right Sink

ScenarioRecommended SinkWhy
Local devConsole with color/themeFast feedback, no files to clean
Single VM prodFile rolling dailyDisk is cheap; simple retention
Distributed prodSeq or ElasticsearchPowerful querying, dashboards
ServerlessAWS CloudWatch / Azure MonitorNative integration, pay‑as‑you‑go

Real‑world note: We switched from rolling files to Seq in a Kubernetes cluster and cut incident triage time by 70 % – searching by CorrelationId instead of grep -R is game‑changing.

Async Performance

Most sinks buffer asynchronously. For high‑traffic APIs enable WriteTo.Async() around your sink chain:

.WriteTo.Async(a => a.Console())

Buffer size & back‑pressure are configurable; always benchmark under load.

Environment‑Aware Logging Levels

.MinimumLevel.ControlledBy(new LoggingLevelSwitch(LogEventLevel.Information))

Expose the switch via your health‑check UI or an admin endpoint so ops can temporarily raise verbosity to Debug without redeploying. Pair with Serilog.Settings.Configuration#reloadOnChange for hot‑reload from appsettings.json.

Tracing, Metrics & OpenTelemetry

Serilog plays nicely with the .NET DiagnosticListener and OpenTelemetry exporters. Use Serilog.Sinks.OpenTelemetry to emit spans side‑by‑side with logs – correlate everything in one backend.

WriteTo.OpenTelemetry(options => {
    options.Endpoint = "http://otel‑collector:4317";
});

FAQ: Serilog Adoption in Existing Projects

Can I keep using ILogger<T>?

Yes. Register Serilog as the provider; your existing calls remain untouched.

Will structured logging bloat my log size?

JSON is verbose, but filtering and retention policies offset storage costs. Compression on disk or in Elastic further helps.

How do I log exceptions with full stack traces?

Log.Error(ex, "Unhandled error processing order {OrderId}", order.Id); Serilog captures ex plus contextual properties – no custom serializer needed.

What about GDPR/PII?

Use Serilog.Filters.Expressions to drop or hash sensitive properties before they leave the process.

Which sink is best for Kubernetes?

Seq for rich queries; OpenTelemetry + Grafana Loki for unified metrics/logs. Both support horizontal scaling.

Conclusion: Logs That Work as Hard as Your Code

When your logs speak JSON and carry every breadcrumb – from CorrelationId to ElapsedMs – root‑cause analysis shrinks from hours to minutes. Serilog makes that upgrade almost friction‑free: add a package, tweak a config, and watch your observability score skyrocket.

Ready to ditch Console.WriteLine‑driven debugging and join the structured‑logging club? Try swapping Serilog into your next sprint and share your experience in the comments – what insight surprised you the most?

Leave a Reply

Your email address will not be published. Required fields are marked *