BrandGhost
OpenTelemetry Exporters in .NET: OTLP, Prometheus, Jaeger, and Azure Monitor

OpenTelemetry Exporters in .NET: OTLP, Prometheus, Jaeger, and Azure Monitor

You've set up instrumentation. Spans are being created. Metrics are being recorded. Logs are flowing through the OpenTelemetry SDK. Now what? Without configured OpenTelemetry exporters in .NET, all that telemetry simply disappears into the void at the end of each request. Exporters are the output layer of the OpenTelemetry SDK -- they take your recorded telemetry and forward it to a backend where it can be stored, queried, and visualized. This guide covers every major exporter option for .NET applications: Console for local development, OTLP for universal compatibility, Prometheus for metrics scraping, Jaeger for trace visualization, and Azure Monitor for Application Insights integration.

If you're looking for the full picture, check out the OpenTelemetry in .NET: Complete Observability Guide.

What Are OpenTelemetry Exporters in .NET?

At a conceptual level, the OpenTelemetry SDK pipeline has three stages: instrumentation (producing telemetry), processing (filtering, batching, enriching), and exporting (sending to a backend). Exporters handle that third stage.

Each signal type -- traces, metrics, and logs -- has its own independent exporter chain. This means you can send your traces to Jaeger, your metrics to Prometheus, and your logs to a log aggregation service, all from the same application and the same OpenTelemetry SDK setup. The pipelines are independent, and each can have multiple exporters attached simultaneously.

It's worth understanding that OpenTelemetry exporters are not the same as logging sinks. If you're familiar with Serilog Sinks, you'll recognize a conceptually similar idea -- but OpenTelemetry exporters cover all three telemetry signals, not just logs. The Serilog Best Practices for Production .NET Applications guide covers the logging side if you're running Serilog and OpenTelemetry side by side and want to understand how they complement each other.

The exporter receives telemetry data from the SDK's processor and is responsible for serializing it and delivering it to a backend. Some exporters -- like the console exporter -- are synchronous and simple. Others -- like the OTLP exporter -- are asynchronous, batched, and designed for production-scale throughput.

Console Exporter: Start Here for Local Development

The console exporter is the simplest way to see what OpenTelemetry is producing. It prints telemetry directly to stdout, which makes it invaluable during initial setup and debugging. You don't need any backend infrastructure -- just run your application and watch the telemetry appear in your terminal.

Install the package:

dotnet add package OpenTelemetry.Exporter.Console

Wire it up in your application startup:

// NuGet: OpenTelemetry.Exporter.Console
builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("MyApp"))
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddConsoleExporter())
    .WithMetrics(m => m
        .AddAspNetCoreInstrumentation()
        .AddConsoleExporter());

The console output is deliberately verbose. Each span dumps its full attribute list, timing, status, and events. This verbosity is exactly what you want when you're first getting instrumentation right -- you can verify that the expected attributes are present, that spans are parented correctly, and that your custom enrichment is producing the tags you intend.

In production, the console exporter is generally not appropriate. Output volume is high, nothing persists, and the sync export model (see the batching section below) would become a bottleneck at scale. Use it locally and in CI pipelines where the noise is acceptable, then replace it with OTLP or another production exporter before deploying.

The console exporter uses SimpleExportProcessor internally, which means it exports each span synchronously as it completes. This is fine for a terminal output scenario but would create performance problems under load. Production exporters use BatchExportProcessor, which we'll discuss shortly.

The OTLP Exporter: Universal Compatibility for OpenTelemetry Exporters in .NET

The OTLP (OpenTelemetry Protocol) exporter is the workhorse of the OpenTelemetry ecosystem. It implements the OpenTelemetry Protocol -- a vendor-neutral wire format for telemetry data. Virtually every modern observability backend supports OTLP, including Jaeger, Grafana Tempo, Honeycomb, Lightstep, SigNoz, New Relic, Datadog (via the collector), and the OpenTelemetry Collector itself.

