BrandGhost
Serilog in .NET: Complete Guide to Structured Logging

Serilog in .NET: Complete Guide to Structured Logging

Serilog in .NET: Complete Guide to Structured Logging

Serilog is the most widely adopted structured logging library in the .NET ecosystem, and for good reason. While the built-in Microsoft.Extensions.Logging framework gives you a great abstraction layer, Serilog provides the production-grade sink ecosystem, enrichment API, and flexible configuration that modern .NET applications actually need.

This guide covers everything about Serilog in .NET: what makes it different from the built-in logging framework, how to set it up in ASP.NET Core, how to configure it through appsettings.json, and how to unlock its full power with sinks and enrichers. Whether you're adding Serilog to an existing project or starting fresh on .NET 10, this is your complete reference.

If you're new to .NET logging concepts -- ILogger, structured logging, log levels, and OpenTelemetry -- start with the complete .NET logging guide for the full foundation before diving into Serilog specifically.


What is Serilog?

Serilog is a structured logging library for .NET that treats log events as first-class data rather than strings. Introduced in 2013 by Nicholas Blumhardt, it pioneered the message template syntax that Microsoft.Extensions.Logging later adopted as its own standard.

What sets Serilog apart in 2026:

  • 100+ sink packages: Write structured log data to Seq, Elasticsearch, Splunk, Azure Table Storage, SQL Server, console, flat files, email, Slack, and dozens more -- all with a consistent API
  • Rich enrichment API: Automatically attach machine name, thread ID, process ID, environment name, correlation IDs, and custom properties to every log event
  • Flexible configuration: Full support for appsettings.json-driven configuration alongside code-based setup
  • ILogger bridge: Works seamlessly with ILogger<T> -- your application code doesn't need to know Serilog exists
  • AOT and trimming: Full support in Serilog 4.x for ahead-of-time compiled and size-trimmed .NET 10 apps
  • Performance: Async, buffered sinks keep log I/O off the request thread

Serilog's Role in the ILogger Ecosystem

A key distinction: Serilog is a provider for Microsoft.Extensions.Logging, not a replacement for it. You write your application code against ILogger<T> (the framework abstraction), and Serilog handles the actual output. This means:

  • Zero vendor lock-in in your business logic
  • Full compatibility with libraries that use ILogger<T> internally (EF Core, ASP.NET Core, gRPC, etc.)
  • The ability to swap or add providers without touching application code

Setting Up Serilog in ASP.NET Core

The recommended setup for an ASP.NET Core application uses Serilog.AspNetCore, which replaces the default host logger before the application even starts.

Install Packages

The packages available on NuGet cover the core library, ASP.NET Core integration, and configuration binding from appsettings.json. Install these three to get started with a full integration:

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Settings.Configuration
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Serilog.Settings.Configuration enables appsettings.json-driven configuration. Serilog.AspNetCore provides UseSerilog() and the request logging middleware.

Bootstrap Configuration in Program.cs

using Serilog;

// Bootstrap logger for startup errors before host is built
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());

    builder.Services.AddControllers();

    var app = builder.Build();

    app.UseSerilogRequestLogging(); // Structured HTTP request logs

    app.MapControllers();
    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application startup failed");
}
finally
{
    Log.CloseAndFlush(); // Ensure all buffered logs are flushed on shutdown
}

The CreateBootstrapLogger() / UseSerilog(context, services, configuration) pattern is the recommended two-stage initialization. A minimal bootstrap logger captures startup exceptions, then the full logger configuration (driven by appsettings.json) is applied after the host is built.

appsettings.json Configuration

{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.File"
    ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 7
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
  }
}

The Override section in MinimumLevel is one of Serilog's most valuable features -- it lets you set per-namespace minimum levels, silencing framework noise while keeping your application logs verbose.


How Serilog Works: The Pipeline

Every log event in Serilog flows through a pipeline of three stages:

Source (ILogger<T>) → Enrichers → Filters → Sinks
  1. Source: Your code calls _logger.LogInformation(...), which routes through the ILogger bridge to Serilog's Log.Logger.
  2. Enrichers: Attached properties are added to the event (machine name, thread ID, HTTP request properties, custom values from LogContext).
  3. Filters: Optional predicates that exclude certain events before they reach sinks.
  4. Sinks: The event is formatted and written to one or more output destinations (console, file, Seq, Elasticsearch, etc.).

This pipeline architecture follows the Chain of Responsibility design pattern -- each stage processes the event and passes it along. Enrichers follow a Proxy-like pattern, transparently augmenting events without the source code knowing.


