BrandGhost
OpenTelemetry in .NET: Complete Observability Guide

OpenTelemetry in .NET: Complete Observability Guide

Production systems fail in ways that unit tests don't catch. A service that passes all its tests can still have a memory leak that takes 48 hours to surface, a database query that performs fine with 1,000 rows but times out at 1 million, or a downstream dependency that degrades intermittently under load. To understand what's happening in a running production system, you need observability -- and OpenTelemetry .NET is the modern, vendor-neutral way to build it.

OpenTelemetry in .NET is an open-source observability framework under the CNCF (Cloud Native Computing Foundation). It gives you a unified API for capturing all three observability signals -- traces, metrics, and logs -- in standardized formats that work with any compatible backend: Jaeger, Grafana, Azure Monitor, Datadog, Honeycomb, Seq, and dozens of others. You instrument your code once and choose your backend through configuration, not code.

This is the complete guide to OpenTelemetry .NET. You'll get a grounding in what OTel is, how .NET's built-in primitives align with it, how to configure all three signals, how to write custom instrumentation for your business logic, and how to set up a production-ready exporter configuration. Whether you're building a new ASP.NET Core API from scratch or adding observability to an existing application, this is the starting point.


What Is OpenTelemetry?

OpenTelemetry emerged from the merger of two earlier frameworks: OpenCensus (from Google) and OpenTracing (a CNCF project). Both had overlapping goals and created a fragmented ecosystem where instrumentation code was tied to specific vendors or backends. OpenTelemetry unified them into a single standard backed by virtually every major observability vendor.

At its core, OTel defines three things:

  • APIs: Language-specific interfaces for recording telemetry. In .NET, these map directly to System.Diagnostics.Activity for traces, System.Diagnostics.Metrics.Meter for metrics, and Microsoft.Extensions.Logging.ILogger for logs -- all of which are already in the .NET BCL.
  • SDKs: Implementations that collect, process, sample, and export telemetry data. The .NET SDK lives in the OpenTelemetry NuGet package family and hooks into the BCL primitives via listener infrastructure.
  • Data formats: Standard wire protocols for transmitting telemetry to backends -- primarily OTLP (OpenTelemetry Protocol), which uses gRPC or HTTP/protobuf.

The vendor-neutral aspect matters in practice. You write your instrumentation code once using the OTel API. You can send the same telemetry to a local Jaeger instance during development, to Grafana in staging, and to Azure Monitor in production -- changing configuration only, not application code. This portability is one of the strongest reasons to adopt OpenTelemetry .NET over vendor-specific SDKs.


Why Observability Matters in .NET Applications

The traditional approach to understanding production issues is log files. You search through text looking for errors, trying to reconstruct what happened from timestamps and message strings. It works, up to a point. It breaks down in a few important scenarios.

Latency investigations. Your API is slow. The logs show requests taking 2 seconds. But where? The database query? A downstream service? Serialization overhead? JSON parsing? Without traces, you're guessing. With traces, you see a timeline showing every operation and its duration -- the slow step is immediately obvious and attributable to the specific code path.

Cross-service correlation. A user reports an error. You find the log entry in your API service. But the actual failure happened in a payment service three hops downstream. Without distributed tracing, connecting those two log entries requires a manually propagated correlation ID -- if you thought to add one in the first place. OpenTelemetry handles this propagation automatically through W3C traceparent headers.

Proactive health monitoring. You want to know that your error rate is below 0.1% and that p99 latency is under 500ms -- and you want an alert if those thresholds are breached. Log-based alerting can do this, but it's expensive (shipping and storing every individual log record) and has ingestion latency. Metrics are aggregated before export, making them far cheaper to store, faster to query, and better suited for dashboards and alerting at scale.

Diagnosing errors in context. When a specific request fails, you want the trace that shows exactly what happened, the metrics that show whether it's an isolated incident or a broad regression, and the structured logs with the full error detail and surrounding context. All three signals together tell the complete story. OpenTelemetry makes it practical to have all three correlated through trace IDs without building that correlation yourself.


The Three Pillars of Observability

OpenTelemetry organizes telemetry into three signal types. Understanding what each is for helps you decide what to instrument and how.

