Serilog Structured Logging in .NET: A Complete Guide
Learn Serilog structured logging in .NET with practical examples, production debugging tips, and best practices.
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
- ✓Basic knowledge of C# and .NET
- ✓Familiarity with ASP.NET Core (for web examples)
- ✓Visual Studio or .NET CLI installed
- 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.
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".
- 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.
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.
Here's a complete example:
MinimumLevel to Warning in production to reduce noise. Use Override to increase verbosity for specific namespaces during debugging.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.
Here's how to configure multiple sinks:
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.
Serilog.Enrichers.CorrelationId package.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.AspNetCoretoWarning). - Filters: Use lambda expressions or
Serilog.Filters.Expressionsto include/exclude events based on properties.
Example: Suppress health check logs.
Microsoft and System to Warning to reduce volume. Use structured filters to exclude health check endpoints.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.
Destructure.ToMaximumDepth to prevent accidental serialization of large object graphs.@ to destructure objects. Implement custom policies to mask sensitive data.Best Practices and Common Pitfalls
- Use named loggers: Inject
ILogger<T>in ASP.NET Core to get a logger with the category name. This helps filtering. - Avoid string interpolation: Always use message templates with placeholders.
Log.Information("User {UserId}", id)notLog.Information($"User {id}"). - Don't log in tight loops: Logging is I/O-bound. Use counters or sample logs instead.
- Use structured exceptions: Log exceptions with
Log.Error(ex, "Message")to capture stack trace and exception properties. - Set up alerts: Use tools like Seq to create alerts on error patterns.
- Test your logging: Write unit tests that verify log events are emitted correctly using
Serilog.Sinks.TestCorrelatororSerilog.Sinks.XUnit.
- Forgetting to call
Log..CloseAndFlush() - Logging sensitive data.
- Using too many sinks (performance).
- Not configuring minimum levels properly.
The Silent Crash: When Logs Lied
- 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.
dotnet run --verbosity detailedCheck SelfLog: Serilog.Debugging.SelfLog.Enable(Console.Error)| File | Command / Code | Purpose |
|---|---|---|
| BasicSetup.cs | using Serilog; | What is Structured Logging and Why Serilog? |
| Program.cs | using Serilog; | Configuring Serilog in ASP.NET Core |
| MultipleSinks.cs | Log.Logger = new LoggerConfiguration() | Serilog Sinks |
| EnrichmentExample.cs | Log.Logger = new LoggerConfiguration() | Enriching Logs with Context |
| Filtering.cs | Log.Logger = new LoggerConfiguration() | Filtering and Controlling Log Output |
| Destructuring.cs | public class Order | Destructuring Complex Objects |
| BestPractices.cs | public class OrderService | Best Practices and Common Pitfalls |
Key takeaways
UseSerilog() in ASP.NET Core.Common mistakes to avoid
4 patternsUsing 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 Questions on This Topic
Explain the difference between structured logging and traditional logging.
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
That's C# Advanced. Mark it forged?
3 min read · try the examples if you haven't