Serilog Sinks: Writing to Multiple Destinations

Sinks are Serilog's output destinations. You can write to one sink or dozens simultaneously, each with independent minimum level configuration and independent formatting. The most commonly used sinks are Console, File, and Seq.

Console Sink

Essential for containers and cloud environments. Console output is automatically picked up by Docker log drivers, Kubernetes log collectors, and cloud logging aggregators like Azure Monitor and AWS CloudWatch:

.WriteTo.Console(
    outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")

For JSON output (ideal for log aggregators):

.WriteTo.Console(new CompactJsonFormatter())

File Sink with Rolling

The file sink writes log events to disk. In production, always configure rolling intervals and retention limits to prevent logs from filling your storage volume:

.WriteTo.File(
    path: "logs/app-.log",
    rollingInterval: RollingInterval.Day,
    retainedFileCountLimit: 30,
    outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")

Seq (Local Development Dashboard)

Seq is the recommended log viewer for Serilog during local development. It runs in Docker and provides a query interface for structured log properties:

docker run -d --name seq -e ACCEPT_EULA=Y -p 5341:80 datalust/seq
.WriteTo.Seq("http://localhost:5341")

Navigate to http://localhost:5341 to query your logs with filters like OrderId = 5678 or @Level = 'Error'.

Async Sink Wrapper

For production scenarios where file or network sink I/O could block your application threads and impact request latency, wrap any sink in the async adapter using Serilog.Sinks.Async. The background thread drains a queue while your request threads return immediately:

dotnet add package Serilog.Sinks.Async
.WriteTo.Async(a => a.File("logs/app-.log"))
.WriteTo.Async(a => a.Seq("https://logs.example.com"))

Async wrapping buffers log events in memory and writes them on a background thread, keeping your request pipeline unblocked.


Serilog Enrichers: Adding Context Everywhere

Enrichers automatically attach properties to every log event without requiring you to pass them manually on each call. They're configured once and apply globally.

Built-In Enrichers

Standard enrichers from the Serilog ecosystem add common properties like machine name, thread ID, and process ID automatically to every log event. Install the packages you need:

dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Process
.Enrich.FromLogContext()        // Picks up LogContext.PushProperty() values
.Enrich.WithMachineName()       // Adds MachineName property
.Enrich.WithThreadId()          // Adds ThreadId
.Enrich.WithProcessId()         // Adds ProcessId
.Enrich.WithEnvironmentName()   // Adds EnvironmentName (Development/Production)

LogContext for Per-Request Enrichment

For dynamic per-request or per-operation context, LogContext.PushProperty() adds a named property to all log events emitted within the scope. The property is automatically removed when the IDisposable scope is disposed, making it ideal for request-level tracing:

public async Task<IActionResult> ProcessOrder(int orderId)
{
    using (LogContext.PushProperty("OrderId", orderId))
    using (LogContext.PushProperty("UserId", User.Identity!.Name))
    {
        _logger.LogInformation("Starting order processing");
        // All logs in this scope carry OrderId and UserId
        await _orderService.ProcessAsync(orderId);
        _logger.LogInformation("Order processing complete");
    }

    return Ok();
}

This is the Serilog approach to what ILogger.BeginScope() does in the built-in framework -- but with richer property semantics.


Serilog Request Logging

UseSerilogRequestLogging() replaces ASP.NET Core's default HTTP request logging with a single structured log entry per request:

app.UseSerilogRequestLogging(options =>
{
    options.MessageTemplate =
        "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";

    options.GetLevel = (httpContext, elapsed, ex) =>
        ex != null || httpContext.Response.StatusCode > 499
            ? LogEventLevel.Error
            : elapsed > 1000
                ? LogEventLevel.Warning
                : LogEventLevel.Information;

    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
        diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
        diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent);
    };
});

This approach produces one structured log entry per HTTP request with timing, status code, and custom properties -- far more useful than the noisy per-event logs ASP.NET Core emits by default.


Output Templates and Formatters

Serilog's output format is fully configurable per sink. For human-readable developer output you use text templates with property placeholders, while for machine-readable production output you use JSON formatters that preserve structured fields for log aggregators.

Text Templates (human-readable)

Text templates use {PropertyName} placeholders with optional formatting specifiers. Customize the format to show exactly the fields you need in development:

[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}
  • {Level:u3} -- three-character uppercase level (INF, WRN, ERR)
  • {Message:lj} -- literal string formatting (not JSON-escaped)
  • {SourceContext} -- the ILogger<T> category name

