3 minute read

5 min read 1062 words

OpenTelemetry (OTel) has become the industry standard for observability, and if you’re building applications with ASP.NET Core, OpenTelemetry.Instrumentation.AspNetCore is your gateway to understanding how your services behave in production.

This instrumentation library automatically collects telemetry data from incoming HTTP requests, giving you deep visibility into performance, errors, and request flows without requiring you to manually wrap every controller action in a “try-catch-log” block.


1. Why Use OpenTelemetry?

Before we dive into the “how,” let’s talk about the “why.” Traditional logging tells you what happened, but Distributed Tracing (powered by OpenTelemetry) tells you where it happened across your entire system.

By using OpenTelemetry.Instrumentation.AspNetCore, you get:

  • Automatic Request Tracking: Every incoming HTTP request is automatically timed and recorded.
  • Standardized Metadata: Captures HTTP methods, status codes, and target URLs out of the box.
  • Context Propagation: Seamlessly connects traces across microservices.

2. Getting Started: Installation

To begin, you’ll need to install the core OpenTelemetry packages and the ASP.NET Core instrumentation library.

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Exporter.Console
  • OpenTelemetry.Extensions.Hosting: Provides the integration with the .NET Dependency Injection (DI) system.
  • OpenTelemetry.Instrumentation.AspNetCore: The “magic” that hooks into the ASP.NET Core pipeline.
  • OpenTelemetry.Exporter.Console: A simple exporter to see your traces in the terminal during development.

3. Basic Configuration

In your Program.cs, you can set up OpenTelemetry in just a few lines of code.

using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

// Configure OpenTelemetry
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService("MyAwesomeApi"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation() // <-- The magic happens here
        .AddConsoleExporter());

var app = builder.Build();

app.MapGet("/", () => "Hello, OpenTelemetry!");

app.Run();

With this setup, every time someone hits your API, a Span (a single operation in a trace) will be printed to your console, containing details like the duration, HTTP method, and status code.


4. Customizing Instrumentation

The default settings are great, but real-world apps often need more control. You can configure the instrumentation using AspNetCoreInstrumentationOptions.

Filtering Requests

You probably don’t want to trace every single health check or heartbeat request. You can filter them out easily:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.Filter = (httpContext) =>
            {
                // Exclude health checks from being traced
                return !httpContext.Request.Path.Value!.Contains("/health");
            };
        }));

Enriching Traces with Custom Data

Want to include the user’s ID or a specific header in your trace? Use the EnrichWithHttpRequest callback:

options.EnrichWithHttpRequest = (activity, request) =>
{
    activity.SetTag("http.client_ip", request.HttpContext.Connection.RemoteIpAddress);
    
    if (request.Headers.ContainsKey("X-Correlation-ID"))
    {
        activity.SetTag("correlation.id", request.Headers["X-Correlation-ID"].ToString());
    }
};

5. Capturing Errors and Exceptions

By default, OpenTelemetry captures the HTTP status code. If you want to include the full exception details (including stack traces) in your spans, enable RecordException:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
        }));

When an unhandled exception occurs, the span will be marked as Error, and the exception details will be attached as “Events” to the activity, making it much easier to debug production issues.


6. Advanced: Changing Span Names

Sometimes the default span name (which is usually the Route Template) isn’t descriptive enough. You can modify the activity name as it begins:

options.EnrichWithHttpRequest = (activity, request) =>
{
    // Example: Rename the span to include the host
    activity.DisplayName = $"{request.Method} {request.Host}{request.Path}";
};

7. Where Does the Data Go? (Exporters)

The Console Exporter is great for testing, but in production, you’ll want to send this data to a visualization tool like Jaeger, Honeycomb, or Azure Monitor.

The most common way to do this is via the OTLP (OpenTelemetry Protocol):

dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

Then update your configuration:

.WithTracing(tracing => tracing
    .AddAspNetCoreInstrumentation()
    .AddOtlpExporter(opt =>
    {
        opt.Endpoint = new Uri("http://localhost:4317");
    }));

8. Summary of Options

Option Purpose Default
Filter Decide which requests to ignore Trace everything
EnrichWithHttpRequest Add custom tags from the Request -
EnrichWithHttpResponse Add custom tags from the Response -
RecordException Include stack traces on failure false

9. Conclusion

OpenTelemetry.Instrumentation.AspNetCore is an essential component for any modern .NET application. It takes care of the “heavy lifting” of observability, allowing you to focus on building features while ensuring you have the data you need when things go wrong.


10. References & Further Reading

Leave a comment