diff --git a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs index cffde717e4..85ae97e1de 100644 --- a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs @@ -242,10 +242,11 @@ public ForwardedOptions(AgentRunOptions? options, AgentSession? session, Activit } string sourceName = this._sourceName; - static IChatClient WrapIfNeeded(IChatClient cc, string sourceName) => + bool enableSensitiveData = this.EnableSensitiveData; + static IChatClient WrapIfNeeded(IChatClient cc, string sourceName, bool enableSensitiveData) => cc.GetService(typeof(OpenTelemetryChatClient)) is not null ? cc - : cc.AsBuilder().UseOpenTelemetry(sourceName: sourceName).Build(); + : cc.AsBuilder().UseOpenTelemetry(sourceName: sourceName, configure: o => o.EnableSensitiveData = enableSensitiveData).Build(); if (options is ChatClientAgentRunOptions ccOptions) { @@ -253,7 +254,7 @@ static IChatClient WrapIfNeeded(IChatClient cc, string sourceName) => // If the user factory already returns an OpenTelemetry-instrumented client, don't double-wrap. var clone = (ChatClientAgentRunOptions)ccOptions.Clone(); var userFactory = clone.ChatClientFactory; - clone.ChatClientFactory = cc => WrapIfNeeded(userFactory is null ? cc : userFactory(cc), sourceName); + clone.ChatClientFactory = cc => WrapIfNeeded(userFactory is null ? cc : userFactory(cc), sourceName, enableSensitiveData); return clone; } @@ -261,7 +262,7 @@ static IChatClient WrapIfNeeded(IChatClient cc, string sourceName) => // any base AgentRunOptions properties from the caller so they reach the inner agent. var newOptions = new ChatClientAgentRunOptions { - ChatClientFactory = cc => WrapIfNeeded(cc, sourceName), + ChatClientFactory = cc => WrapIfNeeded(cc, sourceName, enableSensitiveData), }; if (options is not null) diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs index 1f04d09ba3..9717c6157b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs @@ -1054,6 +1054,61 @@ public async Task AutoWireChatClient_PlainAgentRunOptions_RealChatClientAgent_St Assert.Contains(activities, a => string.Equals(a.GetTagItem("gen_ai.operation.name") as string, "chat", StringComparison.Ordinal)); } + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task AutoWireChatClient_EnableSensitiveData_PropagatedToInnerChatClient_Async(bool enableSensitiveData, bool streaming) + { + // Regression test for: when EnableSensitiveData is set on OpenTelemetryAgent, the auto-wired + // inner OpenTelemetryChatClient must also have EnableSensitiveData propagated to it. Previously, + // GetRunOptionsWithChatClientWiring created the inner client without passing EnableSensitiveData, + // so the inner chat span would never emit gen_ai.input.messages / gen_ai.output.messages even + // when the caller explicitly set EnableSensitiveData = true. + var sourceName = Guid.NewGuid().ToString(); + var activities = new List(); + using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() + .AddSource(sourceName) + .AddInMemoryExporter(activities) + .Build(); + + var fakeChatClient = new AutoWireTestChatClient(); + var inner = new ChatClientAgent(fakeChatClient); + using var agent = new OpenTelemetryAgent(inner, sourceName) { EnableSensitiveData = enableSensitiveData }; + + if (streaming) + { + await foreach (var _ in agent.RunStreamingAsync([new ChatMessage(ChatRole.User, "hello")])) + { + } + } + else + { + _ = await agent.RunAsync([new ChatMessage(ChatRole.User, "hello")]); + } + + // There should be 2 activities: the invoke_agent span and the inner chat span. + Assert.Equal(2, activities.Count); + + var chatSpan = activities.Single(a => string.Equals(a.GetTagItem("gen_ai.operation.name") as string, "chat", StringComparison.Ordinal)); + var chatTags = chatSpan.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + if (enableSensitiveData) + { + // When EnableSensitiveData=true on the outer agent, the auto-wired inner client must also + // capture message content in the chat span. + Assert.True(chatTags.ContainsKey("gen_ai.input.messages"), "gen_ai.input.messages must be present in the inner chat span when EnableSensitiveData=true"); + Assert.True(chatTags.ContainsKey("gen_ai.output.messages"), "gen_ai.output.messages must be present in the inner chat span when EnableSensitiveData=true"); + } + else + { + // By default (EnableSensitiveData=false) message content must NOT be captured. + Assert.False(chatTags.ContainsKey("gen_ai.input.messages"), "gen_ai.input.messages must NOT be present in the inner chat span when EnableSensitiveData=false"); + Assert.False(chatTags.ContainsKey("gen_ai.output.messages"), "gen_ai.output.messages must NOT be present in the inner chat span when EnableSensitiveData=false"); + } + } + private sealed class AutoWireTestChatClient : IChatClient { public Action, ChatOptions?>? OnGetResponseAsync { get; set; }