JSON Formatters (machine-readable)

.WriteTo.Console(new CompactJsonFormatter())
.WriteTo.File(new JsonFormatter(), "logs/app-.json")

CompactJsonFormatter produces one-line JSON per event. JsonFormatter produces indented JSON. Use compact JSON for log aggregators (Fluent Bit, Logstash, Vector) and formatted JSON for human debugging.


Serilog and Design Patterns

Serilog's architecture leverages several design patterns. The enricher pipeline is essentially a Decorator chain -- each enricher wraps the log event and adds properties before passing it along. Sinks implement a variant of the Observer pattern: log events are notifications, and each sink is a subscriber that reacts by writing to its destination.

The sink selection mechanism (which sinks receive which events based on level filters) mirrors the Chain of Responsibility. Understanding these patterns helps when you need to write a custom sink or enricher -- they follow the same contracts as all other components in the pipeline.


Frequently Asked Questions

These are the most common questions developers have when getting started with Serilog in .NET and ASP.NET Core applications.

What is Serilog in .NET?

Serilog is a structured logging library for .NET that captures log events as key-value property collections rather than plain strings. It provides a rich ecosystem of sinks (output destinations), enrichers (automatic property injection), and integrates with ILogger<T> via Microsoft.Extensions.Logging -- meaning your application code stays provider-agnostic.

How is Serilog different from Microsoft.Extensions.Logging?

Microsoft.Extensions.Logging (MEL) is the abstraction layer -- the ILogger<T> interface and the provider registration system. Serilog is a provider for MEL. You write code against ILogger<T>, and Serilog handles the actual output. MEL's built-in providers (Console, Debug) are limited; Serilog adds 100+ output destinations and a rich enrichment API.

Do I need Serilog if I'm deploying to Azure?

Not necessarily. Azure Application Insights integrates natively with MEL's built-in providers. However, if you need multi-sink output (file + Seq for dev, Application Insights for prod), Serilog's configuration flexibility is valuable. Many teams use Serilog in all environments and configure the Application Insights sink for production.

What is Serilog.AspNetCore?

Serilog.AspNetCore is the main integration package for ASP.NET Core. It provides UseSerilog() for host setup, UseSerilogRequestLogging() middleware for structured HTTP request logs, and CreateBootstrapLogger() / two-stage initialization for capturing startup exceptions.

Can I configure Serilog from appsettings.json?

Yes. With Serilog.Settings.Configuration (installed via NuGet), you can configure sinks, enrichers, minimum levels, and overrides entirely from appsettings.json. This is the recommended approach for ASP.NET Core because it allows per-environment configuration without recompiling.

What are Serilog sinks?

Sinks are Serilog's output destinations -- where log events actually get written. Built-in and community sinks cover console, rolling files, Seq, Elasticsearch, Splunk, Azure Table Storage, SQL Server, email, Slack, and many more. Multiple sinks can be active simultaneously, each with independent level filters.

What version of Serilog should I use in 2026?

Serilog 4.x is the current major version, with Serilog.AspNetCore 10.0.0 providing .NET 9 and .NET 10 support including AOT (ahead-of-time) compilation and trimming compatibility. Always target the latest stable release on NuGet.


What's Next

You now have a solid foundation in Serilog in .NET -- how it fits the ILogger ecosystem, how to configure it with two-stage initialization and appsettings.json, how sinks and enrichers work, and how request logging middleware cleans up your HTTP logs.

The next articles in this series cover each component in depth:

Weekly Recap: Logging in .NET, HttpClient Observability, and the Complete C# Visitor Pattern Series [Jul 2026]

This week wraps up the C# Visitor pattern series with real-world examples, best practices, a decision guide, and step-by-step implementation, plus a complete guide to logging in .NET with ILogger, Serilog, and OpenTelemetry. There's also deep HttpClient in .NET 10 coverage on observability, resilience, streaming, and DNS handling, along with practical guides to creating and understanding NuGet packages.

Logging in .NET: The Complete Developer's Guide

Master logging in .NET with ILogger, structured logging, log levels, Serilog, and OpenTelemetry. Complete guide for .NET 9 and .NET 10 developers.

EF Core Performance Best Practices in .NET 10

Optimize EF Core performance in .NET 10 -- AsNoTracking, compiled queries, split queries, bulk operations, N+1 fixes, and logging slow queries with Serilog.

An error has occurred. This application may no longer respond until reloaded. Reload