Install the package:

dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

OTLP supports two transport protocols:

  • gRPC (default, port 4317) -- efficient binary protocol, lower overhead, well-suited for high-throughput services
  • HTTP/protobuf (port 4318) -- works anywhere HTTP works, including environments where gRPC is blocked by proxies or load balancers
// NuGet: OpenTelemetry.Exporter.OpenTelemetryProtocol
using OpenTelemetry.Exporter;

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("MyApp"))
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter(otlp =>
        {
            // gRPC (default) -- port 4317
            otlp.Endpoint = new Uri("http://otel-collector:4317");
            otlp.Protocol = OtlpExportProtocol.Grpc;

            // For HTTP/protobuf instead:
            // otlp.Endpoint = new Uri("http://otel-collector:4318/v1/traces");
            // otlp.Protocol = OtlpExportProtocol.HttpProtobuf;

            // Optional: authentication header
            otlp.Headers = "x-honeycomb-team=YOUR_API_KEY";
        }));

Notice that when using HTTP/protobuf, the endpoint URL includes the signal-specific path (/v1/traces for traces, /v1/metrics for metrics, /v1/logs for logs). This path suffix is required for the HTTP variant. The gRPC variant handles routing internally -- you configure only the base URL without a path suffix.

OTLP is the recommended approach for most production deployments. It decouples your application from any specific observability backend. You can migrate from Jaeger to Grafana Tempo, or from Tempo to Honeycomb, without touching application code -- just reconfigure the collector or the exporter endpoint. This vendor neutrality is one of OpenTelemetry's core promises, and the OTLP exporter is what makes it real in practice.

Sending Traces to Jaeger

Historically, sending traces to Jaeger required the OpenTelemetry.Exporter.Jaeger NuGet package. That package is now deprecated and should not be used in new projects. Jaeger has supported the OTLP protocol natively since version 1.35, and the recommended approach for new .NET projects is to use the standard OTLP exporter pointed at Jaeger's OTLP endpoint.

There is no separate Jaeger-specific package needed. The OTLP exporter is the Jaeger exporter for modern Jaeger installations.

For local development with Jaeger, the all-in-one Docker image exposes OTLP endpoints out of the box:

docker run -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one

Configure your OTLP exporter to point at http://localhost:4317 (gRPC) or http://localhost:4318 (HTTP/protobuf). Traces appear in the Jaeger UI at http://localhost:16686.

This also means that Jaeger, Grafana Tempo, SigNoz, and any other OTLP-compatible backend all use exactly the same exporter configuration in your .NET application. Only the endpoint URL changes between backends. This makes it practical to evaluate different backends without touching your application code.

Prometheus Exporter for Metrics Scraping

Prometheus uses a pull-based metrics model. Rather than your application pushing metrics to Prometheus, Prometheus periodically scrapes a /metrics HTTP endpoint that your application exposes. The OpenTelemetry Prometheus exporter for ASP.NET Core implements this pattern.

Install the package:

dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore

Configure it alongside your metrics instrumentation:

// NuGet: OpenTelemetry.Exporter.Prometheus.AspNetCore
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddMeter("MyApp.*")
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddPrometheusExporter()); // exposes /metrics endpoint

var app = builder.Build();

// IMPORTANT: register the scraping endpoint
app.MapPrometheusScrapingEndpoint();

// In production, restrict access to this endpoint
// app.MapPrometheusScrapingEndpoint().RequireHost("monitoring.internal");

The MapPrometheusScrapingEndpoint() call is not optional -- without it, the exporter is registered but the endpoint is never mounted, and Prometheus scrapes will return 404. The comment about RequireHost is worth taking seriously in production. The /metrics endpoint can expose implementation details -- GC heap sizes, thread pool queue depths, custom business metrics -- that you likely don't want publicly accessible.

