CubeAPM
CubeAPM CubeAPM

How to Set Up OpenTelemetry in .NET Applications 

How to Set Up OpenTelemetry in .NET Applications 

Table of Contents

Setting up OpenTelemetry in a .NET application can be done in two ways: manually configuring the SDK via NuGet packages (the recommended approach for most teams because it gives full control over signals, sampling, and export), or using zero-code automatic instrumentation via the OpenTelemetry.AutoInstrumentation package, which attaches at runtime without code changes. 

This guide covers both paths, manual spans, custom metrics, log correlation, and SQL client instrumentation.

Key Takeaways

  • Latest stable versions as of April 2026: OpenTelemetry SDK 1.15.3, OpenTelemetry.Instrumentation.AspNetCore 1.15.2, OpenTelemetry.Instrumentation.Http 1.15.1, OpenTelemetry.Instrumentation.SqlClient 1.15.2 (now stable, no longer prerelease), OpenTelemetry.AutoInstrumentation 1.15.0
  • .NET 10 (LTS, released November 2025, supported until November 2028) is the recommended target. Both .NET 8 (LTS) and .NET 9 (STS) reach end of support on November 10, 2026. Minimum supported by OTel .NET is .NET Framework 4.6.2
  • UseOtlpExporter() is the recommended single-call extension that configures OTLP export for all three signals simultaneously. It was introduced in SDK 1.9.0 and is stable in 1.15.x
  • On .NET 9 and above, OpenTelemetry.Instrumentation.Runtime automatically registers a meter for the built-in System.Runtime metrics. The package is still the recommended way to enable runtime metrics on all supported .NET versions
  • The OTel .NET SDK uses System.Diagnostics.Activity as its trace backend. ActivitySource in .NET maps to Tracer in OTel terms. You do not need a separate OTel API package to create spans in .NET
  • Log correlation is built into the .NET SDK via ILogger. When logs are emitted inside an active trace context, TraceId and SpanId are automatically injected into the log record

The .NET OTel Architecture: One Important Distinction

.NET has a built-in distributed tracing API in System.Diagnostics that predates OpenTelemetry. The OTel .NET SDK builds on top of it rather than replacing it:

  • System.Diagnostics.ActivitySource is equivalent to OTel’s Tracer
  • System.Diagnostics.Activity is equivalent to OTel’s Span
  • System.Diagnostics.Metrics.Meter is the .NET metrics API that OTel SDK listens to

You write instrumentation code against the .NET APIs. The OTel SDK collects and exports that data. This is why there is no separate OpenTelemetry.Api package to install for basic instrumentation; the API is in the .NET runtime itself from .NET 5 onward.

Step 1: Install the Required Packages

For an ASP.NET Core application:

dotnet add package OpenTelemetry --version 1.15.3

dotnet add package OpenTelemetry.Extensions.Hosting --version 1.15.3

dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol --version 1.15.3

dotnet add package OpenTelemetry.Instrumentation.AspNetCore --version 1.15.2

dotnet add package OpenTelemetry.Instrumentation.Http --version 1.15.1

dotnet add package OpenTelemetry.Instrumentation.SqlClient --version 1.15.2

dotnet add package OpenTelemetry.Instrumentation.Runtime --version 1.15.1

Or in your .csproj file:

<ItemGroup>

  <PackageReference Include="OpenTelemetry" Version="1.15.3" />

  <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />

  <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />

  <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />

  <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />

  <PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.15.2" />

  <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />

</ItemGroup>

Step 2A: Configure the SDK for ASP.NET Core (Recommended)

Add OpenTelemetry in Program.cs using the IHostApplicationBuilder extensions:

using OpenTelemetry.Logs;

using OpenTelemetry.Metrics;

using OpenTelemetry.Resources;

using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

// Define service resource attributes

var resourceBuilder = ResourceBuilder.CreateDefault()

    .AddService(

        serviceName: "order-service",

        serviceVersion: "1.0.0");

// Configure OpenTelemetry for all three signals