Traces answer: "What happened during this request, and how long did each step take?" A trace is a tree of spans, where each span represents a discrete unit of work -- an HTTP request, a database query, a method call. Spans have start times, durations, status codes (ok/error), key-value tags (attributes), and can carry structured event records. Traces are the tool for latency analysis, request flow visualization, and root cause investigation for specific failures.

Metrics answer: "How is the system performing in aggregate over time?" A metric is a measurement series -- a counter that increments with each request, a histogram that tracks the distribution of request durations, a gauge that reports current memory usage. Metrics are aggregated before export, making them cheap to store and fast to query. They're the foundation for dashboards, SLO tracking, and alerting.

Logs answer: "What exactly happened at this moment?" A log record is a structured event with a timestamp, severity, message, and arbitrary key-value attributes. Logs provide the detail that traces and metrics don't capture -- the exact error message, the specific input that triggered a branch, the SQL query that timed out. When OTel logging is configured alongside tracing, log records are automatically tagged with the current trace ID and span ID, enabling direct navigation from trace to correlated log entries in your backend.

All three signals work together. A trace shows you the shape of a failure. Metrics tell you how widespread it is. Logs give you the detail to fix it.

Signal Question Answered Best For Cost Profile
Traces What happened during this request? Latency analysis, request flow, root cause investigation One record per request
Metrics How is the system performing? Dashboards, SLOs, alerting Aggregated in-process -- very cheap
Logs What exactly happened at this moment? Detailed debugging, audit trails, structured events One record per significant event

How .NET Implements OpenTelemetry

One of the most underappreciated aspects of OpenTelemetry .NET is that the core APIs are already in the .NET runtime. Microsoft built OTel-compatible primitives into the standard library years before OpenTelemetry reached its current form, then aligned them precisely with the OTel specification.

System.Diagnostics.Activity is .NET's trace span implementation. Creating an Activity via an ActivitySource is functionally equivalent to creating an OTel span. The OTel SDK hooks into the ActivityListener infrastructure -- a publish/subscribe mechanism baked into the runtime -- to collect activities from all registered sources and export them.

System.Diagnostics.Metrics.Meter and its instrument types (Counter<T>, Histogram<T>, ObservableGauge<T>) are .NET's metrics primitives. The OTel SDK hooks into the MeterListener infrastructure to collect metric readings and export them through the configured pipeline.

Microsoft.Extensions.Logging.ILogger is .NET's logging abstraction. The OTel SDK provides an ILoggerProvider implementation that receives log records from the standard ILogger pipeline and exports them alongside traces and metrics.

The practical consequence: if your library or framework code already uses Activity, Meter, or ILogger, it's already producing OTel-compatible telemetry. ASP.NET Core, Entity Framework Core, gRPC, and other framework components use these primitives internally. Adding OpenTelemetry to an existing ASP.NET Core application can therefore surface significant instrumentation data with very little code -- the framework's own telemetry is already there, waiting to be collected.


Key NuGet Packages for OpenTelemetry .NET

Getting started with OpenTelemetry in .NET requires a handful of packages:

  • OpenTelemetry.Extensions.Hosting -- the core integration with WebApplicationBuilder and IHostBuilder. Provides the AddOpenTelemetry() extension method.
  • OpenTelemetry.Instrumentation.AspNetCore -- auto-instrumentation for ASP.NET Core request handling: HTTP spans, route templates, response status codes.
  • OpenTelemetry.Instrumentation.Http -- auto-instrumentation for outbound HttpClient requests: span creation and W3C traceparent header injection.
  • OpenTelemetry.Exporter.Console -- exports telemetry to stdout. Invaluable during local development and initial setup.
  • OpenTelemetry.Exporter.OpenTelemetryProtocol -- exports via OTLP to any compatible backend: Jaeger, Grafana, Seq, Honeycomb, and others.
  • OpenTelemetry.Instrumentation.Runtime -- .NET runtime metrics: GC pause times, heap sizes, thread pool queue lengths, exception rates.

For production deployments on Azure, Azure.Monitor.OpenTelemetry.AspNetCore provides a single-call integration that exports all three signals to Application Insights without needing a separate OTel collector. For self-hosted Prometheus infrastructure, OpenTelemetry.Exporter.Prometheus.AspNetCore exposes a /metrics scrape endpoint.


