Skip to main content
SEMastery
DevOpsintermediate

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.

11 min readUpdated February 28, 2026

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

Order
Kitchen
Rider
Doorstep

Steps

1

Order

You place it, get a number

2

Kitchen

Cooks, notes the time

3

Rider

Picks up, shares location

4

Doorstep

Delivered, journey done

One order number follows the whole journey, just like a trace ID.

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 loggingStructured logging
Logs are one long sentenceLogs have named fields
Search needs guessing and grepSearch by field, like a database
Hard to count or filterEasy to count, filter, and chart
Good for tiny appsGood 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.

One request creates a trace made of many spans across services.

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

Three services send logs and traces to one Seq instance.

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:latest

Open 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

Pull image
Run container
Open UI

Steps

1

Pull image

docker pulls datalust/seq

2

Run container

Set ACCEPT_EULA=Y

3

Open UI

Visit localhost:5341

From zero to a working log screen in three steps.

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

Now 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.OpenTelemetryProtocol

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

SignalWhat it answersHow 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

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.

The debugging flow once logs and traces are linked by trace ID.

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

Request in
Order span
Payment call
Logs to Seq

Steps

1

Request in

ASP.NET starts a root span

2

Order span

Order logic runs, logs added

3

Payment call

Trace ID flows in HTTP header

4

Logs to Seq

All linked by TraceId

Every box writes a structured log and a span with the same trace ID.

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 localhost inside Docker. Containers cannot see each other through localhost. Use the service name from your Compose file, like http://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.

Optional collector sits between services and Seq for batching and filtering.

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 Activity give 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 localhost inside 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

Related Posts