ASP.NET Core

Serilog - Structured Logging for .NET

Logging is often treated as an afterthought—sprinkle some Console.WriteLine() calls during development, maybe add ILogger if you remember, and hope for the best in production. Then something breaks at 3 AM, and you're grepping through text files trying to reconstruct what happened. Structured logging changes this game entirely.

Serilog - Structured Logging for .NET

Logging is often treated as an afterthought—sprinkle some Console.WriteLine() calls during development, maybe add ILogger if you remember, and hope for the best in production. Then something breaks at 3 AM, and you're grepping through text files trying to reconstruct what happened. Structured logging changes this game entirely.

The answer is refreshingly simple: Serilog. It's a .NET logging library that captures events as structured data instead of text blobs. Instead of parsing "User [email protected] logged in at 2023-12-15", you get a structured event with properties: {Username: "[email protected]", Timestamp: "2023-12-15T10:30:00Z", EventType: "UserLogin"}. Query it, analyze it, alert on it—all without regex.

By capturing structure upfront, Serilog makes production debugging actually feasible. When your application throws an exception in production, you don't just see "Error occurred"—you see the user ID, the request path, the correlation ID linking related events, and the full context that led to the failure. When performance degrades, you can query timing data across millions of events to find the bottleneck.

This isn't to say Serilog is perfect for everyone. If you're building a simple console app that logs to stdout, the built-in .NET logging might be sufficient. But if you're running distributed systems, need queryable logs, want correlation across services, or need production observability, Serilog's structured approach is transformative.

For instance: A microservices architecture with 20+ services? Serilog's enrichers add correlation IDs automatically, letting you trace requests across service boundaries. A monolith with complex workflows? Structured properties let you filter logs by user, tenant, or operation type without parsing text.

Serilog is a diagnostic logging library for .NET applications that captures log events as structured data rather than text. It provides powerful sinks for writing logs to various destinations, enrichers for adding contextual information, and a flexible configuration system.

Key characteristics:

  1. Structured Events - Log events contain properties, not just messages
  2. Flexible Sinks - Write to files, databases, cloud services, and more
  3. Enrichment - Automatically add contextual information
  4. Performance - Minimal overhead with async logging
  5. Correlation - Track related events across distributed systems

Why Use Serilog?

1. Structured Logging

Instead of string concatenation, capture rich structured data that's queryable and analyzable.

Traditional Logging:

_logger.LogInformation($"User {userId} ordered {itemCount} items totaling ${totalAmount}");
// Output: "User 12345 ordered 3 items totaling $149.99"
// How do you find all orders over $100? Parse strings with regex!

With Serilog:

_logger.Information("User {UserId} ordered {ItemCount} items totaling {TotalAmount:C}", 
    userId, itemCount, totalAmount);
// Output: Structured event with properties:
// { UserId: 12345, ItemCount: 3, TotalAmount: 149.99, Message: "User ordered items..." }
// Query: SELECT * FROM logs WHERE TotalAmount > 100

2. Production Observability

Rich contextual information makes production debugging possible:

  • Correlation IDs track requests across services
  • Enrichers add machine name, environment, version automatically
  • Structured exceptions include full context
  • Performance metrics embedded in logs

3. Flexible Destinations

Write logs to multiple destinations simultaneously:

  • Files (rolling, retention policies)
  • Databases (SQL Server, PostgreSQL, MongoDB)
  • Cloud services (Application Insights, Seq, Datadog)
  • Messaging systems (Elasticsearch, RabbitMQ)
  • Centralized logging (Seq, Splunk, ELK stack)

4. Minimal Overhead

Async logging, batching, and buffering ensure logging doesn't slow down your application. Background processing means your API doesn't wait for logs to be written.

5. Configuration Flexibility

Configure in code, appsettings.json, or environment variables. Different sinks and levels per environment without code changes.

6. Integration with .NET

Works seamlessly with Microsoft.Extensions.Logging, ASP.NET Core, and the entire .NET ecosystem.


How Serilog Works

Basic Concept

Traditional Text Logging:

[INFO] 2023-12-15 10:30:00 - User login successful: [email protected]
[ERROR] 2023-12-15 10:31:00 - Failed to process order: Order not found

Problems:

  • Free-form text requires parsing
  • Context is buried in strings
  • Querying requires regex
  • Analysis needs log parsing tools

With Structured Logging:

{
  "Timestamp": "2023-12-15T10:30:00.000Z",
  "Level": "Information",
  "MessageTemplate": "User login successful: {Email}",
  "Properties": {
    "Email": "[email protected]",
    "UserId": 12345,
    "SourceContext": "MyApp.AuthService",
    "CorrelationId": "abc-123-def"
  }
}

