Serilog Logging in .NET: Structured Logging Best Practices
Backend12 min read

Serilog Logging in .NET: Structured Logging Best Practices

Master structured logging with Serilog. Configure sinks, enrich logs, implement correlation IDs, and monitor your applications effectively.

Taha Kocal

Taha Kocal

Full Stack Developer

Apr 25, 2025
#Serilog#.NET#Logging#Observability#C#

Serilog is a structured logging library for .NET that records log events as typed properties rather than plain text, making logs searchable and analyzable in tools like Seq, Elasticsearch, or Grafana. Instead of interpolating values into a string, you write message templates such as "Creating order {OrderId} for user {UserId}", and Serilog captures each named value as a queryable field. The library plugs into ASP.NET Core through Serilog.AspNetCore, ships dozens of sinks for consoles, rolling files, and log servers, and supports enrichers that attach context like machine name, thread ID, or a per-request correlation ID to every event. That structure is what turns logging from console noise into an observability tool: you can filter production logs to a single order, user, or request in seconds. This guide covers setup, structured logging patterns, correlation IDs, custom enrichers, configuration, scopes, and performance logging.

Setup

bash
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
csharp
// Program.cs
using Serilog;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithEnvironmentName()
    .Enrich.WithMachineName()
    .Enrich.WithThreadId()
    .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
    .WriteTo.Seq("http://localhost:5341")
    .CreateLogger();

try
{
    Log.Information("Starting application");

    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseSerilog();

    var app = builder.Build();

    // Add request logging
    app.UseSerilogRequestLogging(options =>
    {
        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
            diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].FirstOrDefault());
        };
    });

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}

How Does Structured Logging Work in Serilog?

csharp
public class OrderService
{
    private readonly ILogger<OrderService> _logger;

    // GOOD: Structured logging with properties
    public async Task CreateOrderAsync(Order order)
    {
        _logger.LogInformation(
            "Creating order {OrderId} for user {UserId} with {ItemCount} items, total: {TotalAmount}",
            order.Id, order.UserId, order.Items.Count, order.TotalAmount);

        // These become searchable properties in Seq/Elasticsearch
    }

    // BAD: String interpolation loses structure
    public async Task BadExample(Order order)
    {
        _logger.LogInformation($"Creating order {order.Id}"); // Don't do this!
    }

    // Log objects
    public async Task LogComplexData(Order order)
    {
        _logger.LogInformation("Processing order {@Order}", order);
        // @ serializes the object, $ just calls ToString()
    }

    // Conditional logging
    public async Task ProcessAsync()
    {
        if (_logger.IsEnabled(LogLevel.Debug))
        {
            var expensiveData = ComputeExpensiveDebugInfo();
            _logger.LogDebug("Debug info: {Data}", expensiveData);
        }
    }
}

How Do You Add Correlation IDs?

csharp
// Middleware to add correlation ID
public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;
    private const string CorrelationIdHeader = "X-Correlation-ID";

    public CorrelationIdMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
            ?? Guid.NewGuid().ToString();

        context.Response.Headers[CorrelationIdHeader] = correlationId;

        using (LogContext.PushProperty("CorrelationId", correlationId))
        {
            await _next(context);
        }
    }
}

// Usage
app.UseMiddleware<CorrelationIdMiddleware>();

// Now all logs within a request have CorrelationId
_logger.LogInformation("Processing request"); // Includes CorrelationId automatically

Custom Enrichers

csharp
// Custom enricher for user info
public class UserEnricher : ILogEventEnricher
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserEnricher(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        if (httpContext?.User?.Identity?.IsAuthenticated == true)
        {
            var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            var userName = httpContext.User.Identity.Name;

            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("UserId", userId));
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("UserName", userName));
        }
    }
}

// Register
builder.Services.AddHttpContextAccessor();
builder.Host.UseSerilog((context, services, config) =>
{
    config
        .Enrich.With(new UserEnricher(services.GetRequiredService<IHttpContextAccessor>()))
        .WriteTo.Console();
});

Configuration from appsettings.json

json
{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Seq"],
    "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": 30,
          "fileSizeLimitBytes": 10485760
        }
      },
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://localhost:5341"
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithEnvironmentName"]
  }
}
csharp
// Program.cs
builder.Host.UseSerilog((context, config) =>
{
    config.ReadFrom.Configuration(context.Configuration);
});