builder.Services.AddOpenTelemetry()

    .WithTracing(tracing => tracing

        .SetResourceBuilder(resourceBuilder)

        .AddAspNetCoreInstrumentation()

        .AddHttpClientInstrumentation()

        .AddSqlClientInstrumentation())

    .WithMetrics(metrics => metrics

        .SetResourceBuilder(resourceBuilder)

        .AddAspNetCoreInstrumentation()

        .AddHttpClientInstrumentation()

        .AddRuntimeInstrumentation())

    .WithLogging(logging => logging

        .SetResourceBuilder(resourceBuilder))

    .UseOtlpExporter();  // Single call configures OTLP export for all signals

var app = builder.Build();

app.MapControllers();

app.Run();

UseOtlpExporter() reads the OTEL_EXPORTER_OTLP_ENDPOINT environment variable to determine where to send data. Configure it via environment variables:

OTEL_SERVICE_NAME=order-service

OTEL_SERVICE_VERSION=1.0.0

OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318

OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

Note on AddRuntimeInstrumentation(): on .NET 9 and above, this package automatically detects the runtime version and registers a meter for the built-in System.Runtime metrics rather than its own custom metrics. The call is valid and recommended on all supported .NET versions; no version-specific branching is needed in your code.

Step 2B: Configure the SDK for a Console or Worker Application

For non-ASP.NET applications, use TracerProviderBuilder directly:

using OpenTelemetry;

using OpenTelemetry.Resources;

using OpenTelemetry.Trace;

using OpenTelemetry.Metrics;

var resourceBuilder = ResourceBuilder.CreateDefault()

    .AddService(serviceName: "order-processor", serviceVersion: "1.0.0");

using var tracerProvider = Sdk.CreateTracerProviderBuilder()

    .SetResourceBuilder(resourceBuilder)

    .AddSource("order-processor")

    .AddOtlpExporter(options =>

    {

        options.Endpoint = new Uri("http://otel-collector:4318/v1/traces");

    })

    .Build();

using var meterProvider = Sdk.CreateMeterProviderBuilder()

    .SetResourceBuilder(resourceBuilder)

    .AddMeter("order-processor")

    .AddOtlpExporter(options =>

    {

        options.Endpoint = new Uri("http://otel-collector:4318/v1/metrics");

    })

    .Build();

// Application logic here

The using keyword ensures TracerProvider and MeterProvider are disposed cleanly on shutdown, which flushes any buffered spans and metrics before the process exits. Forgetting to dispose the providers causes data loss on shutdown.

Step 3: Create Manual Spans and Add Attributes

Use System.Diagnostics.ActivitySource to create manual spans in your business logic:

using System.Diagnostics;

public class OrderService

{

    // Define ActivitySource once per class or assembly

    private static readonly ActivitySource ActivitySource =

        new ActivitySource("order-service", "1.0.0");

    public async Task<Order> ProcessOrderAsync(OrderRequest request)

    {

        // Start a span — automatically becomes a child of the active HTTP request span

        using var activity = ActivitySource.StartActivity("order.process");

        activity?.SetTag("order.customer_id", request.CustomerId);

        activity?.SetTag("order.item_count", request.Items.Count);

        try

        {

            var order = await ValidateAndCreateOrder(request);

            activity?.SetTag("order.id", order.Id);

            activity?.SetStatus(ActivityStatusCode.Ok);

            return order;

        }

        catch (Exception ex)

        {

            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);

            activity?.RecordException(ex);

            throw;

        }

    }

}

The ActivitySource name must be registered with AddSource(“order-service”) in the TracerProviderBuilder or the spans will not be collected. The ? null-conditional operators handle the case where no ActivityListener is registered, for example during unit tests.

Register the source in Program.cs:

