.NET SDK

Updated on Aug 14, 2026

Package: Progress.Observability.Instrumentation

Overview

The .NET SDK instruments AI agents built with IChatClient from Microsoft.Extensions.AI or IAgent from the Microsoft Agent Framework. It integrates directly with the .NET Activity/OpenTelemetry pipeline.

Use this SDK when your agent is built in C# and uses:

  • IChatClient from Microsoft.Extensions.AI (OpenAI, Azure OpenAI, and others)
  • IAgent from the Microsoft Agent Framework
  • MCP tools via McpClientTool

Core capabilities:

  • Automatic tracing of LLM chat completions and streaming responses
  • Tool call instrumentation via AddToolObservability()
  • Custom spans with ObservabilityActivitySource
  • Tag support for filtering in the platform
  • Environment variable overrides for deployment flexibility

Unlike the Python SDK, the .NET SDK does not auto-instrument third-party libraries. Instead, you explicitly wrap your IChatClient or IAgent with observability.

Installation

bash
dotnet add <YourProject.csproj> package Progress.Observability.Instrumentation

The package depends on OpenTelemetry NuGet packages. A project pinned to an older OpenTelemetry version fails to restore with NU1605: Detected package downgrade — raise the project's OpenTelemetry reference first.

From a local NuGet source:

bash
# Copy the .nupkg file to your local source folder (for example, c:\packages)
dotnet nuget add source c:\packages --name LocalFeed

# Verify the source exists
dotnet nuget list source

# Add the package from the local source
dotnet add package Progress.Observability.Instrumentation --source LocalFeed

Initialization

The initialization approach depends on whether you use IChatClient or IAgent.

IChatClient

Call AddObservability() on your chat client:

cs
using Microsoft.Extensions.AI;
using Progress.Observability.Extensions.AI;

IChatClient chatClient = new OpenAI.Chat.ChatClient("gpt-4.1-mini", openAIApiKey)
    .AsIChatClient();

// Add Progress Observability for LLM calls
chatClient = chatClient.AddObservability(options => {
    options.AppName = "My Agent";
    options.ApiKey = "ac_p_001_.....";
});

IAgent

Call ObservabilityTracer.Initialize() and then UseOpenTelemetry():

cs
using Progress.Observability.Extensions.AI;

var agentName = "Awesome AI Agent";

// Initialize the observability tracer
ObservabilityTracer.Initialize(new ObservabilityOptions()
{
    AppName = agentName,
    ApiKey = "ac_p_001_.....",
    AdditionalAttributes = new Dictionary<string, object>()
    {
        { "customer.id", Guid.NewGuid().ToString() },
        { "customer.quota", 2026 }
    },
    AdditionalTags = new List<string>()
    {
        "customer.id:12345",
        "NewCustomer"
    }
});

// Create tools (optionally add tool observability)
List<AITool> tools = [AIFunctionFactory.Create(MyToolFunction)];

// Build the agent with OpenTelemetry
var aiAgent = new AzureOpenAIClient(new(endpoint), new AzureKeyCredential(key))
    .GetChatClient("gpt-4.1")
    .AsAIAgent(
        instructions: "You are a helpful assistant.",
        name: agentName,
        tools: tools)
    .AsBuilder()
    .UseOpenTelemetry(
        sourceName: ObservabilityTracer.SourceName,
        configure: agent => {
            agent.EnableSensitiveData = true;
        })
    .Build();

Configuration

Options

PropertyTypeDefaultDescription
AppNamestringAgentApplication name shown in the platform
ApiKeystringemptyYour API key (required)
Endpointstringhttps://collector.observability.progress.com:443Collector endpoint URL
RecordInputsbooltrueInclude request messages in spans
RecordOutputsbooltrueInclude response content in spans
DebugboolfalseEnable debug logging
AdditionalAttributesDictionary<string, object>emptyCustom attributes on all spans
AdditionalTagsListemptyTags for filtering (max 200 chars)