OpenTelemetry .NET Quick Start -- All Three Signals in One Setup

The fastest path to OpenTelemetry in .NET is a few lines in Program.cs. This configuration captures HTTP request traces, basic ASP.NET Core metrics, and structured logs -- all exported to the console so you can see them immediately without additional infrastructure.

// NuGet packages needed:
// OpenTelemetry.Extensions.Hosting
// OpenTelemetry.Instrumentation.AspNetCore
// OpenTelemetry.Instrumentation.Http
// OpenTelemetry.Exporter.Console

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService("MyApp", serviceVersion: "1.0.0"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddConsoleExporter())
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddConsoleExporter());

builder.Logging.AddOpenTelemetry(logging =>
{
    logging.IncludeFormattedMessage = true;
    logging.AddConsoleExporter();
});

var app = builder.Build();
app.MapGet("/", () => "Hello, observability!");
app.Run();

Run this and make a request to your API. You'll see trace data, metrics records, and log output in the console -- all three signals, with no additional infrastructure. The Console exporter is the recommended way to verify your instrumentation is working before wiring up a real backend. Once you see the expected spans and metrics locally, switching to OTLP for staging is a one-line change.


Traces in Depth -- Custom ActivitySource for Business Logic

Auto-instrumentation from AddAspNetCoreInstrumentation and AddHttpClientInstrumentation captures the outer shell of your request handling: the incoming HTTP request, the route matched, the response status code. That's valuable context. But the interesting information is usually inside your business logic -- the inventory availability check, the pricing calculation, the fraud detection call.

For custom tracing, you create an ActivitySource (the OTel Tracer equivalent in .NET) and start Activity instances around the operations you want to observe:

using System.Diagnostics;

namespace MyApp.Services;

public class CheckoutService
{
    private static readonly ActivitySource _activitySource =
        new("MyApp.Checkout", "1.0.0");
    private readonly ILogger<CheckoutService> _logger;

    public CheckoutService(ILogger<CheckoutService> logger)
    {
        _logger = logger;
    }

