Skip to content
Merged
26 changes: 24 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,43 @@ 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. That property is marked
`[Experimental("MAAI001")]`; suppress that diagnostic in your csproj to use it.

```csharp
// Create TracerProvider
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(AgentOpenTelemetryConsts.DefaultSourceName)
.AddSource(OpenTelemetryAgent.DefaultSourceName)
Comment thread
rogerbarreto marked this conversation as resolved.
.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.
23 changes: 19 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,19 @@ 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>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public static string DefaultSourceName => OpenTelemetryConsts.DefaultSourceName;
Comment thread
rogerbarreto marked this conversation as resolved.
Comment thread
YashvantHange marked this conversation as resolved.

/// <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 +75,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 +94,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 @@ -7,5 +7,6 @@
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.ScanContent(string! fileName, string! content, System.Text.RegularExpressions.Regex! regex) -> Microsoft.Agents.AI.FileSearchResult?
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.SplitLines(string! content) -> System.Collections.Generic.IReadOnlyList<string!>!
[MAAI001]static Microsoft.Agents.AI.OpenTelemetryAgent.DefaultSourceName.get -> string!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.FindMatchingFilesAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<string!>!>!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.ScanContent(string! fileName, string! content, System.Text.RegularExpressions.Regex! regex) -> Microsoft.Agents.AI.FileSearchResult?
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.SplitLines(string! content) -> System.Collections.Generic.IReadOnlyList<string!>!
[MAAI001]static Microsoft.Agents.AI.OpenTelemetryAgent.DefaultSourceName.get -> string!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.FindMatchingFilesAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<string!>!>!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.ScanContent(string! fileName, string! content, System.Text.RegularExpressions.Regex! regex) -> Microsoft.Agents.AI.FileSearchResult?
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.SplitLines(string! content) -> System.Collections.Generic.IReadOnlyList<string!>!
[MAAI001]static Microsoft.Agents.AI.OpenTelemetryAgent.DefaultSourceName.get -> string!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.FindMatchingFilesAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<string!>!>!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.ScanContent(string! fileName, string! content, System.Text.RegularExpressions.Regex! regex) -> Microsoft.Agents.AI.FileSearchResult?
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.SplitLines(string! content) -> System.Collections.Generic.IReadOnlyList<string!>!
[MAAI001]static Microsoft.Agents.AI.OpenTelemetryAgent.DefaultSourceName.get -> string!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.FindMatchingFilesAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<string!>!>!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.ScanContent(string! fileName, string! content, System.Text.RegularExpressions.Regex! regex) -> Microsoft.Agents.AI.FileSearchResult?
[MAAI001]static Microsoft.Agents.AI.AgentFileStore.SplitLines(string! content) -> System.Collections.Generic.IReadOnlyList<string!>!
[MAAI001]static Microsoft.Agents.AI.OpenTelemetryAgent.DefaultSourceName.get -> string!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.FindMatchingFilesAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<string!>!>!
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
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