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.ActivitySourceis equivalent to OTel’s TracerSystem.Diagnostics.Activityis equivalent to OTel’s SpanSystem.Diagnostics.Metrics.Meteris 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.1Or 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/protobufNote 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 hereThe 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.dllFor 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.dllFor 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
| Problem | Likely cause | Fix |
| No traces appearing in the backend | ActivitySource 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 exported | TracerProvider or MeterProvider disposed too early, or UseOtlpExporter() not called | For hosted apps, use AddOpenTelemetry() with UseOtlpExporter(). For console apps, keep the provider alive for the full application lifetime |
| SQL queries have no spans | AddSqlClientInstrumentation() not called, or using System.Data.SqlClient instead of Microsoft.Data.SqlClient | Ensure OpenTelemetry.Instrumentation.SqlClient 1.15.2 is installed. Use Microsoft.Data.SqlClient (not System.Data.SqlClient) for full query text capture |
| Runtime metrics missing | AddRuntimeInstrumentation() not called | Add 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 traces | WithLogging() not called in AddOpenTelemetry(), or using a custom logger not backed by ILogger | Add WithLogging() to AddOpenTelemetry(). Correlation only works with Microsoft.Extensions.Logging.ILogger |
| UseOtlpExporter() not found | Missing OpenTelemetry.Exporter.OpenTelemetryProtocol package or wrong using directive | Install the package at version 1.15.3 and add using OpenTelemetry; at the top of Program.cs |
| Custom metrics not appearing | Meter name not registered with the SDK | Add .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.
| Step | What to do | Key detail |
| Install packages | Core 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.1 | Target .NET 10 LTS. Minimum .NET Framework 4.6.2 |
| Configure SDK | AddOpenTelemetry().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 spans | new ActivitySource(“name”) then ActivitySource.StartActivity(“operation”) | Becomes a child span automatically when called inside an active HTTP request |
| Create custom metrics | new 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 correlation | WithLogging() 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)?





