.NET Structured Logging with Serilog

Learn how to implement powerful structured logging in your .NET applications using Serilog, with examples for enriching log events and writing to sinks like the console and files.

While the built-in ILogger in .NET is capable, many developers turn to Serilog for more advanced logging scenarios. Serilog is a powerful, open-source library that excels at structured logging, making it easy to record log events as JSON or other machine-readable formats.

This is essential for modern applications, as structured logs can be easily ingested, searched, and analyzed by platforms like Datadog, Splunk, or the ELK stack.

Why Serilog?

  • Structured Logging First: It was designed from the ground up with structured data in mind.
  • Rich Enrichment: Serilog can automatically add contextual information to your log events, such as thread IDs, machine names, and request-specific data.
  • Flexible Sinks: It has a huge ecosystem of "sinks" that can write your logs to dozens of different destinations, from the console and files to cloud services like AWS CloudWatch or Seq.

Getting Started

Let's set up Serilog in a .NET 8 web application.

  1. Install the necessary NuGet packages:

    dotnet add package Serilog.AspNetCore
    dotnet add package Serilog.Sinks.Console
    dotnet add package Serilog.Sinks.File
    
  2. Configure Serilog in Program.cs:

    The best practice is to configure the logger before the WebApplication.CreateBuilder call so that you can log issues that happen during application startup.

    // Program.cs
    using Serilog;
    using Serilog.Events;
    
    // Configure the static logger
    Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
        .Enrich.FromLogContext()
        .WriteTo.Console()
        .CreateBootstrapLogger();
    
    try
    {
        Log.Information("Starting up the web host");
    
        var builder = WebApplication.CreateBuilder(args);
    
        // Replace the default logger with Serilog
        builder.Host.UseSerilog((context, services, configuration) => configuration
            .ReadFrom.Configuration(context.Configuration)
            .ReadFrom.Services(services)
            .Enrich.FromLogContext());
    
        // ... rest of your Program.cs
    
        var app = builder.Build();
        
        // Use Serilog for request logging
        app.UseSerilogRequestLogging();
    
        // ...
    
        app.Run();
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "Host terminated unexpectedly");
    }
    finally
    {
        Log.CloseAndFlush();
    }
    

Configuration in appsettings.json

Hardcoding the configuration is inflexible. Serilog's real power comes from its ability to be configured via appsettings.json.

{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console"
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/log-.txt",
          "rollingInterval": "Day",
          "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog"
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
  }
}

This configuration:

  • Sets the default log level to Information.
  • Reduces the noise from ASP.NET Core's own logging by setting its level to Warning.
  • Writes logs to both the console and a daily rolling file.
  • Formats the file logs as JSON.
  • Enriches each log event with the machine name and thread ID.

Structured Logging in Practice

Serilog's message templates allow you to capture structured data easily.

public class MyController : ControllerBase
{
    private readonly ILogger<MyController> _logger;

    public MyController(ILogger<MyController> logger)
    {
        _logger = logger;
    }

    [HttpPost("/orders")]
    public IActionResult CreateOrder([FromBody] Order order)
    {
        // The '@' symbol tells Serilog to serialize the object, not just call ToString()
        _logger.LogInformation("Processing new order {OrderId} for user {UserId}", order.Id, order.UserId);

        // ... processing logic

        _logger.LogInformation("Order {OrderId} processed successfully", order.Id);

        return Ok();
    }
}

When using a JSON formatter, the log event for the first message would look something like this:

{
  "@t": "2024-11-15T14:30:00.123Z",
  "@m": "Processing new order 12345 for user 99",
  "@l": "Information",
  "OrderId": 12345,
  "UserId": 99,
  "SourceContext": "MyApi.MyController",
  "MachineName": "MY-DEV-MACHINE",
  "ThreadId": 10
}

Notice how OrderId and UserId are captured as distinct, queryable properties. This is the power of structured logging.

Conclusion

Serilog provides a powerful and flexible way to implement structured logging in .NET applications. By using message templates to capture contextual data and configuring sinks and enrichers through appsettings.json, you can create a robust logging system that is essential for monitoring and debugging applications in production. It's a must-have tool for any serious .NET developer.