Home C# / .NET Serilog Structured Logging in .NET: A Complete Guide
Intermediate 3 min · July 13, 2026

Serilog Structured Logging in .NET: A Complete Guide

Learn Serilog structured logging in .NET with practical examples, production debugging tips, and best practices.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of C# and .NET
  • Familiarity with ASP.NET Core (for web examples)
  • Visual Studio or .NET CLI installed
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Serilog is a structured logging library for .NET that captures log events as structured data (key-value pairs) instead of plain text.
  • It supports multiple sinks (console, file, database, cloud) and enrichers to add contextual data automatically.
  • Structured logs enable powerful querying and analysis using tools like Seq, Elasticsearch, or Azure Monitor.
  • Configuration is done via code or appsettings.json, with support for minimum levels, overrides, and filters.
  • Best practices include using named loggers, avoiding string interpolation, and enriching with correlation IDs.
✦ Definition~90s read
What is Serilog and Structured Logging in .NET?

Serilog is a structured logging library for .NET that lets you capture log events as structured data with named properties, making them searchable and analyzable.

Think of logging like a detective's notebook.
Plain-English First

Think of logging like a detective's notebook. Traditional logging is like writing a messy paragraph: "User logged in at 3pm." Structured logging is like filling out a form with fields: [User: John, Action: Login, Time: 3pm, IP: 192.168.1.1]. Later, you can search for all logins by John or find all actions from that IP instantly. Serilog is the tool that helps you write those forms automatically.

Imagine you're debugging a production outage. A user reports an error, but your logs only show: "Error processing request." No context, no user ID, no stack trace. You're blind. This is the reality of traditional text-based logging. Enter Serilog and structured logging.

Structured logging treats log events as data, not just strings. Each log entry is a structured object with named properties like {UserId}, {OrderId}, {Duration}. This transforms your logs from a firehose of text into a queryable database. You can ask: "Show me all errors for user 42 in the last hour" or "What's the average response time for /api/orders?"

In this tutorial, you'll learn how to integrate Serilog into your .NET applications, configure sinks (destinations) like console, files, and Seq, enrich logs with contextual data, and avoid common pitfalls. We'll cover real-world scenarios including production debugging, performance considerations, and security best practices. By the end, you'll be able to implement structured logging that makes debugging a breeze.

What is Structured Logging and Why Serilog?

Structured logging is the practice of emitting log events as structured data (e.g., JSON) rather than plain text. Each log event contains named properties that can be queried, filtered, and aggregated. Serilog is the most popular structured logging library for .NET, offering a simple API, extensive sink support, and high performance.

Traditional logging: log.Info("User {0} logged in from {1}", userId, ip) produces a string like "User 42 logged in from 192.168.1.1". To find all logins from a specific IP, you'd need to parse strings. With Serilog: log.Information("User {UserId} logged in from {ClientIP}", userId, ip) produces a JSON object with UserId and ClientIP properties. You can then query: "Show me all events where ClientIP = 192.168.1.1".

Serilog's key features
  • Sinks: Write logs to console, files, databases (SQL, MongoDB), cloud services (Azure, AWS), and more.
  • Enrichers: Automatically add contextual data (e.g., machine name, thread ID, environment).
  • Filters: Control which events are written based on level, source, or properties.
  • Destructuring: Control how complex objects are serialized.
  • Sub-loggers: Route events to different sinks based on criteria.

Let's start by installing Serilog and configuring a basic logger.

BasicSetup.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
using Serilog;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .WriteTo.Console()
    .WriteTo.File("logs/myapp.log", rollingInterval: RollingInterval.Day)
    .CreateLogger();

Log.Information("Application starting up");
Log.Warning("This is a warning with {Property}", 42);
Log.Error(new Exception("Something broke"), "Error occurred");

Log.CloseAndFlush();
Output
[12:00:00 INF] Application starting up
[12:00:00 WRN] This is a warning with 42
[12:00:00 ERR] Error occurred
System.Exception: Something broke
at ...
🔥Always Flush
📊 Production Insight
In production, use asynchronous sinks to avoid blocking the main thread. Serilog's file sink supports buffering.
🎯 Key Takeaway
Structured logging turns logs into queryable data. Serilog makes it easy with sinks, enrichers, and a fluent API.

Configuring Serilog in ASP.NET Core

In ASP.NET Core, Serilog integrates seamlessly with the built-in logging abstraction. You can configure Serilog in Program.cs or via appsettings.json. The recommended approach is to use the UseSerilog extension method.