Benefits:

  • Properties are typed and indexed
  • Queryable without parsing
  • Automatic aggregation and filtering
  • Context automatically included

Message Templates

Serilog uses message templates, not string interpolation:

// ❌ WRONG - String interpolation
_logger.Information($"User {userId} logged in");

// ✅ RIGHT - Message template
_logger.Information("User {UserId} logged in", userId);

Why templates matter:

  • Properties are extracted and stored separately
  • Same template = same event type for analytics
  • Efficient storage (template stored once, values stored per event)
  • Enables querying and aggregation

Getting Started with Serilog

1. Installation

# Core package
dotnet add package Serilog

# ASP.NET Core integration
dotnet add package Serilog.AspNetCore

# Common sinks
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq

# Configuration
dotnet add package Serilog.Settings.Configuration

# Enrichers
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Process

2. Basic Setup (Console App)

using Serilog;
using System;

namespace MyConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Configure Serilog
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .WriteTo.Console()
    .CreateLogger();Console()
                .WriteTo.File("logs/myapp-.txt", rollingInterval: RollingInterval.Day)
                .CreateLogger();

            try
            {
                Log.Information("Application starting up");
                
                // Your application code
                ProcessOrder(12345, 3, 149.99m);
                
                Log.Information("Application completed successfully");
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Application failed to start");
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        static void ProcessOrder(int userId, int itemCount, decimal total)
        {
            Log.Information(
                "Processing order for user {UserId}: {ItemCount} items, total {Total:C}",
                userId, itemCount, total);
        }
    }
}

3. ASP.NET Core Setup

Program.cs (Minimal API / .NET 6+):

using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog
builder.Host.UseSerilog((context, configuration) =>
{
    configuration
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext()
        .Enrich.WithMachineName()
        .Enrich.WithEnvironmentName()
        .WriteTo.Console()
        .WriteTo.File("logs/myapp-.txt", rollingInterval: RollingInterval.Day);
});

var app = builder.Build();

// Add Serilog request logging
app.UseSerilogRequestLogging();

app.MapGet("/", () => "Hello World!");

app.Run();

appsettings.json Configuration:

{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.Hosting.Lifetime": "Information",
        "System": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": {
          "path": "logs/myapp-.txt",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 7,
          "fileSizeLimitBytes": 10485760,
          "rollOnFileSizeLimit": true
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
    "Properties": {
      "Application": "MyWebApp",
      "Environment": "Development"
    }
  }
}

Structured Logging Fundamentals

1. Log Levels

Log.Verbose("Detailed diagnostic information");     // Most verbose
Log.Debug("Internal system events");                 // Development debugging
Log.Information("General informational messages");   // Standard operation
Log.Warning("Unexpected but handled situations");    // Potential issues
Log.Error(ex, "Errors and exceptions");              // Failures
Log.Fatal(ex, "Critical failures");                  // Application crashes

When to use each level:

  • Verbose: Trace-level diagnostics, rarely enabled
  • Debug: Developer debugging, disabled in production
  • Information: Key business events (user login, order placed)
  • Warning: Degraded functionality, fallback used
  • Error: Operation failed but application continues
  • Fatal: Application cannot continue

2. Message Templates

// Properties in templates
Log.Information("User {UserId} from {Country} ordered {ProductName}", 
    userId, country, productName);

// Destructuring with @
Log.Information("Processing {@Order}", order);
// Serializes entire order object as structured data

// Stringification with $
Log.Information("Order summary: {$Order}", order);
// Uses ToString() for simple representation

// Formatting
Log.Information("Order total: {Amount:C}", amount);              // Currency
Log.Information("Processing time: {Duration:0.00} seconds", seconds);  // Numbers
Log.Information("Occurred at {Timestamp:yyyy-MM-dd}", timestamp);      // Dates

3. Structured Exceptions

try
{
    ProcessPayment(orderId);
}
catch (PaymentException ex)
{
    Log.Error(ex, 
        "Payment failed for order {OrderId} by user {UserId}", 
        orderId, userId);
    // Exception details are automatically captured and structured
}

// Result includes:
// - Exception type, message, stack trace
// - OrderId and UserId as properties
// - Full context from enrichers

4. Log Context Enrichment

using Serilog.Context;

// Add properties to all logs in scope
using (LogContext.PushProperty("OrderId", orderId))
using (LogContext.PushProperty("UserId", userId))
{
    Log.Information("Starting order processing");
    ProcessOrder();
    Log.Information("Order processing complete");
    // Both logs automatically include OrderId and UserId
}

