Structured Logging in .NET with Seq: A Developer's Guide
Why Structured Logging?
- Problem: Traditional text logs (e.g
Console.WriteLine
) are hard to query and analyze. - Solution: Structured logging emits logs as machine-readable key/value pairs and enables:
- Filtering (e.g
WHERE UserId = 123
) - Correlation (e.g trace IDs across microservices)
- Integration with dashboards (e.g Seq, Elasticsearch)
- Filtering (e.g
Setting Up Seq
Option A: Local Docker Setup
docker run --name seq -d --restart unless-stopped -e ACCEPT_EULA=Y -p 5341:80 datalust/seq
Access the Seq UI at http://localhost:5341
.
Option B: Cloud Hosting
Sign up for Seq Cloud (free tier available).
Configuring .NET Apps for Seq
Step 1: Install Packages
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Seq
Step 2: Configure Serology (Program.cs)
using Serilog;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.Seq("http://localhost:5341") // Seq server URL
.Enrich.WithProperty("Application", "MyApp") // Global metadata
.CreateLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog(); // <-- Enable Serilog for ASP.NET Core
// ... Your app setup
await app.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
Correlation in Microservices
Challenge:
In distributed systems, a single request can sometimes span across multiple services. Without correlation, debugging is a nightmare.
Solution:
- Assign a Correlation ID at the API gateway (e.g Ocelot)
// Middleware to generate/forward CorrelationId
app.Use(async (ctx, next) =>
{
ctx.Request.Headers.TryGetValue("X-Correlation-ID", out var correlationId);
if (string.IsNullOrEmpty(correlationId))
correlationId = Guid.NewGuid().ToString();
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await next();
}
});
- Enrich logs with the correlation ID:
.Enrich.WithCorrelationID() // Via Serology.Enrichers.CorrelationID
- Forward headers between services (HTTP calls/gRPC)
//HttpClient example
request.Headers.Add("X-Correlation-ID");
A more centralized approach is using DelegatingHandler
to append this header to every request for HttpClients that have been registered in the IHttpClientFactory
.
- Query in Seq:
CorrelationId = 'abc123' | order by @Timestamp
Logging Best Practices
Structured Data (Avoid String Concatenation)
✅ Do this:
Log.Information("Order {Orderid} submitted by {UserId}", order.Id, user.Id);
❌ Not this:
Log.Information($"Order {order.Id} submitted by {user.Id}");
Enrichment (Adding context to logs)
// Add thread/environment info:
.Enrich.WithThreadId()
.Enrich.WithMachineName()
// Custom enricher (e.g., add UserId per request):
.Enrich.With(new UserIdEnricher());
Error Handling
try
{
// Risky operation
}
catch (Exception ex)
{
Log.Error(ex, "Failed to process order {OrderId}", order.Id);
throw; // Re-throw for middleware
}
Querying Logs in Seq
Seq's query language lets you filter, aggregate and visualize logs.
- Filter by property:
Application = 'MyApp' and Level = 'Error'
- Find slow requests:
@Message like '%RequestDuration%' | sort by @Duration desc
Advanced Scenarios
A. Dynamic Log Levels
Update log levels without restarting:
.WriteTo.Seq(serverUrl, controlLevelSwitch: new LoggingLevelSwitch(LogEventLevel.Information))
B. Seq Alerts
Configure email/Slack alerts for critical errors:
In Seq UI, go to Alerts -> Add Alert
Set a condition (e.g Count(Error) > 5 in 5m
`)
Conclusion
Structured logging with Seq transforms debugging from "grepping text files" to "query a database of events". By combining Serilog's rich .NET integration with Sea's analytics, you gain:
- Faster troubleshooting (filter by traces, users or errors)
- Proactive monitoring (alerts for anomalies)
- Audits (retention policies & compliance)