Environment Variables

PowerShell:

bash
# PowerShell
$env:PROGRESS__OBSERVABILITY__APPNAME="Weather Agent"
$env:PROGRESS__OBSERVABILITY__APIKEY="ac_p_001_....."
$env:PROGRESS__OBSERVABILITY__ENDPOINT="https://collector.observability.progress.com:443"

Bash:

bash
# Bash / macOS / Linux
export PROGRESS__OBSERVABILITY__APPNAME="Weather Agent"
export PROGRESS__OBSERVABILITY__APIKEY="ac_p_001_....."
export PROGRESS__OBSERVABILITY__ENDPOINT="https://collector.observability.progress.com:443"
VariableUsed when
PROGRESS__OBSERVABILITY__APPNAMEAppName is not set in code
PROGRESS__OBSERVABILITY__APIKEYApiKey is not set in code
PROGRESS__OBSERVABILITY__ENDPOINTEndpoint is not set in code

The double-underscore (__) separator follows the standard .NET configuration binding convention. Do not use single underscores.

Explicit ObservabilityOptions properties take precedence; the environment variables are a fallback for values not set in code. (This differs from the Python SDK, where environment variables override code.)

IChatClient Usage

Full setup with tools and all configuration options:

cs
using Microsoft.Extensions.AI;
using Progress.Observability.Extensions.AI;

IChatClient chatClient = new OpenAI.Chat.ChatClient("gpt-4.1-mini", openAIApiKey)
    .AsIChatClient();

chatClient = chatClient.AddObservability(options => {
    options.AppName = "Weather Agent";
    options.ApiKey = "ac_p_001_.....";
    options.Endpoint = "https://collector.observability.progress.com:443";
    options.RecordInputs = true;    // include request messages (default: true)
    options.RecordOutputs = true;   // include response content (default: true)
    options.Debug = false;          // enable debug logging (default: false)
    options.AdditionalAttributes = new Dictionary<string, object>
    {
        { "project.id", 2182374 },
        { "environment", "dev" }
    };
    options.AdditionalTags = new List<string>
    {
        "customer.id:12345",
        "NewCustomer"
    };
});

// Enable function invocation wrapping
chatClient = new ChatClientBuilder(chatClient)
    .UseFunctionInvocation()
    .Build();

Tool Observability

Tool spans are captured automatically when UseFunctionInvocation() (or an agent built with AsAIAgent(tools: ...)) drives the calls and .AddObservability() wraps the chat client. AddToolObservability() exists for apps that dispatch tools themselves, outside that middleware:

cs
// Set up tools and enable tool observability
IList<McpClientTool> tools = await mcpClient.ListToolsAsync();
ChatOptions options = new() { Tools = [..tools] };

// Add Progress Observability for tool calls
options.AddToolObservability();

var response = await chatClient.GetResponseAsync(
    "What is the weather in Paris?", options
);

This wraps each AIFunction tool so every invocation creates a span with the tool name, input arguments, and output result. It is idempotent — already-instrumented tools are left alone — so it is safe on a list you do not fully control, but with function-invocation middleware in the chain it is redundant.

IAgent Usage

cs
using Progress.Observability.Extensions.AI;

var agentName = "Awesome AI Agent";

// Initialize the observability tracer
ObservabilityTracer.Initialize(new ObservabilityOptions()
{
    AppName = agentName,
    ApiKey = "ac_p_001_.....",
    AdditionalAttributes = new Dictionary<string, object>()
    {
        { "customer.id", Guid.NewGuid().ToString() },
        { "customer.quota", 2026 }
    },
    AdditionalTags = new List<string>()
    {
        "customer.id:12345",
        "NewCustomer"
    }
});

// Create tools (optionally add tool observability)
List<AITool> tools = [AIFunctionFactory.Create(MyToolFunction)];

