Structured Logging and Distributed Tracing for Microservices with Seq
Learn to add structured logging with Serilog and distributed tracing with OpenTelemetry to .NET microservices, then view it all in Seq with one trace ID.
When you build microservices, your code stops living in one place. One user click might travel through five small services before it finishes. If something breaks, where do you even look? This is the problem that structured logging and distributed tracing solve, and Seq is a friendly tool that shows it all in one screen.
A real-life story to start
Imagine you order food on a delivery app in your city. Your order is not handled by one person. The app takes it, a restaurant cooks it, a delivery partner picks it up, and finally it reaches your door.
Now imagine your food is late. You call support. The helpful person on the phone asks for your order number. With that one number, they can see every step: when the kitchen got it, when the rider left, where the rider is now. They do not read a thousand random notes. They follow one number through the whole journey.
In microservices, that order number is called a trace ID. Structured logging writes down clear notes at each step. Distributed tracing ties all the notes together with that one trace ID. Seq is the screen where the support person looks everything up.
The food delivery analogy
Steps
Order
You place it, get a number
Kitchen
Cooks, notes the time
Rider
Picks up, shares location
Doorstep
Delivered, journey done
What is structured logging?
Most beginners write logs like this:
// Plain text log - hard to search later
logger.LogInformation("User 42 placed order 9981 for 3 items");This is just a sentence. A computer cannot easily ask "show me every order with more than 2 items." It would have to read the sentence like a human.
Structured logging stores the values separately, with names. Serilog, the most popular logging library for .NET, does this for you:
// Structured log - each value has a name
logger.LogInformation(
"User {UserId} placed order {OrderId} for {ItemCount} items",
userId, orderId, itemCount);Now Seq stores UserId, OrderId, and ItemCount as real fields. You can search ItemCount > 2 and get an instant answer. The message still reads nicely for humans, but the data underneath is clean and searchable.
| Plain text logging | Structured logging |
|---|---|
| Logs are one long sentence | Logs have named fields |
| Search needs guessing and grep | Search by field, like a database |
| Hard to count or filter | Easy to count, filter, and chart |
| Good for tiny apps | Good for real microservices |
What is distributed tracing?
A trace is the full story of one request as it moves across services. Each small step inside that story is called a span. A span has a start time, an end time, and a name like "save order to database."
All spans of one request share the same trace ID. Child spans also remember their parent span, so you can draw the request as a tree. This is how you see that the slow part was the payment service, not your own code.
In modern .NET, tracing is built right into the runtime. The System.Diagnostics.Activity class is a span. OpenTelemetry is the open standard that collects these activities and ships them out. The best part: .NET creates many spans automatically for HTTP calls and database queries, so you get a lot for free.
Where Seq fits
Seq is a log and trace server. You run it, point your services at it, and then you search. It speaks two languages your .NET app already knows:
- Serilog sink for structured logs.
- OTLP (the OpenTelemetry Protocol) for traces.
Because Seq understands both, every log line and every trace land in the same place. And when a log carries the trace ID, Seq links them, so one click takes you from "this error happened" to "here is the whole request that caused it."
Step 1: Run Seq in Docker
The easiest way to start is Docker. This single command runs Seq on your machine. Port 5341 is for log ingestion and the UI, and 5342 is added for clean health checks. Accepting the EULA is required to start.
// Run this in your terminal, not in C#:
// docker run --name seq -d --restart unless-stopped \
// -e ACCEPT_EULA=Y \
// -p 5341:80 \
// datalust/seq:latestOpen http://localhost:5341 in your browser. You will see the empty Seq dashboard, waiting for logs. When your services run inside Docker too, they should not use localhost. They use the container name, like http://seq:5341, because inside Docker localhost means "just me."
Getting Seq ready
Steps
Pull image
docker pulls datalust/seq
Run container
Set ACCEPT_EULA=Y
Open UI
Visit localhost:5341
Step 2: Add Serilog and send logs to Seq
Add the NuGet packages to your ASP.NET Core service:
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.SeqNow wire Serilog into your app. This setup writes logs to both the console (handy while you code) and Seq (for searching later). The Enrich calls attach extra fields to every log line automatically.
using Serilog;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, services, config) => config
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithProperty("Service", "OrderService")
.WriteTo.Console()
.WriteTo.Seq("http://localhost:5341"));
var app = builder.Build();
app.MapGet("/orders/{id}", (int id, ILogger<Program> logger) =>
{
logger.LogInformation("Fetched order {OrderId}", id);
return Results.Ok(new { id });
});
app.Run();Note how the route /orders/{id} is written in backticks when I mention it in normal text, but inside the code block the braces are perfectly fine. That difference matters a lot in markdown, so keep route names in code font when you write about them in prose.
When you call this endpoint, refresh Seq. Your log appears with a real OrderId field you can click and filter on. That is structured logging working end to end.
Tip: keep config in appsettings.json
Hard-coding the Seq URL is fine for learning. In real projects, put it in appsettings.json so each environment can change it without touching code. Use http://seq:5341 in Docker Compose and http://localhost:5341 on your laptop.
Step 3: Add OpenTelemetry tracing to Seq
Now we add traces. Seq accepts traces over OTLP at the path /ingest/otlp/v1/traces. Add these packages:
// dotnet add package OpenTelemetry.Extensions.Hosting
// dotnet add package OpenTelemetry.Instrumentation.AspNetCore
// dotnet add package OpenTelemetry.Instrumentation.Http
// dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocolThen register tracing. The instrumentation lines tell OpenTelemetry to create spans for incoming requests and outgoing HTTP calls without you writing any extra code.
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("OrderService"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://localhost:5341/ingest/otlp/v1/traces");
options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;
}));That is it. Run two services that call each other over HTTP, and OpenTelemetry passes the trace ID across the call automatically using standard headers. In Seq you will now see one trace with spans from both services, drawn as a tree.
| Signal | What it answers | How Seq receives it |
|---|---|---|
| Structured log | "What happened at this moment?" | Serilog Seq sink |
| Trace | "What was the full path of this request?" | OpenTelemetry OTLP |
| Span | "How long did this one step take?" | Part of a trace |
Step 4: Link logs to traces
The magic moment is connecting the two. When a log line carries the same TraceId as the trace, Seq lets you jump between them. Good news: when OpenTelemetry is active, .NET fills in Activity.Current, and Serilog's enricher can read it.
// dotnet add package Serilog.Enrichers.Span (community enricher)
builder.Host.UseSerilog((context, services, config) => config
.Enrich.FromLogContext()
.Enrich.WithSpan() // adds TraceId and SpanId to every log
.WriteTo.Seq("http://localhost:5341"));Now click any error log in Seq, see its TraceId, and open the matching trace. You go from "the order failed" to "here is exactly which service was slow" in seconds. This is the whole point of observability: less guessing, more seeing.
How a request flows through everything
Let us walk one real request from start to finish, so the pieces click together. A customer fetches an order. The Order Service handles it, then calls the Payment Service. Both write structured logs and both emit spans, all sharing one trace ID.
One request, full observability
Steps
Request in
ASP.NET starts a root span
Order span
Order logic runs, logs added
Payment call
Trace ID flows in HTTP header
Logs to Seq
All linked by TraceId
If the payment step is slow, the Payment span shows a long duration in Seq. You did not add a stopwatch anywhere. The platform measured it for you. That is the power of building on .NET's native Activity and the OpenTelemetry standard.
Common mistakes beginners make
A few traps catch almost everyone the first time. Knowing them now saves you hours later.
- Using
localhostinside Docker. Containers cannot see each other throughlocalhost. Use the service name from your Compose file, likehttp://seq:5341. - Logging secrets. Never put passwords, card numbers, or tokens into log fields. Logs are searchable, which is great, until someone searches for the wrong thing. Mask sensitive values before logging.
- Console-only logging in production. Writing only to the console can block your app under heavy load. Always add an async-friendly sink like Seq for real workloads.
- Forgetting to flush on shutdown. Call
Log.CloseAndFlush()when the app stops so the last logs are not lost. Serilog's ASP.NET integration handles most of this for you. - Too many or too few logs. Logging every tiny step floods Seq and costs money. Logging nothing leaves you blind. Log decisions, errors, and boundaries between services.
A note on tooling licenses
The .NET ecosystem changes, so check licenses before you commit. Serilog, OpenTelemetry, and the OTLP exporter are open source and free to use. Seq has a free Individual license that is perfect for learning and small teams, with paid tiers for bigger groups. Separately, be aware that some popular libraries like MediatR and MassTransit have moved to commercial licensing for newer versions. They are not needed for this guide, but it is good to know when you plan a real microservices project.
Putting it in a microservices picture
In a real system you will have several services, each with the same Serilog and OpenTelemetry setup, all pointing at one Seq instance. Some teams add an OpenTelemetry Collector in the middle so services do not talk to Seq directly. The collector receives traces, can batch and filter them, then forwards to Seq. For learning, sending straight to Seq is simpler and totally fine.
Quick recap
- Structured logging stores named fields, not just sentences, so logs become searchable like a database. Serilog is the go-to library for .NET.
- Distributed tracing follows one request across services using a shared trace ID, made of many spans. OpenTelemetry and .NET's
Activitygive you this, largely for free. - Seq is one screen for both logs and traces. It reads Serilog logs through its sink and OpenTelemetry traces through OTLP at
/ingest/otlp/v1/traces. - Add the Span enricher so every log carries the trace ID. Then you can jump from a single error log to the full request journey in one click.
- Watch out for
localhostinside Docker, logging secrets, and console-only logging in production. - Serilog and OpenTelemetry are free and open source; Seq has a free tier; note that MediatR and MassTransit newer versions are now commercial.
References and further reading
- Seq docs: Tracing from OpenTelemetry SDKs
- Seq docs: Logging from OpenTelemetry
- OpenTelemetry .NET documentation
- OpenTelemetry: Traces concept
- Serilog on GitHub
- Anton Martyniuk: Structured logging and distributed tracing with Seq
- Milan Jovanovic: 5 Serilog best practices for better structured logging
Related Posts
Getting Started With OpenTelemetry in .NET With Jaeger and Seq
A beginner guide to OpenTelemetry in .NET. Add traces, metrics, and logs, then view them in Jaeger and Seq using the OTLP exporter step by step.
Service Discovery in .NET Microservices with HashiCorp Consul
A beginner-friendly guide to service discovery in .NET microservices using HashiCorp Consul, with registration, health checks, and lookups explained simply.
Monitoring .NET Applications With OpenTelemetry and Grafana
A beginner-friendly guide to monitoring .NET apps with OpenTelemetry and Grafana. Send metrics, traces, and logs to Prometheus, Tempo, and Loki step by step.
Getting Started With Dapr for Building Cloud-Native Microservices in .NET
A beginner-friendly guide to Dapr for .NET developers: learn sidecars, state, pub/sub, and service invocation to build cloud-native microservices.
Introduction to Dapr for .NET Developers: A Beginner Guide
A warm, beginner-friendly introduction to Dapr for .NET developers, covering sidecars, building blocks, state, pub/sub, and service invocation in plain C#.
Logging Best Practices in ASP.NET Core: A Beginner's Guide
Learn logging best practices in ASP.NET Core: log levels, structured logging, source-generated LoggerMessage, scopes, correlation IDs, and keeping secrets out.