Snippet

OpenTelemetry and Azure Monitor setup for an agent API

A complete, runnable ASP.NET Core 10 program: W3C tracing, the agent's spans, trusted baggage, whole-trace sampling, and the trace ID returned to the caller.

csharpOct 6, 2026MIT

AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy

What it does#

Sets up OpenTelemetry tracing for an ASP.NET Core API that hosts an agent: incoming requests, outgoing HTTP calls (including the model call), and the agent’s own spans, exported to Application Insights. It generates one server-side conversation ID, sets it as trusted baggage for child span tags, and returns the trace ID with the answer, so anyone can find the trace later. Outbound HTTP propagation uses W3C trace context only, not caller or server baggage.

You need#

  • .NET 10 SDK
  • dotnet add package Azure.Monitor.OpenTelemetry.AspNetCore --version 1.6.0
  • ConversationBaggageProcessor.cs from Copy only trusted baggage onto spans
  • An APPLICATIONINSIGHTS_CONNECTION_STRING setting (environment variable or app settings)
  • When you build your agent with the Microsoft Agent Framework, call .UseOpenTelemetry(sourceName: "my-agent-api") with the same name as ServiceName here
  • ConversationBaggageProcessor copies the server-generated conversation ID onto child spans; the middleware below clears caller baggage at the request boundary before any outbound HTTP call

Where it goes#

This is a complete Program.cs for a new web project (dotnet new web). The agent call is the one line you replace.

Program.cs
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Sarang Datye
using System.Diagnostics;
using Azure.Monitor.OpenTelemetry.AspNetCore;
using OpenTelemetry;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

const string ServiceName = "my-agent-api"; // also pass this as sourceName to your agent's UseOpenTelemetry

var builder = WebApplication.CreateBuilder(args);

// Use W3C trace context extraction. The request middleware below clears caller baggage before
// any outbound call can happen.
Sdk.SetDefaultTextMapPropagator(new TraceContextPropagator());

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSource(ServiceName)
        .AddProcessor(new ConversationBaggageProcessor()))
    .UseAzureMonitor(options =>
    {
        options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
        // Trace-ID-based sampling keeps whole traces together. 1.0 keeps everything while you
        // commission; lower it later, and use the same ratio in every service.
        options.SamplingRatio = 1.0f;
        // Turn off the default per-process rate limit so SamplingRatio alone decides.
        options.TracesPerSecond = null;
    })
    // Registered after the Azure Monitor distribution so this service name overrides the
    // Container Apps resource detector, which otherwise sets service.name to the app name.
    .ConfigureResource(resource => resource.AddService(serviceName: ServiceName, serviceVersion: "1.0.0"));

var app = builder.Build();

app.Use(async (context, next) =>
{
    if (Activity.Current is { } activity)
    {
        foreach (var item in activity.Baggage.ToArray())
        {
            activity.SetBaggage(item.Key, null);
        }
    }

    await next(context);
});

app.MapPost("/api/chat", (ChatRequest request) =>
{
    var conversationId = Guid.NewGuid().ToString("N");

    // Set the only baggage you trust, before calling the agent. It is generated by this API,
    // not copied from the caller, after the middleware clears inbound baggage.
    Activity.Current?.SetBaggage(ConversationBaggageProcessor.BaggageKey, conversationId);

    // ... run your agent here ...
    return Results.Ok(new { conversationId, traceId = Activity.Current?.TraceId.ToString() });
});

app.Run();

public sealed record ChatRequest(string Message);

Check that it works#

Run it with dotnet run, then send a request with a traceparent header to the URL it prints:

Terminal
curl -s -X POST <your-url>/api/chat \
  -H 'Content-Type: application/json' \
  -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
  -d '{"message":"hi"}'

The response’s traceId is 4bf92f3577b34da6a3ce929d0e0e4736: your API continued the caller’s trace instead of starting a new one.