Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions docs/decisions/0003-agent-opentelemetry-instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,21 +124,42 @@ The implementation is validated through:

### Usage Example

The name passed to `TracerProviderBuilder.AddSource` must match the source name the agent emits under, otherwise the
provider silently receives no agent spans. When no source name is supplied to `UseOpenTelemetry`, the agent emits under
`OpenTelemetryAgent.DefaultSourceName`, so that is the value to register:

```csharp
// Create TracerProvider
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(AgentOpenTelemetryConsts.DefaultSourceName)
.AddSource(OpenTelemetryAgent.DefaultSourceName)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we clarify that AddSource and UseOpenTelemetry must use the same source name? The default pairing is AddSource(OpenTelemetryAgent.DefaultSourceName) with UseOpenTelemetry(). For a custom source, both calls need the same value:

const string sourceName = "MyCompany.MyAgent";

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(sourceName)
    .AddConsoleExporter()
    .Build();

using var telemetryAgent = baseAgent.AsBuilder()
    .UseOpenTelemetry(sourceName: sourceName)
    .Build();

Without matching them, provider silently misses agent spans.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 2a44f9f. The usage section now states up front that the name passed to AddSource must match the source name the agent emits under, otherwise the provider silently receives no agent spans, followed by the default pairing and then your custom-source example with the same value on both calls.

One extra change in that snippet: the existing example wrapped the agent with baseAgent.WithOpenTelemetry(), but that extension only exists for WorkflowBuilder, not for AIAgent, so the snippet did not compile. Both examples now use baseAgent.AsBuilder().UseOpenTelemetry(...).Build().

.AddConsoleExporter()
.Build();

// Create and wrap agent with telemetry
var baseAgent = new ChatClientAgent(chatClient, options);
using var telemetryAgent = baseAgent.WithOpenTelemetry();
using var telemetryAgent = baseAgent.AsBuilder()
.UseOpenTelemetry()
.Build();

// Use agent normally - telemetry is captured automatically
var response = await telemetryAgent.RunAsync(messages);
```

To emit under a custom source name, pass the same value to both calls:

```csharp
const string SourceName = "MyCompany.MyAgent";

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(SourceName)
.AddConsoleExporter()
.Build();

using var telemetryAgent = baseAgent.AsBuilder()
.UseOpenTelemetry(sourceName: SourceName)
.Build();
```

### Relationship to Microsoft.Extensions.AI

This implementation follows the exact patterns established by Microsoft.Extensions.AI's OpenTelemetry instrumentation, ensuring consistency across the AI ecosystem and leveraging proven patterns for telemetry integration.
22 changes: 18 additions & 4 deletions dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
// inner agent not directly but rather via OpenTelemetryChatClient, which wraps a ForwardingChatClient that in turn
// calls back into the inner agent.

/// <summary>
/// Gets the default <see cref="ActivitySource"/> name used by <see cref="OpenTelemetryAgent"/> when no source
/// name is supplied to the constructor.
/// </summary>
/// <remarks>
/// Pass this value to the tracing pipeline (for example, <c>TracerProviderBuilder.AddSource</c>) to subscribe to
/// the spans emitted by agents that use the default source name, instead of hardcoding the literal name. This is
/// a property rather than a constant so that the value is read at run time: a consumer that upgrades the package
/// picks up the current source name without recompiling.
/// </remarks>
public static string DefaultSourceName => OpenTelemetryConsts.DefaultSourceName;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also update OpenTelemetryAgentBuilderExtensions.UseOpenTelemetry XML docs? Suggested wording for sourceName:

/// <param name="sourceName">
/// An optional source name used to identify telemetry data from this agent.
/// When specified, register the same value with <c>TracerProviderBuilder.AddSource</c> so the tracing pipeline subscribes to these spans.
/// When omitted, <see cref="OpenTelemetryAgent.DefaultSourceName"/> is used.
/// </param>

