# How to Configure OpenTelemetry Instrumentation in ASP.NET Core Applications

> Learn to configure OpenTelemetry instrumentation in ASP.NET Core apps. Install SDKs, register via AddOpenTelemetry, and set up tracing, metrics, and logging.

- Repository: [.NET Platform/skills](https://github.com/dotnet/skills)
- Tags: how-to-guide
- Published: 2026-07-07

---

**To configure OpenTelemetry in ASP.NET Core, install the required NuGet packages, register the SDK in [`Program.cs`](https://github.com/dotnet/skills/blob/main/Program.cs) via `AddOpenTelemetry()`, and configure instrumentation for tracing, metrics, and logging with exporters like OTLP.**

OpenTelemetry provides a unified observability framework for distributed tracing, metrics, and logging in modern .NET applications. According to the `dotnet/skills` repository, the recommended approach involves registering the SDK with the dependency injection container and configuring specific instrumentation providers in your application's startup code.

## Installing Required NuGet Packages

Begin by adding the core SDK and instrumentation packages to your project. The skill definition file specifies the minimal set required at [lines 35‑43 of [`plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md`](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md)](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L35‑L43):

```bash
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol   # OTLP exporter

```

Optional packages for **Entity Framework Core**, **gRPC**, **SQL**, and runtime metrics are listed at [lines 54‑58](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L54‑L58) of the same file.

## Registering the OpenTelemetry SDK in Program.cs

Register the SDK in your [`Program.cs`](https://github.com/dotnet/skills/blob/main/Program.cs) file using the `AddOpenTelemetry()` extension method. The full implementation pattern is demonstrated at [lines 61‑111](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L61‑L111):

```csharp
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
using OpenTelemetry.Logs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService(builder.Environment.ApplicationName))
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation(o => 
            o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/healthz"))
        .AddHttpClientInstrumentation(o => o.RecordException = true)
        .AddSource("MyApp.Orders")
        .AddSource("MyApp.Payments")
        .AddSource("MyApp.Messaging"))
    .WithMetrics(m => m
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddMeter("MyApp.Metrics"))
    .WithLogging(l => { l.IncludeScopes = true; })
    .UseOtlpExporter();

```

The `UseOtlpExporter()` method reads the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable, defaulting to `http://localhost:4317` as noted at [lines 107‑110](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L107‑L110).

## Configuring Traces, Metrics, and Logs

### Distributed Tracing Configuration

The `.WithTracing()` builder configures **automatic instrumentation** for ASP.NET Core and HTTP clients. The example above filters out health check endpoints (`/healthz`) to reduce noise. You must explicitly register any custom `ActivitySource` names using `.AddSource()` so the SDK captures those spans.

### Metrics and Log Correlation

The `.WithMetrics()` builder exposes runtime and custom metrics, while `.WithLogging()` automatically injects `TraceId` and `SpanId` into every `ILogger` entry, establishing automatic **log-trace correlation** without additional resource configuration ([lines 113‑119](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L113‑L119)).

## Creating Custom Spans and Metrics

### Custom Activity Sources

To emit custom spans, create a static `ActivitySource` and match its name to an `AddSource()` registration. The sample service at [lines 124‑178](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L124‑L178) demonstrates this pattern:

```csharp
using System.Diagnostics;
using Microsoft.Extensions.Logging;

public class OrderService
{
    private static readonly ActivitySource ActivitySource = new("MyApp.Orders");
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger) => _logger = logger;

    public async Task<Order> ProcessOrderAsync(CreateOrderRequest request)
    {
        using var activity = ActivitySource.StartActivity("ProcessOrder");
        activity?.SetTag("order.customer_id", request.CustomerId);
        
        // Business logic here
        
        return new Order { /* ... */ };
    }
}

```

### Custom Metrics with IMeterFactory

For custom metrics, inject `IMeterFactory` and ensure the meter name matches an `AddMeter()` registration. The `OrderMetrics` class at [lines 185‑228](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L185‑L228) shows counters, histograms, and up-down counters:

```csharp
using System.Diagnostics.Metrics;

public class OrderMetrics
{
    private readonly Counter<long> _ordersProcessed;
    private readonly Histogram<double> _orderProcessingDuration;
    private readonly UpDownCounter<int> _activeOrders;

    public OrderMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("MyApp.Metrics");
        _ordersProcessed = meter.CreateCounter<long>("orders.processed", "orders");
        _orderProcessingDuration = meter.CreateHistogram<double>("orders.processing_duration", "ms");
        _activeOrders = meter.CreateUpDownCounter<int>("orders.active", "orders");
    }

    public void RecordOrderProcessed(string region, double durationMs)
    {
        var tags = new TagList { { "region", region }, { "order.type", "standard" } };
        _ordersProcessed.Add(1, tags);
        _orderProcessingDuration.Record(durationMs, tags);
    }
}

```

## Propagating Context Across Non-HTTP Boundaries

For message queues or custom protocols, manually propagate trace context using `Propagators.DefaultTextMapPropagator`. The implementation at [lines 381‑668](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L381‑L668) shows injection and extraction:

```csharp
var propagator = Propagators.DefaultTextMapPropagator;
var carrier = new Dictionary<string, string>();

// SEND side - inject current context
propagator.Inject(
    new PropagationContext(Activity.Current?.Context ?? default, Baggage.Current),
    carrier,
    (dict, key, value) => dict[key] = value);

// RECEIVE side - extract parent context
var parentContext = propagator.Extract(default, carrier,
    (dict, key) => dict.TryGetValue(key, out var v) ? new[] { v } : Array.Empty<string>());
Baggage.Current = parentContext.Baggage;

using var activity = ActivitySource.StartActivity("ProcessMessage",
    ActivityKind.Consumer,
    parentContext.ActivityContext);

```

## Avoiding Common Configuration Errors

The skill documentation at [lines 781‑889](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/configuring-opentelemetry-dotnet/SKILL.md#L781‑L889) identifies frequent pitfalls:

- **Mismatched ActivitySource names**: Ensure the string passed to `AddSource()` exactly matches the `ActivitySource` constructor name.
- **Exporter endpoint mismatches**: Verify `OTEL_EXPORTER_OTLP_ENDPOINT` points to the correct collector port (4317 for gRPC, 4318 for HTTP).
- **High-cardinality tags**: Avoid setting unique IDs (like user IDs or request GUIDs) as tag keys, which can overwhelm the telemetry backend.

## Summary

- Install the core packages `OpenTelemetry.Extensions.Hosting`, `OpenTelemetry.Instrumentation.AspNetCore`, and `OpenTelemetry.Exporter.OpenTelemetryProtocol`.
- Register the SDK in [`Program.cs`](https://github.com/dotnet/skills/blob/main/Program.cs) using `builder.Services.AddOpenTelemetry()` and configure resources, tracing, metrics, and logging.
- Use `AddSource()` and `AddMeter()` to register custom telemetry sources, ensuring names match exactly.
- Configure `UseOtlpExporter()` to export telemetry via the OpenTelemetry Protocol.
- Apply manual context propagation for messaging systems and asynchronous processing.

## Frequently Asked Questions

### How do I filter health check endpoints from OpenTelemetry tracing?

Use the `Filter` option in `AddAspNetCoreInstrumentation()` to exclude specific request paths. The recommended pattern excludes `/healthz` to prevent noise from health probes: `o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/healthz")`.

### What environment variable configures the OTLP exporter endpoint?

The `UseOtlpExporter()` method reads the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable, defaulting to `http://localhost:4317` for gRPC communication. For HTTP transport, append `/v1/traces` or `/v1/metrics` to the endpoint URL and set the protocol accordingly.

### Why aren't my custom spans appearing in the trace viewer?

Your `ActivitySource` name must exactly match a string passed to `AddSource()` during SDK configuration. Additionally, ensure you are creating the `ActivitySource` as a static instance and that you call `StartActivity()` within a `using` statement to ensure proper disposal and export.

### How do I correlate logs with traces automatically?

Call `.WithLogging(l => { l.IncludeScopes = true; })` during SDK configuration. This automatically injects `TraceId` and `SpanId` into `ILogger` output without requiring manual scope creation or separate resource builders.