First, install the package: dotnet add package Serilog.AspNetCore.

Then, in Program.cs, call UseSerilog() on the host builder. You can configure the logger before building the host to capture early startup logs.

Configuration via appsettings.json is powerful: you can define sinks, minimum levels, overrides per namespace, and enrichers without recompiling. The Serilog.Settings.Configuration package reads the configuration.

Program.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Serilog;

Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .CreateLogger();

builder.Host.UseSerilog();

var app = builder.Build();

app.MapGet("/", () =>
{
    Log.Information("Handling request");
    return "Hello World!";
});

app.Run();
💡Early Logging
📊 Production Insight
Set MinimumLevel to Warning in production to reduce noise. Use Override to increase verbosity for specific namespaces during debugging.
🎯 Key Takeaway
Use UseSerilog() in ASP.NET Core and configure via appsettings.json for flexibility.

Serilog Sinks: Where Your Logs Go

Serilog sinks are destinations for log events. The most common sinks include: - Console: Great for development and Docker containers. - File: Writes to rolling files by day, size, or both. - Seq: A structured log server that provides a web UI for querying. - Elasticsearch: Indexes logs into Elasticsearch for Kibana dashboards. - Application Insights: Sends logs to Azure. - MongoDB: Stores logs in MongoDB collections.

Each sink has its own configuration options. For example, the file sink supports rollingInterval, retainedFileCountLimit, and buffered. The Seq sink requires a server URL and optional API key.

MultipleSinks.csCSHARP
1
2
3
4
5
6
7
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day)
    .WriteTo.Seq("http://localhost:5341", apiKey: "myApiKey")
    .WriteTo.ApplicationInsights(telemetryClient, TelemetryConverter.Traces)
    .CreateLogger();
⚠ Sink Performance
📊 Production Insight
In production, avoid logging to the same file from multiple processes. Use a centralized sink like Seq or Elasticsearch.
🎯 Key Takeaway
Choose sinks based on your infrastructure. Seq is excellent for development and small teams; Elasticsearch for large-scale analytics.

Enriching Logs with Context

Enrichers add additional properties to every log event automatically. Common enrichers include: - WithMachineName(): Adds the machine name. - WithThreadId(): Adds the managed thread ID. - WithEnvironmentName(): Adds the environment (Development, Production). - FromLogContext(): Allows you to push properties dynamically using LogContext.PushProperty.

You can also create custom enrichers by implementing ILogEventEnricher.

Dynamic enrichment is powerful for adding correlation IDs, user IDs, or request-specific data. In ASP.NET Core, you can use middleware to push properties into the log context.

EnrichmentExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Static enrichment
Log.Logger = new LoggerConfiguration()
    .Enrich.WithMachineName()
    .Enrich.WithThreadId()
    .Enrich.WithEnvironmentName()
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .CreateLogger();

// Dynamic enrichment using LogContext
using (LogContext.PushProperty("UserId", 42))
{
    Log.Information("Processing order");
    // This log event will have UserId=42
}

// In ASP.NET Core middleware
app.Use(async (context, next) =>
{
    using (LogContext.PushProperty("CorrelationId", context.TraceIdentifier))
    {
        await next();
    }
});
Output
[12:00:00 INF] Processing order {MachineName="MyPC", ThreadId=1, EnvironmentName="Development", UserId=42}
💡Avoid Over-Enriching
📊 Production Insight
In microservices, always enrich with a correlation ID to trace requests across services. Use the Serilog.Enrichers.CorrelationId package.
🎯 Key Takeaway
Enrichers add automatic context. Use FromLogContext for dynamic properties like correlation IDs.

Filtering and Controlling Log Output

Serilog provides powerful filtering capabilities to control which events are written. You can filter by level, by source context, or by custom criteria.

  • Minimum Level: Set a global minimum level (e.g., Information).
  • Override: Increase or decrease level for specific namespaces (e.g., Microsoft.AspNetCore to Warning).
  • Filters: Use lambda expressions or Serilog.Filters.Expressions to include/exclude events based on properties.

Example: Suppress health check logs.

Filtering.csCSHARP
1
2
3
4
5
6
7
8
9
10
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .MinimumLevel.Override("System", LogEventLevel.Error)
    .Filter.ByExcluding(e => e.Properties.ContainsKey("HealthCheck"))
    .WriteTo.Console()
    .CreateLogger();

