Observability for .NET with OpenTelemetry

Khimananda Oli 7 min read Programming and Languages
Observability for .NET with OpenTelemetry

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.

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.

ASP.NET Core AppSystem.DiagnosticsOTel SDK / APIAuto-InstrumentationOTLP CollectorBatch ProcessorAttribute EnrichmentObservability BackendJaeger / TempoPrometheusLoki / ELK
Data flow architecture for observability for .NET with OpenTelemetry showing signal path from application to backend storage

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.

HTTP RequestBusiness ServiceDatabaseAuto: ASP.NET Core Middleware SpanManual: ProcessOrder SpanValidateInventory()CalculateDiscount()Auto: SQL Query
Trace hierarchy showing auto-instrumented infrastructure spans wrapping manual business logic spans

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.

BackendSignal TypeRecommended ExporterConfiguration Note
Jaeger / TempoTracesAddOtlpExporterUse gRPC endpoint (port 4317) for best performance
PrometheusMetricsAddPrometheusExporterExposes /metrics endpoint for scraping
Grafana CloudAllAddOtlpExporterRequires Authorization header with API key
ConsoleDebugAddConsoleExporterDevelopment 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 BatchActivityExportProcessor buffers 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 Activity objects breaks the parent-child relationship chain. Always use using statements or explicit Stop() calls. Leaked activities accumulate in memory and corrupt trace topology.
Safe Practices✓ Low-cardinality tags (region, status)✓ Batch processing with async export✓ Head-based or tail-based sampling✓ Proper Activity disposal (using)✓ OTLP gRPC for efficient transportPerformance Risks✗ High-cardinality tags (userId, GUID)✗ Synchronous blocking exporters✗ 100% sampling on high-traffic paths✗ Leaked Activity objects in memory✗ Verbose console logging in prod
Safe versus risky telemetry patterns affecting .NET application performance and stability

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.

Frequently Asked Questions

OpenTelemetry is a vendor-neutral observability framework for collecting traces, metrics, and logs from .NET applications using standardized APIs and SDKs.

Install the OpenTelemetry.Extensions.Hosting NuGet package and configure tracing, metrics, and logging services in your Program.cs builder during startup.

Yes, it automatically captures HTTP requests, database queries, and gRPC calls without modifying application code when using supported libraries.

Yes, export telemetry directly to Azure Monitor using the Azure.Monitor.OpenTelemetry.Exporter package with your connection string configured.

Common exporters include OTLP, Jaeger, Zipkin, Prometheus, Console, and cloud-specific options like AWS X-Ray and Google Cloud Trace.

OpenTelemetry uses open standards and avoids vendor lock-in, while Application Insights SDK is Microsoft-specific with proprietary data models and ingestion protocols.

Overhead is typically under two percent when sampling is enabled and batch exporting is configured correctly for high-throughput .NET services.

Use enrichment processors or custom span processors to redact headers, query strings, or body content before telemetry leaves your application boundary.

Yes, enable log correlation by configuring the OpenTelemetry logging provider to inject trace and span IDs into structured log entries automatically.

Use parent-based sampling with a configurable ratio to retain full traces for errors while reducing volume for successful high-frequency requests.

Verify instrumentation libraries are registered, check exporter endpoint connectivity, and ensure activity source names match your configured trace providers.

Partial support exists via legacy packages, but full feature parity requires migrating to .NET 6 or later for modern instrumentation capabilities.

Always use TLS encryption for OTLP endpoints and authenticate with API keys or mTLS certificates to prevent unauthorized data interception.

Yes, replace Serilog sinks with the OpenTelemetry logging provider while keeping Serilog as the formatting layer via bridging packages.

The OpenTelemetry .NET SDK v1.12 is the current stable release with full GA support for traces, metrics, and logs in production environments.