How to instrument ASP.NET Core with OpenTelemetry .NET

OpenTelemetry .NET instrumentation adds vendor-neutral traces and metrics to ASP.NET Core request handling. It is useful when a web service needs to send route names, status codes, and request-duration measurements to an OpenTelemetry Collector before a vendor backend is chosen.

The ASP.NET Core instrumentation package hooks into the hosting pipeline through AddAspNetCoreInstrumentation(). The OTLP exporter sends collected telemetry to the Collector on port 4317, and the Collector debug exporter prints the received spans and metric points so the first request can be inspected before connecting a production backend.

Local instrumentation testing needs a .NET SDK 8 or newer project and Docker for the Collector. The sample service name and route are neutral; replace serviceName and the exporter endpoint for the real service after the local trace and HTTP server metric appear in Collector output.

Steps to instrument ASP.NET Core with OpenTelemetry .NET:

  1. Create a minimal ASP.NET Core web project.
    $ dotnet new web -o aspnetcoreapp
    The template "ASP.NET Core Empty" was created successfully.
    ##### snipped #####
    Restore succeeded.
  2. Change into the project directory.
    $ cd aspnetcoreapp
  3. Add the OpenTelemetry hosting package.
    $ dotnet add package \
      OpenTelemetry.Extensions.Hosting
  4. Add the ASP.NET Core instrumentation package.
    $ dotnet add package \
      OpenTelemetry.Instrumentation.AspNetCore
  5. Add the OTLP exporter package.
    $ dotnet add package \
      OpenTelemetry.Exporter.OpenTelemetryProtocol
  6. Replace Program.cs with the instrumented request handler.
    Program.cs
    using OpenTelemetry.Metrics;
    using OpenTelemetry.Resources;
    using OpenTelemetry.Trace;
     
    const string serviceName = "orders-api";
     
    var builder = WebApplication.CreateBuilder(args);
     
    builder.Services.AddOpenTelemetry()
        .ConfigureResource(resource => resource.AddService(serviceName: serviceName))
        .WithTracing(tracing => tracing
            .AddAspNetCoreInstrumentation()
            .AddOtlpExporter())
        .WithMetrics(metrics => metrics
            .AddAspNetCoreInstrumentation()
            .AddOtlpExporter((exporterOptions, metricReaderOptions) =>
            {
                var readerOptions = metricReaderOptions.PeriodicExportingMetricReaderOptions;
                readerOptions.ExportIntervalMilliseconds = 1000;
            }));
     
    var app = builder.Build();
     
    app.MapGet("/orders/{id:int}", (int id) => Results.Ok(new { id, status = "accepted" }));
     
    app.Run();

    The one-second metric export interval keeps the local smoke test short. Use a normal export interval for service traffic after the first Collector check is complete.

  7. Create a local Collector configuration that accepts OTLP and prints telemetry through the debug exporter.
    collector-config.yaml
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    
    exporters:
      debug:
        verbosity: detailed
    
    service:
      pipelines:
        traces:
          receivers: [otlp]
          exporters: [debug]
        metrics:
          receivers: [otlp]
          exporters: [debug]

    Use debug for local inspection only because it writes telemetry attributes to container logs.
    Related: How to test OpenTelemetry Collector pipelines with the debug exporter
    Tool: OpenTelemetry Collector Config Generator

  8. Run the Collector in a separate terminal.
    $ docker run -d --rm --name otelcol-debug \
      -p 4317:4317 \
      -p 4318:4318 \
      -v "$PWD/collector-config.yaml:/etc/otelcol/config.yaml:ro" \
      otel/opentelemetry-collector:latest

    Keep this container running while the ASP.NET Core app sends telemetry.
    Related: How to run the OpenTelemetry Collector in Docker

  9. Set the local OTLP endpoint for the app shell.
    $ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
  10. Start the ASP.NET Core app.
    $ dotnet run --urls http://localhost:5080
    Building...
    info: Microsoft.Hosting.Lifetime[14]
          Now listening on: http://localhost:5080
    ##### snipped #####

    AddOtlpExporter() uses OTLP/gRPC by default, which matches the Collector listener on port 4317.

  11. Send one request to the instrumented route from another terminal.
    $ curl -s http://localhost:5080/orders/42
    {"id":42,"status":"accepted"}
  12. Check the Collector output for the ASP.NET Core trace and HTTP server metric.
    $ docker logs otelcol-debug
    Resource attributes:
         -> service.name: Str(orders-api)
         -> telemetry.sdk.language: Str(dotnet)
    InstrumentationScope Microsoft.AspNetCore
    Span #0
        Name           : GET /orders/{id:int}
        Kind           : Server
    Attributes:
         -> http.request.method: Str(GET)
         -> http.route: Str(/orders/{id:int})
         -> http.response.status_code: Int(200)
    ##### snipped #####
    Metric #1
    Descriptor:
         -> Name: http.server.request.duration

    The trace shows request instrumentation is active. The http.server.request.duration metric shows the ASP.NET Core server metric pipeline is also reaching the Collector.

  13. Stop the debug Collector after the local check is complete.
    $ docker stop otelcol-debug
    otelcol-debug