// Or using expression filter (requires Serilog.Filters.Expressions)
.Filter.ByExcluding("HealthCheck = true")
🔥Filter Performance
📊 Production Insight
In production, set Microsoft and System to Warning to reduce volume. Use structured filters to exclude health check endpoints.
🎯 Key Takeaway
Use MinimumLevel.Override to reduce noise from framework logs. Use filters to exclude specific events.

Destructuring Complex Objects

By default, Serilog calls ToString() on objects. For complex objects, you may want to destructure them into their properties. Use the @ operator to force destructuring.

You can also create custom destructuring policies to control how specific types are serialized. This is useful for security (e.g., masking credit card numbers) or for reducing log size.

Example: Destructure an order object.

Destructuring.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Order
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
    public string CreditCard { get; set; }
}

// In logger configuration
Log.Logger = new LoggerConfiguration()
    .Destructure.ToMaximumDepth(4)
    .Destructure.ToMaximumStringLength(100)
    .Destructure.ToMaximumCollectionCount(10)
    .Destructure.With<MaskCreditCardPolicy>()
    .WriteTo.Console()
    .CreateLogger();

// Custom destructuring policy
public class MaskCreditCardPolicy : IDestructuringPolicy
{
    public bool TryDestructure(object value, ILogEventPropertyValueFactory factory, out LogEventPropertyValue result)
    {
        if (value is string s && s.Length == 16 && s.All(char.IsDigit))
        {
            result = new ScalarValue("****-****-****-" + s[^4..]);
            return true;
        }
        result = null;
        return false;
    }
}

// Usage
var order = new Order { Id = 1, Amount = 100, CreditCard = "1234567812345678" };
Log.Information("Order processed: {@Order}", order);
Output
[12:00:00 INF] Order processed: {@Order} {Id=1, Amount=100, CreditCard="****-****-****-5678"}
⚠ Security First
📊 Production Insight
Audit your logs regularly to ensure no sensitive data leaks. Use Serilog's Destructure.ToMaximumDepth to prevent accidental serialization of large object graphs.
🎯 Key Takeaway
Use @ to destructure objects. Implement custom policies to mask sensitive data.

Best Practices and Common Pitfalls

  1. Use named loggers: Inject ILogger<T> in ASP.NET Core to get a logger with the category name. This helps filtering.
  2. Avoid string interpolation: Always use message templates with placeholders. Log.Information("User {UserId}", id) not Log.Information($"User {id}").
  3. Don't log in tight loops: Logging is I/O-bound. Use counters or sample logs instead.
  4. Use structured exceptions: Log exceptions with Log.Error(ex, "Message") to capture stack trace and exception properties.
  5. Set up alerts: Use tools like Seq to create alerts on error patterns.
  6. Test your logging: Write unit tests that verify log events are emitted correctly using Serilog.Sinks.TestCorrelator or Serilog.Sinks.XUnit.
Common mistakes
  • Forgetting to call Log.CloseAndFlush().
  • Logging sensitive data.
  • Using too many sinks (performance).
  • Not configuring minimum levels properly.
BestPractices.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Good: named logger
public class OrderService
{
    private readonly ILogger<OrderService> _logger;
    public OrderService(ILogger<OrderService> logger) => _logger = logger;
    
    public void Process(Order order)
    {
        _logger.LogInformation("Processing order {OrderId}", order.Id);
    }
}

// Bad: string interpolation
Log.Information($"Processing order {order.Id}"); // This loses the property

// Good: structured exception
Log.Error(ex, "Failed to process order {OrderId}", order.Id);
💡Test Your Logs
📊 Production Insight
Set up a dashboard (e.g., Seq, Kibana) to monitor log volume and error rates. Use alerts to detect anomalies.
🎯 Key Takeaway
Follow best practices: use named loggers, avoid interpolation, and test your logging.
● Production incidentPOST-MORTEMseverity: high

The Silent Crash: When Logs Lied

Symptom
Users reported failed payments, but logs only showed generic 'Transaction failed' messages.
Assumption
Developers assumed the payment gateway was down.
Root cause
A database connection pool exhaustion caused timeouts, but the log message didn't include the connection string or pool stats.
Fix
Added structured logging with connection pool metrics and correlation IDs. The next incident was resolved in minutes.
Key lesson
  • Always log structured data (e.g., connection pool size, timeout duration).
  • Use correlation IDs to trace requests across services.
  • Avoid logging sensitive data like full credit card numbers.
  • Set up alerts on structured log patterns (e.g., 'pool exhausted').
  • Regularly review logs for missing context.