// Dynamic context
public async Task<IActionResult> ProcessOrder(int orderId)
{
    using (LogContext.PushProperty("OrderId", orderId))
    using (LogContext.PushProperty("UserId", User.GetUserId()))
    {
        // All logs in this scope have OrderId and UserId
        _logger.Information("Processing order");
        await _orderService.Process(orderId);
        _logger.Information("Order completed");
    }
}

Sinks - Output Destinations

1. Console Sink

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();

// With formatting
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console(
        outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

// JSON output
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console(new JsonFormatter())
    .CreateLogger();

2. File Sink

// Basic file logging
Log.Logger = new LoggerConfiguration()
    .WriteTo.File("logs/myapp.txt")
    .CreateLogger();

// Rolling files by date
Log.Logger = new LoggerConfiguration()
    .WriteTo.File(
        "logs/myapp-.txt",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 7)  // Keep last 7 days
    .CreateLogger();

// Rolling by size
Log.Logger = new LoggerConfiguration()
    .WriteTo.File(
        "logs/myapp.txt",
        fileSizeLimitBytes: 10_485_760,  // 10MB
        rollOnFileSizeLimit: true,
        retainedFileCountLimit: 10)
    .CreateLogger();

// JSON files for structured parsing
Log.Logger = new LoggerConfiguration()
    .WriteTo.File(
        new JsonFormatter(),
        "logs/myapp-.json",
        rollingInterval: RollingInterval.Day)
    .CreateLogger();

3. Seq Sink

// Seq - structured log server
dotnet add package Serilog.Sinks.Seq

Log.Logger = new LoggerConfiguration()
    .WriteTo.Seq("http://localhost:5341")
    .CreateLogger();

// With API key
Log.Logger = new LoggerConfiguration()
    .WriteTo.Seq(
        serverUrl: "http://localhost:5341",
        apiKey: "your-api-key")
    .CreateLogger();

// Batch size for performance
Log.Logger = new LoggerConfiguration()
    .WriteTo.Seq(
        "http://localhost:5341",
        batchPostingLimit: 100,
        period: TimeSpan.FromSeconds(2))
    .CreateLogger();

4. Database Sinks

SQL Server:

dotnet add package Serilog.Sinks.MSSqlServer

Log.Logger = new LoggerConfiguration()
    .WriteTo.MSSqlServer(
        connectionString: "Server=localhost;Database=Logs;Trusted_Connection=True;",
        sinkOptions: new MSSqlServerSinkOptions 
        { 
            TableName = "Logs",
            AutoCreateSqlTable = true 
        })
    .CreateLogger();

// Custom columns
var columnOptions = new ColumnOptions();
columnOptions.Store.Remove(StandardColumn.Properties);
columnOptions.Store.Add(StandardColumn.LogEvent);
columnOptions.AdditionalColumns = new Collection<SqlColumn>
{
    new SqlColumn { ColumnName = "UserId", DataType = SqlDbType.Int },
    new SqlColumn { ColumnName = "RequestPath", DataType = SqlDbType.NVarChar, DataLength = 500 }
};

Log.Logger = new LoggerConfiguration()
    .WriteTo.MSSqlServer(
        connectionString,
        sinkOptions: new MSSqlServerSinkOptions { TableName = "Logs" },
        columnOptions: columnOptions)
    .CreateLogger();

PostgreSQL:

dotnet add package Serilog.Sinks.PostgreSQL

Log.Logger = new LoggerConfiguration()
    .WriteTo.PostgreSQL(
        connectionString: "Host=localhost;Database=logs;Username=postgres;Password=pass",
        tableName: "logs",
        needAutoCreateTable: true)
    .CreateLogger();

5. Cloud Sinks

Application Insights:

dotnet add package Serilog.Sinks.ApplicationInsights

Log.Logger = new LoggerConfiguration()
    .WriteTo.ApplicationInsights(
        telemetryConfiguration: new TelemetryConfiguration 
        { 
            ConnectionString = "your-connection-string" 
        },
        TelemetryConverter.Traces)
    .CreateLogger();

AWS CloudWatch:

dotnet add package Serilog.Sinks.AwsCloudWatch

Log.Logger = new LoggerConfiguration()
    .WriteTo.AmazonCloudWatch(
        logGroup: "/myapp/logs",
        logStreamPrefix: "production",
        cloudWatchClient: new AmazonCloudWatchLogsClient())
    .CreateLogger();

6. Multiple Sinks

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .WriteTo.File("logs/myapp-.txt", rollingInterval: RollingInterval.Day)
    .WriteTo.Seq("http://localhost:5341")
    .WriteTo.MSSqlServer(connectionString, sinkOptions: new MSSqlServerSinkOptions())
    .CreateLogger();

// Different levels per sink
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
    .WriteTo.File("logs/debug-.txt", 
        rollingInterval: RollingInterval.Day,
        restrictedToMinimumLevel: LogEventLevel.Debug)
    .WriteTo.File("logs/errors-.txt",
        rollingInterval: RollingInterval.Day,
        restrictedToMinimumLevel: LogEventLevel.Error)
    .CreateLogger();

Enrichers - Adding Context

1. Built-in Enrichers

dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Process

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()          // From LogContext.PushProperty
    .Enrich.WithMachineName()          // Computer name
    .Enrich.WithEnvironmentName()      // Development/Production
    .Enrich.WithEnvironmentUserName()  // Current user
    .Enrich.WithThreadId()             // Thread ID
    .Enrich.WithThreadName()           // Thread name
    .Enrich.WithProcessId()            // Process ID
    .Enrich.WithProcessName()          // Process name
    .WriteTo.Console()
    .CreateLogger();

2. Custom Enrichers

using Serilog.Core;
using Serilog.Events;

public class ApplicationVersionEnricher : ILogEventEnricher
{
    private LogEventProperty _cachedProperty;

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        _cachedProperty ??= propertyFactory.CreateProperty(
            "ApplicationVersion",
            typeof(Program).Assembly.GetName().Version?.ToString() ?? "Unknown");
        
        logEvent.AddPropertyIfAbsent(_cachedProperty);
    }
}

