Structured Logging in .NET with Seq: A Developer's Guide

.Net Core Oct 27, 2023

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)

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:

  1. 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();  
    }  
});  
  1. Enrich logs with the correlation ID:

.Enrich.WithCorrelationID() // Via Serology.Enrichers.CorrelationID

  1. 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.

  1. 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)

Tags

Views: Loading...