Production debug guideSymptom to Action4 entries
Symptom · 01
Logs are not appearing in the expected sink (e.g., file or Seq).
Fix
Check the minimum level configuration and ensure the sink is properly configured. Verify that the log level (e.g., Information) matches the sink's level. Use SelfLog to capture Serilog internal errors.
Symptom · 02
Logs are missing contextual properties (e.g., UserId).
Fix
Ensure enrichers are added to the logger configuration. Check that the enricher is not conditionally disabled. Verify that the property is being passed correctly in the log call.
Symptom · 03
Performance degradation due to logging.
Fix
Use asynchronous sinks (e.g., File with buffering). Reduce log level in production to Warning or Error. Avoid expensive operations in enrichers. Use Serilog's filtering to drop verbose logs.
Symptom · 04
Sensitive data appears in logs.
Fix
Implement a custom destructuring policy to mask or redact sensitive properties. Use Serilog's Filters to exclude certain properties. Review log output regularly.
★ Quick Debug Cheat SheetCommon Serilog issues and immediate fixes.
No logs at all
Immediate action
Check appsettings.json for correct Serilog section. Verify logger is created.
Commands
dotnet run --verbosity detailed
Check SelfLog: Serilog.Debugging.SelfLog.Enable(Console.Error)
Fix now
Add a console sink with minimum level Debug.
Missing properties+
Immediate action
Check enrichers in configuration.
Commands
Log.Information("Test {Property}", "value")
Verify enricher: .Enrich.WithProperty("App", "MyApp")
Fix now
Add .Enrich.FromLogContext() and use LogContext.PushProperty.
Seq not receiving logs+
Immediate action
Check Seq URL and API key.
Commands
curl http://seq-server:5341
Check Serilog internal logs: SelfLog.Enable(msg => Console.WriteLine(msg))
Fix now
Verify network connectivity and sink configuration.
FeatureSerilogNLoglog4net
Structured loggingFirst-class supportVia layoutsLimited
Sinks100+50+30+
ConfigurationCode or JSONXML or JSONXML
PerformanceHighHighMedium
ASP.NET Core integrationExcellent (UseSerilog)Good (NLog.Web)Manual
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
BasicSetup.csusing Serilog;What is Structured Logging and Why Serilog?
Program.csusing Serilog;Configuring Serilog in ASP.NET Core
MultipleSinks.csLog.Logger = new LoggerConfiguration()Serilog Sinks
EnrichmentExample.csLog.Logger = new LoggerConfiguration()Enriching Logs with Context
Filtering.csLog.Logger = new LoggerConfiguration()Filtering and Controlling Log Output
Destructuring.cspublic class OrderDestructuring Complex Objects
BestPractices.cspublic class OrderServiceBest Practices and Common Pitfalls

Key takeaways

1
Structured logging with Serilog turns logs into queryable data, enabling faster debugging and analysis.
2
Configure Serilog via appsettings.json for flexibility and use UseSerilog() in ASP.NET Core.
3
Enrich logs with contextual data like correlation IDs and machine names, but avoid over-enriching.
4
Always mask sensitive data using custom destructuring policies.
5
Test your logging configuration and monitor log volume in production.

Common mistakes to avoid

4 patterns
×

Using string interpolation in log messages

×

Not calling `Log.CloseAndFlush()` before app exit

×

Logging sensitive data like passwords or credit cards

×

Configuring too many sinks without performance consideration

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the difference between structured logging and traditional loggin...
Q02SENIOR
How do you configure Serilog in an ASP.NET Core application?
Q03JUNIOR
What is a Serilog sink and give examples?
Q04SENIOR
How can you mask sensitive data in Serilog logs?
Q05SENIOR
What is the purpose of `LogContext.PushProperty`?
Q01 of 05JUNIOR

Explain the difference between structured logging and traditional logging.

ANSWER
Traditional logging produces plain text messages, making it hard to search and analyze. Structured logging emits log events as structured data (e.g., JSON) with named properties, enabling powerful querying, filtering, and aggregation.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Serilog and Microsoft.Extensions.Logging?
02
How do I configure Serilog to log to a database?
03
Can I use Serilog with .NET Framework?
04
How do I pass correlation ID across microservices?
05
Is Serilog thread-safe?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
🔥

That's C# Advanced. Mark it forged?

3 min read · try the examples if you haven't

Previous
BenchmarkDotNet for .NET Performance
21 / 22 · C# Advanced
Next
Configuration and Options Pattern in .NET