
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Debugging distributed .NET applications without unified telemetry is a guessing game that wastes engineering hours and delays incident resolution. Observability for .NET with OpenTelemetry solves this by standardizing how your ASP.NET Core services emit traces, metrics, and logs through a single vendor-neutral SDK. Instead of juggling proprietary agents, you gain portable, high-fidelity signal generation that works across AWS, Azure, and on-premise environments.
OpenTelemetry.Extensions.Hosting NuGet package, configuring tracing and metrics via the builder pattern in Program.cs, and exporting data to an OTLP-compatible backend like Jaeger or Prometheus for unified system visibility.How do you configure observability for .NET with OpenTelemetry?
Setting up instrumentation in your application correctly requires understanding the distinction between auto-instrumentation and manual SDK configuration. In 2026, the .NET ecosystem has matured significantly; the OpenTelemetry .NET SDK is now stable for tracing and metrics, and it integrates natively with the built-in System.Diagnostics APIs. You no longer need heavy third-party agents for basic coverage.
The foundation of any production setup is the hosting extension. This wires the SDK into the dependency injection container, ensuring lifecycle management aligns with your web host. Install the core packages first:
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.SqlClient In your Program.cs, register the services using the builder pattern. This approach is idiomatic for modern .NET and keeps configuration declarative:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation(opts =>
opts.SetDbStatementForText = true)
.AddOtlpExporter(opts =>
opts.Endpoint = new Uri("http://localhost:4317")))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter(opts =>
opts.Endpoint = new Uri("http://localhost:4317"))); A common mistake I see in audits is neglecting the AddOtlpExporter endpoint configuration. Without it, signals are generated but dropped silently. Always verify connectivity to your collector or backend during initial setup. For teams managing structured logging, remember that OTel also supports log bridging, allowing you to correlate log entries directly with trace IDs.
What is the difference between auto-instrumentation and manual tracing?
Understanding when to rely on libraries versus writing custom spans is critical for maintaining clean code while achieving deep visibility. Auto-instrumentation handles the "plumbing": HTTP requests, database calls, gRPC, and message queue publishing. It uses .NET's ActivitySource listeners to capture these operations without modifying your business logic.
Manual tracing fills the gaps inside your business logic. If a specific calculation, validation step, or internal workflow is slow, auto-instrumentation won't see it because it happens within a single method call. You must create spans explicitly to measure these internal durations.
To implement manual tracing, inject an ActivitySource into your service. Never create static ActivitySources in library code unless you intend them to be public API surface; dependency injection makes testing and configuration easier.
public class OrderService
{
private readonly ActivitySource _activitySource;
public OrderService(ActivitySource activitySource)
{
_activitySource = activitySource;
}
public async Task<Order> ProcessOrderAsync(OrderRequest request)
{
using var activity = _activitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", request.Id);
activity?.SetTag("order.value", request.TotalAmount);
// Business logic here...
await ValidateInventoryAsync(request);
return await CreateOrderRecordAsync(request);
}
} Note the null-conditional operator (?.) on the activity. If no listener is registered (e.g., during unit tests or if OTel is disabled), StartActivity returns null. Checking for null prevents unnecessary allocations and exceptions. This pattern is non-negotiable for high-throughput services where every allocation counts.
How do you export .NET telemetry to Jaeger, Prometheus, or Grafana?
The OpenTelemetry Protocol (OTLP) is the universal language for telemetry transport in 2026. While legacy exporters exist for specific backends, using OTLP ensures you can swap backends without changing application code. Most modern stacks, including Prometheus and Grafana setups, now support OTLP ingestion natively or via a collector.
| Backend | Signal Type | Recommended Exporter | Configuration Note |
|---|---|---|---|
| Jaeger / Tempo | Traces | AddOtlpExporter | Use gRPC endpoint (port 4317) for best performance |
| Prometheus | Metrics | AddPrometheusExporter | Exposes /metrics endpoint for scraping |
| Grafana Cloud | All | AddOtlpExporter | Requires Authorization header with API key |
| Console | Debug | AddConsoleExporter | Development only; high overhead in production |
For Prometheus specifically, the push model differs from OTLP's pull model. You typically expose an HTTP endpoint that Prometheus scrapes at intervals. Configure this alongside your OTLP trace exporter:
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddPrometheusExporter());
// In middleware pipeline
app.MapPrometheusScrapingEndpoint(); When deploying to Kubernetes, consider running the OpenTelemetry Collector as a DaemonSet or sidecar. This offloads batching, retry logic, and credential management from your .NET application. Your app sends data to localhost:4317, and the collector handles the secure upstream transmission. This decoupling is essential for resilient production architectures.
What are common performance pitfalls in .NET OpenTelemetry?
Observability should never become the bottleneck. I have debugged more than one incident where the monitoring itself caused the outage. Adhering to monitoring golden signals includes watching the overhead of your instrumentation.
- High Cardinality Tags: Never tag spans with unbounded values like user IDs, session tokens, or full URLs. These explode memory usage in both your app and the backend. Use low-cardinality attributes (status codes, region, operation type) and store high-cardinality data in span events or logs instead.
- Synchronous Exporters: Always use batch processors. The default
BatchActivityExportProcessorbuffers spans and exports them asynchronously. Synchronous exporters block the request thread on every network call, adding latency directly to your p99 response times. - Excessive Sampling: In high-traffic systems, capturing 100% of traces is often wasteful. Implement head-based sampling to capture a representative percentage, or use tail-based sampling in the collector to retain only error traces and slow requests. This reduces cost and noise simultaneously.
- Missing Disposal: Failing to dispose of
Activityobjects breaks the parent-child relationship chain. Always useusingstatements or explicitStop()calls. Leaked activities accumulate in memory and corrupt trace topology.
Monitor the overhead of OpenTelemetry itself. The SDK exposes internal metrics about queue depth, dropped spans, and export latency. Add AddOpenTelemetrySdkSelfDiagnostics() to your metrics configuration to track these. If the export queue consistently fills up, your backend is too slow or your network is saturated. Adjust batch sizes or increase sampling rates accordingly.
Implementing Production-Grade Observability for .NET with OpenTelemetry
Adopting observability for .NET with OpenTelemetry is an iterative process, not a one-time setup. Start with auto-instrumentation to establish baseline visibility, then layer in manual spans for critical business paths. Standardize on OTLP to maintain backend flexibility, and enforce strict cardinality limits to protect system performance. Treat your telemetry configuration with the same rigor as your application code: version it, review it, and test it.
If your team needs help designing a compliant, audit-ready observability stack or optimizing existing .NET telemetry pipelines, reach out to discuss your architecture. Getting the foundation right early prevents costly rework and ensures your monitoring actually serves your reliability goals.