// Build the agent with OpenTelemetry
var aiAgent = new AzureOpenAIClient(new(endpoint), new AzureKeyCredential(key))
    .GetChatClient("gpt-4.1")
    .AsAIAgent(
        instructions: "You are a helpful assistant.",
        name: agentName,
        tools: tools)
    .AsBuilder()
    .UseOpenTelemetry(
        sourceName: ObservabilityTracer.SourceName,
        configure: agent => {
            agent.EnableSensitiveData = true;
        })
    .Build();

Custom Spans

Use ObservabilityActivitySource to create your own spans for operations that are not automatically instrumented:

cs
using Progress.Observability.Extensions.AI;

// Create custom activities for fine-grained observability
using (var activity = ObservabilityActivitySource.Instance.StartActivity("weather agent call"))
{
    // Add custom attributes visible in the dashboard
    activity?.SetTag("date", DateTime.Now.ToString("yyyy-MM-dd"));
    activity?.SetTag("region", "europe");

    var response = await chatClient.GetResponseAsync(
        "What is the weather in Paris?", options
    );
}

Custom spans appear in the trace tree alongside auto-instrumented LLM and tool spans.

Tags

Tags are set during initialization and apply to all spans:

cs
// Tags set during initialization apply to all spans
ObservabilityTracer.Initialize(new ObservabilityOptions()
{
    AppName = "My Agent",
    ApiKey = "ac_p_001_.....",
    AdditionalTags = new List<string>()
    {
        "environment:production",
        "team:platform"
    }
});

Tags are sanitized automatically: the SDK removes empty strings and trims each tag to a maximum of 200 characters.

The .NET SDK does not currently support scoped tag propagation like Python's propagate_attributes. Tags must be set globally at initialization time.

Shutdown

Always call Shutdown() before your process exits to flush pending telemetry:

cs
// Always call Shutdown() before exiting to flush pending telemetry
try
{
    // Run your agent
    await RunAgent();
}
finally
{
    ObservabilityTracer.Shutdown();
}

For IChatClient usage, the SDK manages the tracer lifecycle internally, but call Shutdown() explicitly to ensure all spans are sent.

Hosted Apps (ASP.NET, Generic Host, Workers)

In a dependency-injection app all three touch points move:

cs
ObservabilityTracer.Initialize(new ObservabilityOptions
{
    AppName = appName,
    ApiKey = Environment.GetEnvironmentVariable("OBSERVABILITY_API_KEY")!
});

var builder = Host.CreateApplicationBuilder(args);

builder.Services
    .AddChatClient(sp => sp.GetRequiredService<OpenAIClient>()
        .GetChatClient(model).AsIChatClient()
        .AddObservability()) // inside the factory, on the IChatClient
    .UseFunctionInvocation();

var host = builder.Build();
host.Services.GetRequiredService<IHostApplicationLifetime>()
    .ApplicationStopping.Register(ObservabilityTracer.Shutdown);

Initialize() runs before Build(), so the tracer is live before DI resolves any registered client.

.AddObservability() goes inside the AddChatClient factory. It is an extension on IChatClient, not on ChatClientBuilder, so chaining it after .UseFunctionInvocation() is a compile error (CS1929). Inside the factory it captures LLM calls and the tool calls the middleware makes.

Shutdown() registers on ApplicationStopping — a hosted app has no console try/finally to flush from.

Using Progress Alongside Existing OpenTelemetry

Initialization order does not matter in .NET: Initialize() builds its own TracerProvider, and one the app built with Sdk.CreateTracerProviderBuilder() keeps working either way. Two things to know:

Do not add .AddObservability() to a chat client that already has .UseOpenTelemetry() — both layers record every call, doubling token and cost figures. Keep one, or expect doubled numbers.

Progress spans appear in their own trace rather than nesting under the app's activities. This is by design. If you want Progress spans in your own backend too, subscribe your provider to the Progress.Observability.AgentMonitoring source.

See Also