Structured Logging in C# with Serilog
Structured logging is a modern approach to application logging that treats log entries as structured data rather than flat text strings. This article explores what structured logging libraries like Serilog are, why they are essential in modern software development, and how they capture, serialize, and format log events in C#.
Understanding Structured Logging vs. Traditional Logging
Traditional logging formats data into unformatted strings using standard string concatenation or interpolation. For example:
// Traditional string formatting
logger.Info($"Order {orderId} processed for customer {customerId} with total {amount}.");While readable by humans, this approach produces flat text. Extracting specific fields—such as searching for all orders where the amount exceeded $100—requires complex regular expressions that break easily if the message wording changes.
Structured logging solves this by preserving the original data types and values alongside the human-readable message. Instead of flattening data into a single string at the moment of logging, it captures fields as key-value pairs, often formatted as JSON:
{
"@timestamp": "2023-10-25T14:30:00Z",
"level": "Information",
"messageTemplate": "Order {OrderId} processed for customer {CustomerId} with total {Amount}.",
"properties": {
"OrderId": 1042,
"CustomerId": "CUST-987",
"Amount": 150.75
}
}This format allows modern log ingestion systems (such as Seq, Elasticsearch, Datadog, or Azure Application Insights) to index each field natively, enabling fast searching, filtering, and aggregation.
What is Serilog?
Serilog is one of the most widely used structured logging libraries
for the .NET ecosystem. It integrates natively with the
Microsoft.Extensions.Logging abstraction and provides a
modular architecture composed of:
- Message Templates: The core mechanism for defining structured data in log statements.
- Sinks: Destinations for log events, such as the Console, local files, relational databases, or cloud-based aggregators.
- Enrichers: Middleware that automatically attaches contextual properties (such as Machine Name, Thread ID, or Correlation IDs) to every log entry.
How Serilog Formats Logs in C
Serilog achieves structured logging through a concept called Message Templates. A message template uses named placeholders inside a string literal instead of positional or interpolated parameters.
1. Capturing Scalar Values
When logging simple data types (strings, integers, dates), provide property names within curly braces:
int orderId = 1042;
string customerId = "CUST-987";
decimal amount = 150.75m;
// Correct: Using Serilog message templates
logger.Information("Order {OrderId} processed for customer {CustomerId} with total {Amount}",
orderId, customerId, amount);Behind the scenes, Serilog parses {OrderId},
{CustomerId}, and {Amount} as distinct
properties, captures their runtime values, and maintains them in an
event property dictionary.
2. Capturing Complex Objects (Destructuring)
By default, passing a complex C# object to a placeholder calls its
.ToString() method. To instruct Serilog to serialize the
entire object graph into structured properties, prefix the identifier
with the destructuring operator (@):
var customer = new Customer
{
Id = "CUST-987",
Name = "Jane Doe",
Email = "jane@example.com"
};
// Destructuring the customer object into JSON properties
logger.Information("Created new customer: {@Customer}", customer);This generates a nested JSON object within the log payload, allowing
individual properties like Customer.Email to be queried
independently in log management tools.
3. Stringification Operator
If you want to ensure a complex object is explicitly captured as a
flat string rather than decomposed, prefix the property name with
$:
logger.Information("Processed payload: {$Payload}", complexPayload);Basic Configuration Example in C
Configuring Serilog in a modern .NET application involves creating a
LoggerConfiguration and directing the output to the desired
sinks.
using Serilog;
using Serilog.Formatting.Json;
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console() // Human-readable console output
.WriteTo.File(new JsonFormatter(), "logs/app-log.json", rollingInterval: RollingInterval.Day) // Structured JSON
.CreateLogger();
try
{
Log.Information("Application starting up on {Environment}", "Production");
// Application code...
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}In this setup, standard console logs remain easy to read during local development, while file output is written as structured JSON ready for ingestion into monitoring platforms.