Serilog — Structured Logging for Production
Serilog replaces Console.WriteLine with structured JSON logs that can be queried, filtered, and aggregated in Elastic Stack, Seq, Datadog, or Azure Monitor. Structured logs attach properties (UserId, ProductId, Duration) to log events — not just text messages.
Serilog Setup & Structured Logging
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.Seq (local dev)
// dotnet add package Serilog.Sinks.Elasticsearch (production)
// dotnet add package Serilog.Enrichers.CorrelationId
// ━━ Program.cs — configure Serilog FIRST (before builder.Build()) ━━
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning) // suppress noisy framework logs
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext() // picks up BeginScope properties
.Enrich.WithEnvironmentName()
.Enrich.WithMachineName()
.Enrich.WithProperty("Application", "MyApp")
.Enrich.WithProperty("Version", "1.0.0")
.WriteTo.Console(new Serilog.Formatting.Compact.CompactJsonFormatter()) // JSON in prod
.WriteTo.Seq("http://localhost:5341") // query logs in Seq UI
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elastic:9200"))
{
IndexFormat = "myapp-logs-{0:yyyy-MM}",
AutoRegisterTemplate = true,
OverwriteTemplate = true,
})
.CreateBootstrapLogger(); // early logger (before host is built)
builder.Host.UseSerilog((ctx, services, config) =>
config.ReadFrom.Configuration(ctx.Configuration) // appsettings.json overrides
.ReadFrom.Services(services) // IEnricher from DI
.Enrich.FromLogContext());
// ━━ Request logging middleware ━━
app.UseSerilogRequestLogging(opts =>
{
opts.MessageTemplate = "{RequestMethod} {RequestPath} → {StatusCode} in {Elapsed:0.0}ms";
opts.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
diagnosticContext.Set("UserId", httpContext.User.FindFirst("sub")?.Value ?? "anonymous");
diagnosticContext.Set("CorrelationId", httpContext.Items["CorrelationId"]);
};
});
// ━━ Structured logging in services ━━
public class ProductService(ILogger<ProductService> logger)
{
public async Task<Product?> GetByIdAsync(Guid id)
{
// GOOD: structured — {ProductId} is a searchable property, not just text
logger.LogInformation("Fetching product {ProductId} for user {UserId}", id, GetUserId());
var product = await FetchProduct(id);
if (product is null)
logger.LogWarning("Product {ProductId} not found — returning 404", id);
else
logger.LogDebug("Product {ProductId} loaded from DB in {Category}", id, product.Category);
return product;
}
// Log scope — ALL logs in this scope automatically inherit these properties
public async Task ProcessOrderAsync(Guid orderId)
{
using (logger.BeginScope(new Dictionary<string, object>
{ ["OrderId"] = orderId, ["Operation"] = "ProcessOrder" }))
{
logger.LogInformation("Order processing started"); // has OrderId, Operation
// ... processing ...
logger.LogInformation("Order processing complete"); // has OrderId, Operation
}
}
private static Guid GetUserId() => Guid.NewGuid();
private static Task<Product?> FetchProduct(Guid id) => Task.FromResult<Product?>(null);
}
// ━━ appsettings.json — override log levels per environment ━━
// {
// "Serilog": {
// "MinimumLevel": {
// "Default": "Information",
// "Override": {
// "Microsoft.EntityFrameworkCore.Database.Command": "Warning",
// "Microsoft.AspNetCore.Routing": "Warning"
// }
// }
// }
// }
class Product { public string Category { get; set; } = ""; }Tip
Tip
Practice Serilog Structured Logging for Production in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
.NET provides a unified platform for building various application types
Practice Task
Note
Practice Task — (1) Write a working example of Serilog Structured Logging for Production from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Serilog Structured Logging for Production is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready dotnet code.