// Usage
Log.Logger = new LoggerConfiguration()
    .Enrich.With<ApplicationVersionEnricher>()
    .WriteTo.Console()
    .CreateLogger();

Custom Enricher Examples:

// Tenant ID Enricher
public class TenantIdEnricher : ILogEventEnricher
{
    private readonly IHttpContextAccessor _contextAccessor;

    public TenantIdEnricher(IHttpContextAccessor contextAccessor)
    {
        _contextAccessor = contextAccessor;
    }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var tenantId = _contextAccessor.HttpContext?.User?.FindFirst("TenantId")?.Value;
        if (tenantId != null)
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TenantId", tenantId));
        }
    }
}

// Git Commit Enricher
public class GitCommitEnricher : ILogEventEnricher
{
    private LogEventProperty _cachedProperty;

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        _cachedProperty ??= propertyFactory.CreateProperty(
            "GitCommit",
            Environment.GetEnvironmentVariable("GIT_COMMIT") ?? "Unknown");
        
        logEvent.AddPropertyIfAbsent(_cachedProperty);
    }
}

3. ASP.NET Core Request Enrichment

app.UseSerilogRequestLogging(options =>
{
    options.MessageTemplate = 
        "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
    
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
        diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
        diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].ToString());
        
        // Add user information
        if (httpContext.User.Identity?.IsAuthenticated == true)
        {
            diagnosticContext.Set("UserId", httpContext.User.FindFirst("sub")?.Value);
            diagnosticContext.Set("UserName", httpContext.User.Identity.Name);
        }
        
        // Add custom headers
        if (httpContext.Request.Headers.TryGetValue("X-Correlation-Id", out var correlationId))
        {
            diagnosticContext.Set("CorrelationId", correlationId.ToString());
        }
    };
    
    options.GetLevel = (httpContext, elapsed, ex) =>
    {
        if (ex != null) return LogEventLevel.Error;
        if (httpContext.Response.StatusCode > 499) return LogEventLevel.Error;
        if (elapsed > 10000) return LogEventLevel.Warning;  // Slow requests
        return LogEventLevel.Information;
    };
});

Correlation and Distributed Tracing

1. Correlation ID Middleware

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.Add(CorrelationIdHeader, correlationId);

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

// Register in Program.cs
app.UseMiddleware<CorrelationIdMiddleware>();

2. Correlation Across Services

// Service A - API Gateway
public async Task<IActionResult> ProcessOrder(OrderRequest request)
{
    var correlationId = HttpContext.Request.Headers["X-Correlation-Id"].FirstOrDefault()
                        ?? Guid.NewGuid().ToString();
    
    using (LogContext.PushProperty("CorrelationId", correlationId))
    {
        Log.Information("Received order request: {@Request}", request);
        
        // Call downstream service
        using var httpClient = _httpClientFactory.CreateClient();
        httpClient.DefaultRequestHeaders.Add("X-Correlation-Id", correlationId);
        
        var response = await httpClient.PostAsJsonAsync(
            "http://inventory-service/api/check", 
            request);
        
        Log.Information("Inventory check completed");
        return Ok();
    }
}