.WithTracing(tracing => tracing

    .AddSource("order-service")

    .AddAspNetCoreInstrumentation()

    ...

Step 4: Create Custom Metrics

Use System.Diagnostics.Metrics for custom metrics. Register the meter name with AddMeter to tell the OTel SDK to collect from it:

using System.Diagnostics.Metrics;

public class OrderMetrics

{

    private static readonly Meter Meter = new Meter("order-service", "1.0.0");

    private static readonly Counter<long> OrdersProcessed =

        Meter.CreateCounter<long>(

            "orders.processed",

            unit: "{order}",

            description: "Total number of orders processed");

    private static readonly Histogram<double> OrderValue =

        Meter.CreateHistogram<double>(

            "orders.value",

            unit: "USD",

            description: "Distribution of order values");

    public void RecordOrder(string status, string currency, double value)

    {

        OrdersProcessed.Add(1,

            new KeyValuePair<string, object?>("order.status", status),

            new KeyValuePair<string, object?>("order.currency", currency));

        OrderValue.Record(value,

            new KeyValuePair<string, object?>("order.currency", currency));

    }

}

Register the meter in Program.cs:

.WithMetrics(metrics => metrics

    .AddMeter("order-service")

    .AddAspNetCoreInstrumentation()

    ...

Step 5: Log Correlation with Traces

In .NET, ILogger log correlation with traces is built into the OTel SDK. When you call WithLogging() in AddOpenTelemetry(), every ILogger log record emitted during an active trace automatically carries the TraceId and SpanId of the active activity:

public class OrderController : ControllerBase

{

    private readonly ILogger<OrderController> _logger;

    public OrderController(ILogger<OrderController> logger)

    {

        _logger = logger;

    }

    [HttpPost("/orders")]

    public async Task<IActionResult> CreateOrder([FromBody] OrderRequest request)

    {

        // TraceId and SpanId are injected automatically into this log record

        _logger.LogInformation(

            "Processing order for customer {CustomerId}",

            request.CustomerId);

        var order = await _orderService.ProcessOrderAsync(request);

        _logger.LogInformation(

            "Order {OrderId} created successfully",

            order.Id);

        return Ok(order);

    }

}

No additional configuration is needed for log-trace correlation when using ILogger with the OTel SDK’s WithLogging() call.

Zero-Code Auto-Instrumentation

For applications you cannot modify, use OpenTelemetry.AutoInstrumentation (v1.15.0). Add the NuGet package to your project:

<PackageReference Include="OpenTelemetry.AutoInstrumentation" Version="1.15.0" />

Configure via environment variables and run normally:

OTEL_SERVICE_NAME=order-service

OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318

OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED=true

OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED=true

OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED=true

dotnet your-app.dll

For Linux or macOS via install script (when you cannot add the NuGet package):

curl -sSfL https://raw.githubusercontent.com/open-telemetry/opentelemetry-dotnet-instrumentation/main/otel-dotnet-auto-install.sh | bash

. $HOME/.otel-dotnet-auto/instrument.sh

OTEL_SERVICE_NAME=order-service \

OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \

dotnet your-app.dll

For Kubernetes, the OpenTelemetry Operator can inject auto-instrumentation via a namespace annotation without any package or script change:

instrumentation.opentelemetry.io/inject-dotnet: "true"

Configure the OTel Collector

receivers:

  otlp:

    protocols:

      grpc:

        endpoint: 0.0.0.0:4317

      http:

        endpoint: 0.0.0.0:4318

processors:

  batch:

    timeout: 5s

    send_batch_size: 512

exporters:

  otlp:

    endpoint: your-backend:4317

service:

  pipelines:

    traces:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

    metrics:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

    logs:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

Common Setup Problems

ProblemLikely causeFix
No traces appearing in the backendActivitySource name not registered with AddSource()Add .AddSource(“your-source-name”) in WithTracing(). The name must match exactly what you pass to new ActivitySource(“your-source-name”)
Spans created but not exportedTracerProvider or MeterProvider disposed too early, or UseOtlpExporter() not calledFor hosted apps, use AddOpenTelemetry() with UseOtlpExporter(). For console apps, keep the provider alive for the full application lifetime
SQL queries have no spansAddSqlClientInstrumentation() not called, or using System.Data.SqlClient instead of Microsoft.Data.SqlClientEnsure OpenTelemetry.Instrumentation.SqlClient 1.15.2 is installed. Use Microsoft.Data.SqlClient (not System.Data.SqlClient) for full query text capture
Runtime metrics missingAddRuntimeInstrumentation() not calledAdd OpenTelemetry.Instrumentation.Runtime 1.15.1 and call .AddRuntimeInstrumentation(). The package works on all supported .NET versions including .NET 9 and above
Log records not correlated with tracesWithLogging() not called in AddOpenTelemetry(), or using a custom logger not backed by ILoggerAdd WithLogging() to AddOpenTelemetry(). Correlation only works with Microsoft.Extensions.Logging.ILogger
UseOtlpExporter() not foundMissing OpenTelemetry.Exporter.OpenTelemetryProtocol package or wrong using directiveInstall the package at version 1.15.3 and add using OpenTelemetry; at the top of Program.cs
Custom metrics not appearingMeter name not registered with the SDKAdd .AddMeter(“your-meter-name”) in WithMetrics(). The name must match exactly what you pass to new Meter(“your-meter-name”)

How CubeAPM Helps in .NET Monitoring with OpenTelemetry

Once OpenTelemetry is flowing from your .NET application, the value depends on what your backend does with the trace data. A slow span on POST /orders is visible in any trace viewer. Understanding whether the slowdown came from a SQL query, an outbound HttpClient call, or a downstream microservice requires a backend that correlates spans with infrastructure metrics and logs from the same request.

CubeAPM accepts OTLP directly from your existing .NET OTel setup with no additional configuration. It correlates distributed request traces from ASP.NET Core with SQL client spans, outbound HTTP call durations, and infrastructure metrics using the shared trace context. When a slow trace appears, CubeAPM links it to the specific database query or downstream service call responsible. It runs self-hosted inside your own infrastructure at $0.15/GB ingestion with no per-user fees.

Summary

Setting up OpenTelemetry in a .NET application requires installing the SDK packages, calling AddOpenTelemetry() with WithTracing(), WithMetrics(), and WithLogging() in Program.cs, and pointing UseOtlpExporter() at your Collector. Custom spans use System.Diagnostics.ActivitySource. Custom metrics use System.Diagnostics.Metrics.Meter. Both must be registered by name with the SDK. Log correlation with traces is automatic when using ILogger with WithLogging() configured.

StepWhat to doKey detail
Install packagesCore SDK 1.15.3, Extensions.Hosting 1.15.3, OTLP exporter 1.15.3, AspNetCore 1.15.2, Http 1.15.1, SqlClient 1.15.2, Runtime 1.15.1Target .NET 10 LTS. Minimum .NET Framework 4.6.2
Configure SDKAddOpenTelemetry().WithTracing().WithMetrics().WithLogging().UseOtlpExporter()UseOtlpExporter() configures all signals in one call via OTEL_EXPORTER_OTLP_ENDPOINT
Register sources.AddSource(“your-source-name”) in WithTracing()Must match the ActivitySource name exactly
Create manual spansnew ActivitySource(“name”) then ActivitySource.StartActivity(“operation”)Becomes a child span automatically when called inside an active HTTP request
Create custom metricsnew Meter(“name”) then Meter.CreateCounter() or Meter.CreateHistogram()Register with .AddMeter(“name”) in WithMetrics()
SQL instrumentation.AddSqlClientInstrumentation()Use Microsoft.Data.SqlClient. SqlClient 1.15.2 is stable
Runtime metrics.AddRuntimeInstrumentation()Works on all .NET versions. On .NET 9+ the package uses built-in System.Runtime metrics automatically
Log correlationWithLogging() in AddOpenTelemetry()TraceId and SpanId injected automatically into ILogger records during active traces

Disclaimer: Package versions verified directly from NuGet.org: OpenTelemetry SDK 1.15.3 (April 21, 2026), OpenTelemetry.Instrumentation.AspNetCore 1.15.2 (April 21, 2026), OpenTelemetry.Instrumentation.Http 1.15.1 (April 21, 2026), OpenTelemetry.Instrumentation.SqlClient 1.15.2 (April 21, 2026), OpenTelemetry.Instrumentation.Runtime 1.15.1 (April 21, 2026), OpenTelemetry.AutoInstrumentation 1.15.0 (April 23, 2026). .NET version support dates verified from dotnet.microsoft.com/platform/support/policy and github.com/dotnet/core/releases.md. OTel .NET best practices docs last modified May 19, 2026. All verified as of May 2026.

Also read:

How to Instrument Django Applications with OpenTelemetry 

How to Add OpenTelemetry Tracing to a Spring Boot Application 

What is the Difference Between Synthetic monitoring and Real User Monitoring (RUM)?

×
×