This makes the required pairing visible at the API call site and prevents silent loss of custom-source spans.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2a44f9f, using your wording for OpenTelemetryAgentBuilderExtensions.UseOpenTelemetry. I applied the same wording to the sourceName parameter on both OpenTelemetryAgent constructors, since they are the other public entry points that take a source name and carried the identical vague text. Let me know if you would rather keep that to the builder extension only.


/// <summary>The <see cref="OpenTelemetryChatClient"/> providing the bulk of the telemetry.</summary>
private readonly OpenTelemetryChatClient _otelClient;
/// <summary>The provider name extracted from <see cref="AIAgentMetadata"/>.</summary>
Expand All @@ -62,8 +74,9 @@ public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
/// <summary>Initializes a new instance of the <see cref="OpenTelemetryAgent"/> class.</summary>
/// <param name="innerAgent">The underlying <see cref="AIAgent"/> to be augmented with telemetry capabilities.</param>
/// <param name="sourceName">
/// An optional source name that will be used to identify telemetry data from this agent.
/// If not provided, a default source name will be used for telemetry identification.
/// An optional source name used to identify telemetry data from this agent.
/// When specified, register the same value with <c>TracerProviderBuilder.AddSource</c> so the tracing pipeline
/// subscribes to these spans. When omitted, <see cref="DefaultSourceName"/> is used.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="innerAgent"/> is <see langword="null"/>.</exception>
/// <remarks>
Expand All @@ -80,8 +93,9 @@ public OpenTelemetryAgent(AIAgent innerAgent, string? sourceName = null)
/// <summary>Initializes a new instance of the <see cref="OpenTelemetryAgent"/> class.</summary>
/// <param name="innerAgent">The underlying <see cref="AIAgent"/> to be augmented with telemetry capabilities.</param>
/// <param name="sourceName">
/// An optional source name that will be used to identify telemetry data from this agent.
/// If not provided, a default source name will be used for telemetry identification.
/// An optional source name used to identify telemetry data from this agent.
/// When specified, register the same value with <c>TracerProviderBuilder.AddSource</c> so the tracing pipeline
/// subscribes to these spans. When omitted, <see cref="DefaultSourceName"/> is used.
/// </param>
/// <param name="autoWireChatClient">
/// When <see langword="true"/> and the inner agent is a <see cref="ChatClientAgent"/>, the underlying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ public static class OpenTelemetryAgentBuilderExtensions
/// </summary>
/// <param name="builder">The <see cref="AIAgentBuilder"/> to which OpenTelemetry support will be added.</param>
/// <param name="sourceName">
/// An optional source name that will be used to identify telemetry data from this agent.
/// If not specified, a default source name will be used.
/// An optional source name used to identify telemetry data from this agent.
/// When specified, register the same value with <c>TracerProviderBuilder.AddSource</c> so the tracing pipeline
/// subscribes to these spans. When omitted, <see cref="OpenTelemetryAgent.DefaultSourceName"/> is used.
/// </param>
/// <param name="configure">
/// An optional callback that provides additional configuration of the <see cref="OpenTelemetryAgent"/> instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,17 @@ public async Task AutoWireChatClient_UserFactoryAddsOwnOTel_CoexistsWithBelowFic
Assert.Equal(2, activities.Count(a => string.Equals(a.GetTagItem("gen_ai.operation.name") as string, "chat", StringComparison.Ordinal)));
}

[Fact]
public void DefaultSourceName_ReturnsDocumentedSourceName()
{
// Callers pass this to TracerProviderBuilder.AddSource, so it must stay in sync with the source name the
// agent emits spans under, which Ctor_NullOrWhitespaceSourceName_AutoWiredChatClientUsesDefaultSource_Async
// pins to the same literal. Comparing against the literal here guards a rename of the internal constant.

// Arrange & Act & Assert
Assert.Equal("Experimental.Microsoft.Agents.AI", OpenTelemetryAgent.DefaultSourceName);
}

[Theory]
[InlineData(null)]
[InlineData("")]
Expand Down
Loading