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
9 changes: 5 additions & 4 deletions dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,26 +242,27 @@ 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)
{
// Don't mutate the caller's options; clone and chain any caller-provided factory.
// 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;
}

// For a plain AgentRunOptions (or null), create a ChatClientAgentRunOptions and preserve
// 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Activity>();
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<IEnumerable<ChatMessage>, ChatOptions?>? OnGetResponseAsync { get; set; }
Expand Down
Loading