OpenTelemetry is the standard for generating and collecting telemetry data, and it’s becoming the go-to for adding distributed tracing to your C# applications.

Here’s a look at a typical C# application using OpenTelemetry for tracing:

using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using System.Diagnostics;

public class Program
{
    private static readonly ActivitySource _activitySource = new ActivitySource("MyCompany.MyProduct");

    public static void Main(string[] args)
    {
        using var tracerProvider = Sdk.CreateTracerProviderBuilder()
            .AddSource(_activitySource.Name)
            .AddAspNetCoreInstrumentation() // For ASP.NET Core apps
            .AddHttpClientInstrumentation()   // For outgoing HTTP calls
            .AddOtlpExporter()                // Export to OTLP collector
            .Build();

        // ... your application logic ...

        var activity = _activitySource.StartActivity("PerformSomeWork");
        try
        {
            Console.WriteLine("Doing some work...");
            Thread.Sleep(100); // Simulate work
            MakeExternalCall();
        }
        finally
        {
            activity?.Stop();
        }

        Console.WriteLine("Work done.");
    }

    public static void MakeExternalCall()
    {
        using var activity = _activitySource.StartActivity("MakeExternalCall");
        using var httpClient = new HttpClient();
        try
        {
            // Simulate an outgoing HTTP request
            httpClient.GetAsync("https://www.example.com").Wait();
        }
        catch (Exception ex)
        {
            activity?.RecordException(ex);
            throw;
        }
    }
}

This setup allows you to visualize the flow of requests across different services, identify bottlenecks, and debug issues that span multiple components. The core idea is to instrument your code to create "spans" that represent units of work, and these spans are linked together to form a trace.

The ActivitySource is your entry point for creating activities (spans). When you start an activity with _activitySource.StartActivity("OperationName"), you’re creating a new span. The using statement ensures that activity?.Stop() is called, marking the end of the operation and recording its duration.

Instrumentation packages, like AddAspNetCoreInstrumentation() and AddHttpClientInstrumentation(), automatically create spans for common frameworks and libraries. This means you don’t have to manually wrap every HTTP request or ASP.NET Core endpoint. The AddOtlpExporter() configures OpenTelemetry to send your trace data to an OpenTelemetry Protocol (OTLP) collector, which then forwards it to your chosen backend (like Jaeger, Zipkin, or a cloud-native observability platform).

The real power comes when you have multiple services. Imagine ServiceA calls ServiceB, and ServiceB calls ServiceC. Without distributed tracing, understanding the latency of the entire request chain is difficult. With OpenTelemetry, each service generates its own spans. The traceId is propagated between services, allowing your tracing backend to stitch these individual spans together into a single, coherent trace. This traceId propagation is often handled automatically by the instrumentation packages when they detect incoming requests (e.g., via HTTP headers).

A key concept to grasp is the relationship between Activity, Span, Trace, and TraceId. An Activity in OpenTelemetry.NET is the concrete implementation of a Span. A Span represents a single operation within a trace (e.g., an HTTP request, a database query, a function call). A Trace is a collection of spans that represent the entire path of a request through your system. The TraceId is a unique identifier that links all spans belonging to the same trace, regardless of which service they originated from. When one service calls another, it must propagate the TraceId (and the SpanId of the parent span) so the downstream service can create a new span that’s correctly linked.

When you’re dealing with asynchronous operations in C#, especially those involving async/await, propagating the Activity context can sometimes require explicit handling. While many modern instrumentation packages handle this automatically for common scenarios like HttpClient and ASP.NET Core, you might encounter situations where an Activity doesn’t get correctly linked if you’re manually managing threads or using less common asynchronous patterns. In such cases, you might need to manually capture and restore the Activity context, often using AsyncLocal<T> or specific helper methods provided by the OpenTelemetry SDK to ensure spans are correctly parented.

The next hurdle you’ll likely face is configuring sampling, which determines how many traces are actually exported.

Want structured learning?

Take the full Csharp course →