The Prometheus exporter is specific to the metrics signal. Prometheus is a metrics system and has no concept of traces or logs. For traces, you'd use the OTLP exporter pointed at a tracing backend alongside the Prometheus metrics setup. The two exporters operate on independent signal pipelines and don't interfere with each other.

The AddRuntimeInstrumentation() call is a common addition for production Prometheus setups. It adds .NET runtime metrics -- GC collection counts and durations, thread pool queue lengths, working set size, and similar process-level metrics -- giving you immediate visibility into the health of the .NET runtime without any application-level instrumentation code.

Azure Monitor: Application Insights via OpenTelemetry Exporter .NET

If you're deploying to Azure and using Application Insights, the Azure.Monitor.OpenTelemetry.AspNetCore package provides a streamlined single-call setup that configures all three signals -- traces, metrics, and logs -- for Application Insights:

Install the package:

dotnet add package Azure.Monitor.OpenTelemetry.AspNetCore

Then configure it:

// NuGet: Azure.Monitor.OpenTelemetry.AspNetCore
// This single call configures traces, metrics, and logs for Application Insights
builder.Services.AddOpenTelemetry()
    .UseAzureMonitor(options =>
    {
        options.ConnectionString =
            builder.Configuration["ApplicationInsights:ConnectionString"]
            ?? Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
    })
    .WithTracing(t => t.AddSource("MyApp.*"))
    .WithMetrics(m => m.AddMeter("MyApp.*"));

The UseAzureMonitor() call handles the Application Insights connection and maps OpenTelemetry concepts to Application Insights constructs. Traces become requests and dependencies in Application Insights. Metrics appear in the metrics explorer. Log records are forwarded as traces. The mapping is handled by the Azure Monitor exporter -- you don't need to understand the Application Insights data model to use it.

One important note: UseAzureMonitor() sets up default instrumentation for ASP.NET Core, HttpClient, and other common libraries. If you also call AddAspNetCoreInstrumentation() or AddHttpClientInstrumentation() separately, you will get duplicate span creation for the same requests. Use UseAzureMonitor() alone and configure additional instrumentation through its options if needed, rather than calling AddAspNetCoreInstrumentation() or AddHttpClientInstrumentation() separately.

The connection string approach shown above is the recommended pattern for production. In Azure App Service, you can set APPLICATIONINSIGHTS_CONNECTION_STRING as an application setting and it will be injected as an environment variable automatically. For deployment patterns that make this configuration management clean, the Deploying ASP.NET Core to Azure and Docker guide covers practical deployment configurations in detail.

Using Multiple Exporters: Dev vs Production

One of the more practical patterns in OpenTelemetry exporters in .NET configuration is using different exporters based on environment. Console output for local development, OTLP for staging and production. Here's how to wire that up cleanly:

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("MyApp"))
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation();

        if (builder.Environment.IsDevelopment())
        {
            tracing.AddConsoleExporter();
        }
        else
        {
            tracing.AddOtlpExporter(otlp =>
            {
                otlp.Endpoint = new Uri(
                    builder.Configuration["Otel:OtlpEndpoint"]
                    ?? "http://otel-collector:4317");
            });
        }
    });

This pattern keeps your local development experience clean -- spans appear in the terminal -- while ensuring production telemetry goes to the right backend. The same approach applies to metrics and logs: add console in development, add OTLP (or Prometheus, or Azure Monitor) in production.

You can also add multiple exporters to the same pipeline without conditional logic. Adding both AddConsoleExporter() and AddOtlpExporter() means every span goes to both destinations simultaneously. This is useful during brief debugging sessions in staging where you want local console output without interrupting your production trace data stream.

Setting up the DI container correctly for all of this is covered in the IServiceCollection in C# Complete Guide. The OpenTelemetry SDK uses the standard IServiceCollection extension methods throughout, so the registration patterns you already know apply directly.

Batching: BatchExportProcessor vs SimpleExportProcessor