Logging Scopes

csharp
public class OrderProcessor
{
    private readonly ILogger<OrderProcessor> _logger;

    public async Task ProcessOrderAsync(Order order)
    {
        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["OrderId"] = order.Id,
            ["CustomerId"] = order.CustomerId
        }))
        {
            _logger.LogInformation("Starting order processing");

            await ValidateOrder(order);
            await ProcessPayment(order);
            await UpdateInventory(order);

            _logger.LogInformation("Order processing completed");
        }
        // All logs within scope include OrderId and CustomerId
    }

    private async Task ValidateOrder(Order order)
    {
        _logger.LogDebug("Validating order"); // Includes OrderId, CustomerId
    }
}

How Should You Log Exceptions?

csharp
public async Task ProcessAsync()
{
    try
    {
        await DoWorkAsync();
    }
    catch (ValidationException ex)
    {
        // Log expected exceptions as warnings
        _logger.LogWarning(ex, "Validation failed for request");
        throw;
    }
    catch (Exception ex)
    {
        // Log unexpected exceptions as errors
        _logger.LogError(ex, "Unexpected error during processing");
        throw;
    }
}

// Global exception handler middleware
public class ExceptionLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionLoggingMiddleware> _logger;

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Unhandled exception for {Method} {Path}",
                context.Request.Method,
                context.Request.Path);
            throw;
        }
    }
}

Performance Logging

csharp
public class PerformanceLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<PerformanceLoggingMiddleware> _logger;

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();

        try
        {
            await _next(context);
        }
        finally
        {
            sw.Stop();

            if (sw.ElapsedMilliseconds > 500)
            {
                _logger.LogWarning(
                    "Slow request: {Method} {Path} took {ElapsedMs}ms",
                    context.Request.Method,
                    context.Request.Path,
                    sw.ElapsedMilliseconds);
            }
        }
    }
}

// Operation timing helper
public static class LoggerExtensions
{
    public static IDisposable TimeOperation(this ILogger logger, string operationName)
    {
        return new OperationTimer(logger, operationName);
    }

    private class OperationTimer : IDisposable
    {
        private readonly ILogger _logger;
        private readonly string _operationName;
        private readonly Stopwatch _sw;

        public OperationTimer(ILogger logger, string operationName)
        {
            _logger = logger;
            _operationName = operationName;
            _sw = Stopwatch.StartNew();
            _logger.LogDebug("Starting {Operation}", operationName);
        }

        public void Dispose()
        {
            _sw.Stop();
            _logger.LogInformation(
                "{Operation} completed in {ElapsedMs}ms",
                _operationName,
                _sw.ElapsedMilliseconds);
        }
    }
}

// Usage
using (_logger.TimeOperation("DatabaseQuery"))
{
    await _repository.GetDataAsync();
}

Best Practices

Logging best practices:

  • Use structured logging with named properties, not string interpolation
  • Log at appropriate levels: Debug for dev, Info for operations, Warn for issues, Error for failures
  • Include correlation IDs for request tracing
  • Don't log sensitive data (passwords, tokens, PII)
  • Use scopes for operation context
  • Log timing for performance monitoring
  • Configure different outputs for different environments
csharp
// Sensitive data filtering
Log.Logger = new LoggerConfiguration()
    .Destructure.ByTransforming<User>(u => new
    {
        u.Id,
        u.Email,
        Password = "***REDACTED***"
    })
    .CreateLogger();

// Or use a custom policy
public class SensitiveDataPolicy : IDestructuringPolicy
{
    public bool TryDestructure(object value, ILogEventPropertyValueFactory factory, out LogEventPropertyValue? result)
    {
        if (value is CreditCard card)
        {
            result = factory.CreatePropertyValue(new
            {
                LastFour = card.Number[^4..],
                card.ExpiryMonth,
                card.ExpiryYear
            });
            return true;
        }

        result = null;
        return false;
    }
}

Logs are your application's story. Write them for your future self who's debugging at 3 AM. Structure them for machines to search, but make them readable for humans to understand.

Good logging practices pay dividends when troubleshooting production issues. Invest in structured logging with Serilog, ship logs to a centralized platform like Seq or Elasticsearch, and establish consistent logging patterns across your team.

Share this article

Related Articles