OpenTelemetry .NET can feel like a black box, but it’s actually just an unusually well-behaved observer that doesn’t change the system it’s watching.
Here’s a .NET application, a simple ASP.NET Core API, with OpenTelemetry configured to send traces to an OTLP collector:
// Program.cs
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Configure OpenTelemetry
builder.Services.AddOpenTelemetry()
.ConfigureResource(resourceBuilder => resourceBuilder
.AddService("MyAspNetCoreApp")) // Identifies this service
.WithTracing(tracingBuilder => tracingBuilder
.AddAspNetCoreInstrumentation() // Instruments ASP.NET Core requests
.AddHttpClientInstrumentation() // Instruments outgoing HTTP requests
.AddOtlpExporter(otlpExporterOptions => // Configure OTLP exporter
{
otlpExporterOptions.Endpoint = new Uri("http://localhost:4318"); // Your OTLP collector endpoint
otlpExporterOptions.Protocol = OtlpExportProtocol.HttpProtobuf;
}))
.WithMetrics(metricsBuilder => metricsBuilder
.AddAspNetCoreInstrumentation() // Instruments ASP.NET Core metrics
.AddRuntimeMetrics() // Collects .NET runtime metrics (GC, CPU, etc.)
.AddOtlpExporter(otlpExporterOptions => // Configure OTLP exporter for metrics
{
otlpExporterOptions.Endpoint = new Uri("http://localhost:4318"); // Your OTLP collector endpoint
otlpExporterOptions.Protocol = OtlpExportProtocol.HttpProtobuf;
}));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
This code sets up OpenTelemetry to automatically capture traces and metrics from ASP.NET Core requests and outgoing HTTP calls. The .ConfigureResource() call is crucial for identifying your application within your observability system. The .AddAspNetCoreInstrumentation() and .AddHttpClientInstrumentation() are the "magic" that injects the observation logic into your existing request and HTTP client pipelines. Finally, .AddOtlpExporter() tells OpenTelemetry where to send the collected data.
The problem OpenTelemetry solves is the manual, often inconsistent, and incomplete instrumentation of distributed systems. Before OpenTelemetry, you’d typically write custom code to log request start/end times, track dependencies, and correlate events across different services. This was tedious, error-prone, and difficult to maintain. OpenTelemetry provides a standardized, vendor-neutral way to automatically collect this telemetry, giving you visibility into request latency, error rates, and the flow of requests through your entire system.
Internally, the instrumentation libraries work by hooking into the existing .NET extensibility points. For ASP.NET Core, this means leveraging middleware. When a request comes in, the OpenTelemetry middleware starts a trace, records attributes about the request (like URL, method, status code), and then passes the request down the pipeline. When the request finishes, the middleware captures the end time and sends the trace. For HttpClient, it intercepts the request before it’s sent and the response after it’s received, performing similar start/end timing and attribute capture.
The exact levers you control are primarily through the AddAspNetCoreInstrumentation(), AddHttpClientInstrumentation(), and other Add...Instrumentation() methods. You can configure these to:
- Filter requests: Exclude certain paths or HTTP methods from being traced.
- Enrich spans: Add custom attributes to spans based on request content or application state.
- Control sampling: Decide how many traces to collect (e.g., 100% for debugging, a small percentage for production).
- Configure exporters: Choose where and how to send your telemetry data (OTLP, Jaeger, Prometheus, etc.).
A common point of confusion is how to exclude specific outgoing HTTP requests, like calls to health check endpoints or internal service discovery mechanisms, from being traced. You can achieve this by providing a predicate to the AddHttpClientInstrumentation configuration. For example, to exclude requests to /health:
.WithTracing(tracingBuilder => tracingBuilder
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation(options =>
{
options.FilterHttpRequestMessage = request =>
!request.RequestUri.ToString().Contains("/health");
})
.AddOtlpExporter(...))
This FilterHttpRequestMessage delegate allows you to inspect each outgoing HttpRequestMessage and decide whether to instrument it. If the delegate returns false, the request will not generate an outgoing span.
The next thing you’ll likely want to tackle is distributed tracing, specifically how to ensure traces are correctly propagated across service boundaries.