This is a detail that matters significantly in production. By default, the OTLP exporter and most production-oriented exporters use BatchExportProcessor. The console exporter uses SimpleExportProcessor. Understanding the difference matters when you're evaluating performance overhead.

SimpleExportProcessor exports each span individually and synchronously as soon as it completes. There's no buffering. For a local stdout write, this is fine -- a write to a terminal buffer is cheap. For a remote backend over the network, synchronous export means each span completion blocks the application thread waiting for a network call to return. At scale, this becomes a meaningful performance problem.

BatchExportProcessor buffers completed spans in memory and exports them in configurable batches on a background thread. The application thread that completes a span returns immediately without waiting for the export call. This is the production default for good reason -- it keeps the export overhead off the hot path.

The batch processor behavior is configurable via environment variables:

OTEL_BSP_MAX_QUEUE_SIZE        (default: 2048 spans)
OTEL_BSP_SCHEDULE_DELAY        (default: 5000ms)
OTEL_BSP_MAX_EXPORT_BATCH_SIZE (default: 512 spans)

The defaults work well for most services. If you're operating at very high span volumes -- tens of thousands of spans per second -- you may need to tune the queue size and batch size to avoid dropping spans when the queue fills faster than it's drained. Most .NET services operate comfortably within the defaults.

Configuring OpenTelemetry Exporter .NET via Environment Variables

A core design goal of OpenTelemetry is that you should be able to configure the SDK entirely through environment variables, without recompiling. This is particularly valuable in Kubernetes environments where configuration varies by cluster or namespace. The OTLP exporter respects these standard environment variables:

  • OTEL_EXPORTER_OTLP_ENDPOINT -- overrides the endpoint URL set in code. Most commonly used to point at the local cluster's collector sidecar or a shared collector deployment.
  • OTEL_EXPORTER_OTLP_HEADERS -- comma-separated key=value pairs added as headers to every export call. Used for authentication tokens to hosted backends (Honeycomb, Lightstep, etc.).
  • OTEL_EXPORTER_OTLP_PROTOCOL -- grpc or http/protobuf. Overrides the code-level protocol setting.
  • OTEL_EXPORTER_OTLP_TIMEOUT -- export timeout in milliseconds. The default is 10000ms (10 seconds).

When an environment variable is set, it takes precedence over the corresponding code-level configuration. This means you can set a sensible default endpoint in code for local development and override it via environment variable in your deployment manifests -- without touching the application binary.

This environment-variable-first configuration approach integrates well with the standard .NET configuration system. The ASP.NET Core Web API Complete Guide covers the layered configuration system -- environment variables, appsettings.json, secrets -- and how they interact across different deployment environments.

For a broader view of how OpenTelemetry observability fits into your ASP.NET Core request pipeline alongside middleware and routing, the ASP.NET Core Middleware guide explains how the pipeline is composed and where observability hooks in.

FAQ

What is the difference between the OTLP exporter and the deprecated Jaeger exporter in .NET?

The OpenTelemetry.Exporter.Jaeger NuGet package is deprecated and should not be used in new projects. Jaeger has supported the OTLP protocol natively since version 1.35, so the current recommended approach is to use OpenTelemetry.Exporter.OpenTelemetryProtocol (the OTLP exporter) and point it at Jaeger's OTLP port. This gives you a single exporter package that works with Jaeger, Grafana Tempo, Honeycomb, and any other OTLP-compatible backend -- no backend-specific packages required.

Can I use multiple OpenTelemetry exporters at the same time?

Yes. You can add multiple exporters to the same signal pipeline and each one receives a copy of the telemetry. A common pattern is to add both a console exporter and an OTLP exporter conditionally based on environment. You can also add two OTLP exporters pointing at different endpoints -- useful when you want to forward the same telemetry to two different backends simultaneously, such as a team-specific Jaeger instance and a centralized monitoring platform.

Does the Prometheus exporter work for traces and logs too?

