Logs tell you something went wrong. Metrics tell you how often. Neither tells you which of the six services a slow checkout request actually spent 4 seconds in. That's what distributed tracing is for — and OpenTelemetry has become the vendor-neutral way to get it in .NET, whether you ship traces to Application Insights, Jaeger, Grafana Tempo, or AWS X-Ray.

Key takeaways
  • A trace is a tree of spans connected by a shared trace ID that follows a request across every service it touches.
  • ASP.NET Core, HttpClient, and EF Core all support automatic instrumentation — you get useful traces before writing a single custom span.
  • Add custom spans with ActivitySource for the business operations that matter, not every method call.
  • Sample aggressively in high-traffic services. 100% tracing sounds appealing until it's the largest line item on your observability bill.

Wiring up the SDK

The OpenTelemetry .NET SDK plugs into the same IServiceCollection pipeline as everything else. Automatic instrumentation packages cover ASP.NET Core, HttpClient, and most common databases.

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("orders-api", serviceVersion: "1.4.0"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddSource("Orders.Api")
        .AddOtlpExporter(o => o.Endpoint = new Uri("http://otel-collector:4317")))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddOtlpExporter());

var app = builder.Build();

That single registration gives you a span for every inbound HTTP request, every outbound HttpClient call, and every EF Core command — automatically nested, automatically correlated by trace ID.

Custom spans for the operations that matter

Automatic instrumentation covers infrastructure. For business operations — "reserve inventory," "authorize payment" — add explicit spans via ActivitySource so they show up as first-class steps in the trace, not just an unlabeled gap between two HTTP calls.

csharp
public class InventoryReservationService(IInventoryRepository repo)
{
    private static readonly ActivitySource Source = new("Orders.Api");

    public async Task ReserveAsync(Guid orderId, IReadOnlyList items, CancellationToken ct)
    {
        using var activity = Source.StartActivity("inventory.reserve", ActivityKind.Internal);
        activity?.SetTag("order.id", orderId);
        activity?.SetTag("order.item_count", items.Count);

        var result = await repo.ReserveAsync(orderId, items, ct);

        activity?.SetTag("reservation.success", result.Success);
        if (!result.Success)
        {
            activity?.SetStatus(ActivityStatusCode.Error, result.FailureReason);
        }

        return result;
    }
}

Correlating traces with logs

A trace tells you where time went. A log line tells you why a step failed. OpenTelemetry's Activity.Current exposes the current trace ID, so a Serilog enricher can attach it to every log line written during that request — then you can jump from a slow span straight to the exact log entries for it.

csharp
builder.Host.UseSerilog((ctx, services, cfg) => cfg
    .ReadFrom.Configuration(ctx.Configuration)
    .Enrich.WithSpan()
    .Enrich.FromLogContext());
RelatedWe covered structured logging with Serilog separately — the two are meant to work together. Traces answer “where,” logs answer “why,” and a shared trace ID is what lets you move between them without guessing.

Sampling: don't trace everything

At meaningful traffic volumes, 100% sampling is expensive to store and slow to query. Head-based sampling makes the keep/drop decision at the start of the trace, based on a ratio or a custom rule:

csharp
.WithTracing(tracing => tracing
    .SetSampler(new ParentBasedSampler(new TraceIdRatioBasedSampler(0.10)))
    .AddAspNetCoreInstrumentation())

Ten percent of traffic traced end-to-end is usually enough to see systemic latency patterns without paying to store every healthy request. Most teams also always sample requests that error, regardless of the ratio, since those are the ones worth keeping.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.