.NET SDK
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:
IChatClientfrom Microsoft.Extensions.AI (OpenAI, Azure OpenAI, and others)IAgentfrom 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
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:
# 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:
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():
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
| Property | Type | Default | Description |
|---|---|---|---|
| AppName | string | Agent | Application name shown in the platform |
| ApiKey | string | empty | Your API key (required) |
| Endpoint | string | https://collector.observability.progress.com:443 | Collector endpoint URL |
| RecordInputs | bool | true | Include request messages in spans |
| RecordOutputs | bool | true | Include response content in spans |
| Debug | bool | false | Enable debug logging |
| AdditionalAttributes | Dictionary<string, object> | empty | Custom attributes on all spans |
| AdditionalTags | List | empty | Tags for filtering (max 200 chars) |
Environment Variables
PowerShell:
# 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 / 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"
| Variable | Used when |
|---|---|
| PROGRESS__OBSERVABILITY__APPNAME | AppName is not set in code |
| PROGRESS__OBSERVABILITY__APIKEY | ApiKey is not set in code |
| PROGRESS__OBSERVABILITY__ENDPOINT | Endpoint 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:
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:
// 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
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:
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:
// 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:
// 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:
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.