No. The Prometheus exporter is specific to the metrics signal. Prometheus is a metrics system -- it has no native concept of traces or logs. For traces, use the OTLP exporter pointing at a tracing backend like Jaeger or Grafana Tempo. For logs, use the OTLP exporter pointing at a log aggregation backend or configure OpenTelemetry logging via builder.Logging.AddOpenTelemetry(). All three signal pipelines can run simultaneously in the same application without interfering with each other.

What NuGet packages do I need for an OpenTelemetry exporter .NET setup?

The packages depend on which exporters you're using. OpenTelemetry.Exporter.OpenTelemetryProtocol covers OTLP (the most universally useful choice). OpenTelemetry.Exporter.Console covers local development. OpenTelemetry.Exporter.Prometheus.AspNetCore covers the Prometheus scraping endpoint. Azure.Monitor.OpenTelemetry.AspNetCore covers Application Insights. You'll also need OpenTelemetry.Extensions.Hosting for the AddOpenTelemetry() entry point, plus any instrumentation packages for your chosen signals.

How do I authenticate with a hosted observability backend using the OTLP exporter?

Set the Headers property on the OTLP exporter options to include your API key header. For example, Honeycomb uses x-honeycomb-team=YOUR_API_KEY and Lightstep uses lightstep-access-token=YOUR_TOKEN. You can also set this via the OTEL_EXPORTER_OTLP_HEADERS environment variable, which is the preferred approach for production since it keeps credentials out of source code and application configuration files.

Should I use BatchExportProcessor or SimpleExportProcessor in production?

Use BatchExportProcessor (the default for OTLP and most production exporters) in production. It buffers spans in memory and exports them asynchronously on a background thread, so span completion never blocks your application threads waiting for a network call. SimpleExportProcessor exports synchronously, which is appropriate for console output but can become a throughput bottleneck for any remote backend under meaningful load.

How do I verify that my OpenTelemetry exporter is actually sending data?

The quickest verification is to temporarily add the console exporter alongside your production exporter. If spans appear in stdout, the instrumentation pipeline is working correctly. If the console shows spans but your backend doesn't receive them, the issue is in exporter configuration -- wrong endpoint, missing auth header, or a network routing problem. You can also enable the OpenTelemetry SDK's self-diagnostics by setting the OTEL_LOG_LEVEL environment variable to debug, which logs internal SDK activity including export successes, failures, and dropped spans.


Wrapping Up

The OpenTelemetry exporter .NET ecosystem covers every major observability destination. Console for local debugging, OTLP for universal production compatibility, Prometheus for pull-based metrics scraping, Jaeger via OTLP for trace visualization, and UseAzureMonitor() for Application Insights. The right configuration depends on your deployment target and observability infrastructure -- but the patterns are consistent across all of them.

A few principles worth taking forward: prefer OTLP in production because it decouples your application from any specific backend. Use the console exporter locally to verify instrumentation is working before you set up backend infrastructure. Take advantage of environment variable configuration in containerized deployments to keep backend endpoints out of your application binary. And remember that multiple exporters can coexist in the same signal pipeline -- development and production exporters don't need to be mutually exclusive.

For more on building fully observable .NET applications, the Logging in .NET: The Complete Developer's Guide covers the logging signal in depth -- including how to correlate log records with your OpenTelemetry traces so both data streams tell the same story.

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 exporters in .NET.

OpenTelemetry in .NET: Complete Observability Guide

The complete OpenTelemetry .NET guide for developers: traces, metrics, and logs with C# examples, ASP.NET Core setup, exporters, and distributed tracing.

OpenTelemetry ASP.NET Core: Setup, Auto-Instrumentation, and Configuration

Set up OpenTelemetry ASP.NET Core with AddOpenTelemetry(), auto-instrumentation, metrics collection, and practical configuration examples for .NET 10.

OpenTelemetry Logging .NET: ILogger Integration and Structured Log Export

How to set up OpenTelemetry logging .NET using ILogger -- structured log export, trace context correlation, OTLP export, and practical C# examples.

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