In a monolithic application, when a request is slow, you can fire up a profiler and find the bottleneck. In a microservices architecture, it’s not so simple. A single user request might travel through five, ten, or even more services before a response is returned.
If that request is slow, where is the problem? Is it Service A’s database query? Is it the network call from Service B to Service C? Is Service D waiting on an external API?
This is the problem that distributed tracing solves. It allows you to visualize the entire lifecycle of a request as it flows through your system, giving you a detailed breakdown of how much time was spent in each service.
The industry standard for implementing tracing is OpenTelemetry (OTel), a vendor-neutral, open-source observability framework.
The Core Concepts of Distributed Tracing#
- Trace: Represents the entire journey of a request. A trace is a collection of spans.
- Span: Represents a single unit of work within a trace, like an HTTP call, a database query, or a specific method execution. Spans have a start time, a duration, and can be nested.
- Trace Context: A set of unique identifiers (
TraceId,SpanId) that are passed between services with each request (usually as HTTP headers). This context is what allows the system to stitch the individual spans together into a single, coherent trace.
Implementing OpenTelemetry in .NET#
Let’s imagine we have two services: an OrderService (an ASP.NET Core Web API) that receives the initial request, and a ProductService that it calls to get product details.
Step 1: Add the NuGet Packages#
You’ll need a few packages in both of your service projects.
| |
Other exporters are available for systems like Jaeger, Zipkin, or Application Insights.
Step 2: Configure OpenTelemetry in Program.cs#
In both OrderService and ProductService, you need to configure OpenTelemetry.
| |
That’s the basic setup. With just this code, OpenTelemetry will automatically:
- Start a new trace and a root span for every incoming request to
OrderService. - When
OrderServicemakes anHttpClientcall toProductService, OTel will automatically inject the trace context headers (traceparent,tracestate) into that outgoing request. - When
ProductServicereceives the request, it will see the trace context headers and continue the same trace, creating a new child span.
Step 3: The Service Code#
Let’s look at the code for the two services.
OrderService’s Program.cs Endpoints:
| |
ProductService’s Program.cs Endpoints:
| |
Step 4: Run and Observe#
When you run both services and make a request to http://localhost:5000/order/1, you’ll see output in the console of both applications from the ConsoleExporter.
OrderService Console Output:
| |
This is the root span.
ProductService Console Output:
| |
This is the child span. A backend like Jaeger or Zipkin would visualize this as a waterfall diagram, showing that the OrderService span took 200ms, and 150ms of that was spent waiting for the ProductService span to complete.
Adding Custom Spans#
Automatic instrumentation is great, but sometimes you want to trace a specific piece of work inside a method. You can create custom spans for this.
First, you need to define an ActivitySource. A common pattern is to hold this in a static field or inject it.
| |
Crucial Step: For these custom spans to appear in your trace, you must tell the OpenTelemetry builder to listen to this specific ActivitySource. I’ve updated the configuration code above to include .AddSource(serviceName). Without that line, StartActivity returns null!
Conclusion#
Distributed tracing is no longer a “nice-to-have”—it’s a necessity for understanding and debugging modern, distributed systems. OpenTelemetry provides a powerful, standardized, and easy-to-use framework for adding this capability to your .NET applications. With just a few lines of configuration, you can gain deep insights into the performance of your entire system, making it faster and more reliable.
Further Reading#
- OpenTelemetry .NET Documentation - Official OpenTelemetry documentation
- .NET Observability with OpenTelemetry - Microsoft’s guide
- Jaeger Documentation - Learn about the Jaeger tracing backend
- OpenTelemetry on GitHub - The .NET SDK repository