// Service B - Inventory Service
public async Task<IActionResult> CheckInventory(InventoryRequest request)
{
    var correlationId = HttpContext.Request.Headers["X-Correlation-Id"].FirstOrDefault();
    
    using (LogContext.PushProperty("CorrelationId", correlationId))
    {
        Log.Information("Checking inventory: {@Request}", request);
        
        // All logs in this service now have the same correlation ID
        var result = await _inventoryRepository.Check(request.ProductId);
        
        Log.Information("Inventory check result: {Available}", result.Available);
        return Ok(result);
    }
}

// Now you can query logs across both services using the correlation ID:
// SELECT * FROM Logs WHERE CorrelationId = 'abc-123-def'

3. Activity ID Tracking

using System.Diagnostics;

public class ActivityEnricher : ILogEventEnricher
{
    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var activity = Activity.Current;
        if (activity != null)
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TraceId", activity.TraceId));
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("SpanId", activity.SpanId));
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ParentId", activity.ParentId));
        }
    }
}

Log.Logger = new LoggerConfiguration()
    .Enrich.With<ActivityEnricher>()
    .WriteTo.Console()
    .CreateLogger();

4. User Context Enrichment

public class UserContextEnricher : ILogEventEnricher
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserContextEnricher(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;
            var email = httpContext.User.FindFirst(ClaimTypes.Email)?.Value;
            var roles = httpContext.User.FindAll(ClaimTypes.Role)
                .Select(c => c.Value)
                .ToArray();

            if (userId != null)
                logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("UserId", userId));
            
            if (userName != null)
                logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("UserName", userName));
            
            if (email != null)
                logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("UserEmail", email));
            
            if (roles.Any())
                logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("UserRoles", roles));
        }

        // IP Address
        var ipAddress = httpContext?.Connection?.RemoteIpAddress?.ToString();
        if (ipAddress != null)
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ClientIpAddress", ipAddress));
        }
    }
}

// Register enricher
services.AddHttpContextAccessor();
services.AddSingleton<ILogEventEnricher, UserContextEnricher>();

Log.Logger = new LoggerConfiguration()
    .Enrich.With<UserContextEnricher>()
    .WriteTo.Console()
    .CreateLogger();

Advanced Configuration

1. Minimum Level Overrides

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
    .MinimumLevel.Override("System", LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
    .WriteTo.Console()
    .CreateLogger();

// In appsettings.json
{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.Hosting.Lifetime": "Information",
        "System": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning"
      }
    }
  }
}

2. Filtering

using Serilog.Filters;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .Filter.ByExcluding(Matching.FromSource("Microsoft"))
    .Filter.ByExcluding(Matching.WithProperty("RequestPath", "/health"))
    .Filter.ByExcluding(e => e.Properties.ContainsKey("SensitiveData"))
    .WriteTo.Console()
    .CreateLogger();

// Filter specific sinks
Log.Logger = new LoggerConfiguration()
    .WriteTo.Logger(lc => lc
        .Filter.ByIncludingOnly(e => e.Level >= LogEventLevel.Error)
        .WriteTo.File("logs/errors-.txt", rollingInterval: RollingInterval.Day))
    .WriteTo.Logger(lc => lc
        .Filter.ByIncludingOnly(e => e.Level < LogEventLevel.Error)
        .WriteTo.File("logs/info-.txt", rollingInterval: RollingInterval.Day))
    .CreateLogger();

3. Async Logging

dotnet add package Serilog.Sinks.Async

Log.Logger = new LoggerConfiguration()
    .WriteTo.Async(a => a.File("logs/myapp-.txt", rollingInterval: RollingInterval.Day))
    .WriteTo.Async(a => a.MSSqlServer(connectionString, sinkOptions))
    .CreateLogger();

// With buffering configuration
Log.Logger = new LoggerConfiguration()
    .WriteTo.Async(
        configure => configure.File("logs/myapp-.txt"),
        bufferSize: 10000,
        blockWhenFull: false)
    .CreateLogger();

4. Subloggers

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Logger(lc => lc
        .Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Error)
        .WriteTo.File("logs/errors-.txt"))
    .WriteTo.Logger(lc => lc
        .Filter.ByIncludingOnly(e => 
            e.Properties.ContainsKey("Audit") && 
            (bool)((ScalarValue)e.Properties["Audit"]).Value)
        .WriteTo.File("logs/audit-.txt"))
    .WriteTo.