    public async Task<CheckoutResult> CheckoutAsync(Cart cart, string userId)
    {
        using var activity = _activitySource.StartActivity("Checkout");
        activity?.SetTag("cart.item_count", cart.Items.Count);
        activity?.SetTag("user.id", userId);

        _logger.LogInformation(
            "Starting checkout for user {UserId} with {ItemCount} items",
            userId, cart.Items.Count);

        try
        {
            var result = await ProcessCheckoutAsync(cart);
            activity?.SetStatus(ActivityStatusCode.Ok);
            return result;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
}

A few important details in this pattern worth calling out:

The ? null-conditional operator on activity is deliberate. StartActivity returns null when no listeners are registered for the source -- which happens when OTel isn't configured, or when sampling decided not to record this trace. Your business code should handle null gracefully rather than throw NullReferenceException.

RecordException adds a structured event to the span with the exception type, message, and stack trace. This is more useful than a plain error status -- observability backends can index and search exceptions across all spans, often letting you find error patterns without manually correlating log entries.

Tags follow OpenTelemetry's semantic conventions where possible. The OTel project publishes standard attribute names for common domains: user.id, db.system, http.method, messaging.system, and many others. Using standard names makes your telemetry work with out-of-the-box dashboards and queries in backends that understand OTel conventions.

Remember to register your ActivitySource: .AddSource("MyApp.Checkout") or .AddSource("MyApp.*") in your WithTracing(...) configuration. Without this, the spans are created but silently dropped.


Metrics in Depth -- Counters, Histograms, and Gauges

Metrics are the most cost-effective observability signal for production monitoring. Unlike traces (one record per request) and logs (one record per significant event), metrics are aggregated in-process before export. A histogram of 10,000 requests becomes a compact set of bucket counts. This makes metrics cheap to store, fast to query, and well-suited for high-traffic production systems.

OpenTelemetry .NET uses System.Diagnostics.Metrics -- the same BCL API you would use without OTel at all:

using System.Diagnostics;
using System.Diagnostics.Metrics;

public class ApiMetrics : IDisposable
{
    private readonly Meter _meter;
    private readonly Counter<long> _requestsTotal;
    private readonly Histogram<double> _requestDuration;

    public ApiMetrics(IMeterFactory meterFactory)
    {
        _meter = meterFactory.Create("MyApp.Api");

        _requestsTotal = _meter.CreateCounter<long>(
            "api.requests.total",
            description: "Total API requests processed");

        _requestDuration = _meter.CreateHistogram<double>(
            "api.request.duration",
            unit: "ms",
            description: "Request processing duration");
    }

    public void RecordRequest(string endpoint, int statusCode, double durationMs)
    {
        var tags = new TagList
        {
            { "endpoint", endpoint },
            { "status_code", statusCode }
        };
        _requestsTotal.Add(1, tags);
        _requestDuration.Record(durationMs, tags);
    }

    public void Dispose() => _meter.Dispose();
}

A few practical notes on metrics design that matter at production scale:

Use IMeterFactory in DI-integrated code. It handles Meter lifecycle correctly and integrates with OTel's meter collection pipeline. Avoid new Meter(...) directly in production services.

Be careful with high-cardinality tags. Tags with many unique values -- raw user IDs, full URLs, individual error messages -- cause cardinality explosion. Your backend creates a separate time series per unique tag combination, which can become prohibitively expensive at scale. Use low-cardinality tags: status codes, route templates (not raw paths), service names, predefined categories.

Choose instrument types based on what you're measuring. Counter<T> for values that only increase (requests processed, bytes sent, errors encountered). Histogram<T> for distributions where you care about percentiles (latency, payload sizes). ObservableGauge<T> for point-in-time values where measurement has cost and should happen on demand (active connections, queue depth). UpDownCounter<T> for values that both increase and decrease (in-flight requests, cache entries).


Logging in Depth -- Structured Logs with Trace Correlation

OpenTelemetry doesn't replace your logging setup. It enhances it. You keep using ILogger<T> exactly as before. The OTel logging integration adds two things: routing log records through the OTel export pipeline (to the same backend receiving your traces), and automatically annotating each log record with the current trace ID and span ID from the ambient Activity.

That automatic annotation is what makes the combination especially useful. When you're investigating a trace in Grafana Tempo and want to see the detailed log records from that exact request, you click through to the correlated logs in Grafana Loki using the trace ID. No manual cross-referencing. No grep-for-request-ID archaeology. The Logging in .NET guide covers structured logging patterns in depth -- all of which work naturally alongside OTel's log correlation.

For teams already using Serilog, the two can coexist productively. Serilog has a much richer sink ecosystem -- file rotation, Seq, Elasticsearch, Slack, and dozens more. OpenTelemetry's log export is focused on the OTLP pipeline. You can route ILogger output through both simultaneously: Serilog handles its configured sinks, OTel handles OTLP export and trace ID annotation. The Serilog in .NET guide covers Serilog's setup and capabilities in detail.


ASP.NET Core Integration -- What Auto-Instrumentation Covers

AddAspNetCoreInstrumentation() hooks into ASP.NET Core's middleware pipeline and automatically creates spans for every incoming HTTP request. Each span captures the HTTP method, the matched route template (not the raw URL -- this is intentional to avoid cardinality explosion from URL parameters), the response status code, any exception that escaped the pipeline, and the total request duration.

It also reads incoming W3C traceparent headers and establishes the parent context automatically. Combined with AddHttpClientInstrumentation() for outbound calls, you get end-to-end distributed tracing for HTTP-based service communication without writing a single span creation line in your controllers.

The ASP.NET Core Web API complete guide and the ASP.NET Core middleware guide cover the ASP.NET Core internals that OTel's instrumentation hooks into. Understanding the middleware pipeline helps you reason about where instrumentation starts and ends relative to your own middleware and error handling.


Exporters -- Getting Telemetry to Your Backend

An exporter is responsible for serializing telemetry and sending it out of your process to a storage and visualization backend. OpenTelemetry .NET provides several options at different points in the development-to-production lifecycle.

Console exporter writes telemetry to stdout. It's only appropriate for local development and initial configuration verification -- not for production.

OTLP exporter is the production standard. It sends telemetry via OpenTelemetry Protocol (gRPC or HTTP/protobuf) to any compatible backend. Virtually every modern observability platform supports OTLP: Jaeger, Grafana Tempo, Grafana Loki, Seq, Honeycomb, and many others. Configure the endpoint via the OTEL_EXPORTER_OTLP_ENDPOINT environment variable for easy environment-specific configuration.

Prometheus exporter (metrics only) exposes a /metrics scrape endpoint that Prometheus polls on its configured interval. It's the right choice when you already have Prometheus infrastructure and don't want to introduce an OTel collector.

Azure Monitor exporter (Azure.Monitor.OpenTelemetry.AspNetCore) exports all three signals to Application Insights with a single package reference and a single line of configuration. It's the recommended path for teams deploying to Azure. The Deploying ASP.NET Core to Azure guide covers the deployment environment where your OTel-instrumented application will run.

Here's a production-ready setup using OTLP with environment-based configuration:

// Production setup with OTLP and environment-based config
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(
            serviceName: builder.Configuration["Otel:ServiceName"] ?? "MyApp",
            serviceVersion: builder.Configuration["Otel:ServiceVersion"] ?? "1.0.0")
        .AddAttributes(new Dictionary<string, object>
        {
            ["deployment.environment"] = builder.Environment.EnvironmentName
        }))
    .WithTracing(tracing => tracing
        .AddSource("MyApp.*")
        .AddAspNetCoreInstrumentation(o =>
        {
            o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health");
        })
        .AddHttpClientInstrumentation()
        .AddOtlpExporter())
    .WithMetrics(metrics => metrics
        .AddMeter("MyApp.*")
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddOtlpExporter());

builder.Logging.AddOpenTelemetry(logging =>
{
    logging.IncludeFormattedMessage = true;
    logging.IncludeScopes = true;
    logging.AddOtlpExporter();
});

The health check filter in the tracing configuration (o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health")) is worth highlighting. Health check endpoints are typically called every few seconds by load balancers and orchestrators. They're rarely interesting from a tracing perspective and create significant noise in your trace backend. Filtering them out reduces storage costs and keeps your traces focused on actual business operations.

AddRuntimeInstrumentation() for metrics adds a valuable set of .NET runtime health indicators: GC pause durations, heap generation sizes, thread pool queue depth, lock contention, and exception rates. These give you a window into the health of the runtime itself, not just your application logic. Memory leaks, thread pool exhaustion, and GC pressure often show up here before they manifest as latency or errors.


Distributed Tracing and Multi-Service Correlation

OpenTelemetry .NET uses the W3C Trace Context standard for distributed tracing. When AddHttpClientInstrumentation() is configured, every outbound HTTP request automatically gets a traceparent header injected. When AddAspNetCoreInstrumentation() is configured in a downstream service, incoming traceparent headers are automatically read and used as the parent context for that request's root span.

The result: a single trace ID links spans across all services in a call chain. In your observability backend, you see the complete call tree -- Service A calls Service B calls Service C -- as a unified timeline with network latency visible between each hop.

For service communication that doesn't use HTTP -- message queues, gRPC streams, custom protocols -- context propagation requires manual Inject and Extract calls using Propagators.DefaultTextMapPropagator. This is especially relevant in architectures where not all service interactions are synchronous HTTP calls.

The same distributed tracing mechanism works in modular monolith architectures too. In a monolith, context flows through in-memory Activity state rather than HTTP headers, but the trace tree structure is identical. When you transition from a monolith to microservices, your OTel instrumentation carries forward -- you just start seeing network hops where there were previously in-process calls.


OpenTelemetry .NET vs Serilog -- When to Use Each

A common question from teams already running Serilog: "Do I need OpenTelemetry if I'm already logging everything?"

They serve meaningfully different purposes.

Capability OpenTelemetry .NET Serilog
Distributed tracing ✓ Built-in ✗ Not available
Metrics aggregation ✓ Built-in ✗ Not available
Structured logging ✓ Via ILogger ✓ Native
Log routing sinks Console, OTLP 30+ sinks (file, Seq, Elasticsearch, etc.)
Vendor-neutral transport ✓ OTLP standard ✗ Sink-specific
Trace-log correlation ✓ Automatic via trace ID Manual (requires separate tracing library)

Serilog is a structured logging library with a rich ecosystem of output sinks: files, databases, Seq, Elasticsearch, Splunk, Slack, and many others. If your primary observability need is structured log routing with flexible destinations and formatting, Serilog excels. It doesn't provide distributed tracing or metrics.

OpenTelemetry provides unified telemetry across all three signals with vendor-neutral transport. It excels at distributed tracing and metrics aggregation. Its logging integration is solid but less feature-rich than Serilog's sink ecosystem.

Using both together is a reasonable production setup. Serilog handles rich log sink routing and formatting. OpenTelemetry collects ILogger output for OTLP export and trace ID annotation. The builder.Logging.AddOpenTelemetry(...) call hooks into ILogger, so any log record written via ILogger<T> flows through both pipelines simultaneously -- Serilog gets it for sink routing, OTel gets it for backend export and trace correlation.

If you're building a new system from scratch and your primary observability backend supports OTLP, OpenTelemetry alone is a solid choice. If you have an existing Serilog setup with meaningful sink configuration, adding OTel alongside it adds distributed tracing and metrics without disrupting your log routing.


Dependency Injection and Architecture Patterns

OpenTelemetry .NET is designed from the ground up to work with .NET's dependency injection model. ActivitySource, Meter, and ILogger<T> are all registered through the DI container. IMeterFactory resolves correctly from the container and integrates with OTel's meter collection pipeline. The AddOpenTelemetry() extension registers the OTel SDK as a hosted service that manages the telemetry pipeline lifecycle.

This alignment with .NET's DI model means OTel fits naturally into any application that follows standard .NET patterns. If you're not familiar with IServiceCollection configuration and service lifetime management, the IServiceCollection guide covers the fundamentals that underpin how OTel registers its services.

The Dependency Inversion Principle guide is also relevant here: the OTel SDK's design follows dependency inversion throughout -- exporters, samplers, processors, and propagators are all injected abstractions, which is why the "change backend by changing configuration" promise is achievable.


Getting Started with OpenTelemetry .NET

The practical path to OpenTelemetry .NET in production is iterative.

Start with Console exporter locally. Add the quick-start configuration shown earlier and run your app. Verify you see trace and metric output in the console for normal requests. This confirms your instrumentation is wired up correctly before you introduce a real backend.

Add custom spans for key business operations. Identify the 3-5 most important workflows in your application and add ActivitySource spans around them. Don't over-instrument early -- focus on operations where understanding timing and parameters actually helps diagnose problems.

Switch to OTLP for staging. Run a Grafana stack (Tempo + Loki + Prometheus) via Docker Compose, or use a free tier of a hosted OTel backend like Honeycomb or Grafana Cloud. Verify your traces appear end-to-end, including distributed traces if you have multiple services.

Add metrics for your SLOs. Define your service level objectives -- request rate, error rate, latency percentiles -- and create the specific metric instruments that track them. Set up dashboards and alerting based on these metrics.

Deploy to production with ParentBasedSampler. Start at 10-20% sampling for traces. To ensure error traces are always captured, consider a custom sampler that returns RecordAndSample when an error is detected, or use tail-based sampling in the OTel Collector -- which makes sampling decisions after the fact based on trace outcomes. For services with significant traffic volume, tail sampling (via an OTel Collector) lets you make sampling decisions after the fact based on trace outcomes.

The OTel SDK's configuration model is entirely code-based, which means you can use environment variables and IConfiguration to drive exporter endpoints, sampling rates, and service names -- without code changes between environments.


FAQ

What is OpenTelemetry .NET and how does it differ from traditional logging?

OpenTelemetry .NET is an observability SDK that collects traces, metrics, and logs in a unified, vendor-neutral format. Traditional logging libraries focus on log records. OTel adds distributed traces (for visualizing request flow and latency across services) and metrics (for aggregated health monitoring), and automatically correlates all three signals through trace IDs. The key practical difference: OTel can show you not just what was logged, but the complete shape and timing of a request's execution across multiple services -- something log files alone can't provide.

Do I need OpenTelemetry if I'm already using Application Insights?

If you're on Azure and currently using the Application Insights SDK directly, migrating to OpenTelemetry is increasingly the recommended path. Microsoft has been building Application Insights on top of OTel instrumentation, and the Azure.Monitor.OpenTelemetry.AspNetCore package exports OTel-based telemetry to Application Insights without changing your observability backend. The advantage is portability: your instrumentation code works with any OTel-compatible backend, giving you flexibility if you ever need to add a secondary backend or change providers.

How does OpenTelemetry .NET affect application performance?

The OTel SDK is designed to minimize overhead. When sampling is configured (for example, 10% trace sampling), unsampled requests incur only a small allocation cost for the Activity null check. Sampled requests have measurable but typically small overhead from span creation, tag attachment, and export queue writes. Metrics are the cheapest signal -- they're aggregated in-process and the export cost is amortized across many measurements. In most web API workloads, OTel overhead is well under 1ms per request at moderate sampling rates. Benchmark your specific workload if performance is a primary concern.

Can I use OpenTelemetry in a modular monolith or only in microservices?

OpenTelemetry works well in any .NET application, including modular monoliths. In a monolith, all modules run in the same process and span context flows through in-memory Activity state -- no HTTP headers are involved. You still get full trace trees showing how requests flow through modules, where time is spent, and where errors originate. When you eventually move toward microservices, your OTel instrumentation carries over unchanged. The spans that were previously linked in memory become linked via traceparent headers over HTTP, but the trace structure in your backend looks the same.

What exporters should I use for a production .NET application?

For most production setups, OTLP via OpenTelemetry.Exporter.OpenTelemetryProtocol is the recommended choice. It's supported by virtually every modern observability backend and gives you flexibility to change backends without changing application code. If you're on Azure, Azure.Monitor.OpenTelemetry.AspNetCore provides a simpler single-call integration for Application Insights. If you have existing Prometheus infrastructure, the Prometheus exporter adds a scrape endpoint for metrics. During local development, the Console exporter is the fastest way to verify your instrumentation.

What is the difference between traces, metrics, and logs in OpenTelemetry?

Traces capture the structure and timing of individual requests -- they show what happened, in what order, and how long each step took. They're the right tool for latency analysis and root cause investigation of specific failures. Metrics capture aggregated measurements over time -- request counts, error rates, duration distributions -- and are the right tool for dashboards, SLO monitoring, and alerting at scale. Logs capture detailed, point-in-time events with full context. All three are collected by the OTel SDK and, when configured together, log records are automatically tagged with trace IDs, enabling direct navigation between a trace and its correlated log entries in your backend.

How do I add OpenTelemetry to an existing ASP.NET Core application without rewriting my instrumentation?

The minimum addition is three NuGet packages (OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, and an exporter), plus an AddOpenTelemetry() call in Program.cs. Because ASP.NET Core already uses Activity internally for request tracing, AddAspNetCoreInstrumentation() immediately starts capturing HTTP request spans for every endpoint -- zero changes to your controllers or middleware. You can then incrementally add custom ActivitySource spans for business logic, Meter instruments for application metrics, and the OTel logging integration for trace-correlated logs as your needs grow.


Disclaimer: Yes! This was written by AI based on input and direction from yours truly, Nick Cosentino, for Dev Leader. I have ensured that it represents my thoughts and perspectives on OpenTelemetry in .NET and application observability.

OpenTelemetry and Observability in Microsoft Agent Framework

Set up microsoft agent framework opentelemetry observability in C# to track token usage, latency, tool calls, and errors in your AI agents.

Logging in .NET: The Complete Developer's Guide

Master logging in .NET with ILogger, structured logging, log levels, Serilog, and OpenTelemetry. Complete guide for .NET 9 and .NET 10 developers.

HttpClient Logging and Observability in .NET 10: ILogger, DelegatingHandler, and OpenTelemetry

How to implement HttpClient logging and observability in .NET 10 -- covering built-in ILogger integration, DelegatingHandler for custom request logging, OpenTelemetry tracing and metrics, and redacting sensitive headers in production.

An error has occurred. This application may no longer respond until reloaded. Reload