.NET: [Breaking Change] Auto-wire ChatClient with OpenTelemetryChatClient in OpenTelemetryAgent#5750
Merged
Merged
Conversation
Copilot
AI
changed the title
[WIP] Fix MAF telemetry flow by default
.NET: Auto-wire ChatClient with OpenTelemetryChatClient in OpenTelemetryAgent
May 11, 2026
westey-m
reviewed
May 13, 2026
…tryAgent Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/96dd033a-0c48-4d3f-9148-324bfd436b75 Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
…tAsIs; drop redundant check Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/6ac3f75d-eeb7-4811-8043-9a27511b0a8b Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
…lient Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/008d914d-8cbb-4e9f-81b6-f8c3c8bd8d04 Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
…eName) signature Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/a890c9a7-0b77-40ab-802c-dfbf09f8c260 Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
…r factory Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/3afbf18c-de22-4236-a2f2-02ca1e98ae21 Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
…g path coverage Normalize the configured source name once in the constructor so the outer OpenTelemetryChatClient and the auto-wired inner OpenTelemetryChatClient always emit spans on the same ActivitySource. A caller passing an empty string previously produced agent-level spans on DefaultSourceName but auto-wired chat spans on the empty source, causing the chat spans to be silently dropped by exporters subscribed to the default source. Tests added to cover the previously unexercised OTEL wiring branches: - Ctor_NullOrEmptySourceName_AutoWiredChatClientUsesDefaultSource_Async (Theory: null and empty) - AutoWireChatClient_PlainAgentRunOptions_PreservesContinuationToken_Async - AutoWireChatClient_ChatClientAgentRunOptions_NoUserFactory_PreservesChatOptions_Async - AutoWireChatClient_StreamingDisabled_DoesNotEmitChatSpan_Async
Annotate the new 3-arg OpenTelemetryAgent(AIAgent, string?, bool) constructor with [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] (MAAI001) so callers must explicitly opt in to the auto-wire toggle. The original 2-arg constructor stays non-experimental and delegates with autoWireChatClient: true; the delegating call is locally suppressed so the existing source compatibility surface is preserved.
- Use string.IsNullOrWhiteSpace (not IsNullOrEmpty) when normalizing the constructor sourceName, so callers passing whitespace-only strings still land on OpenTelemetryConsts.DefaultSourceName instead of an unsubscribed ActivitySource.
- Fix the misleading pragma comment on the 2-arg ctor delegating call: auto-wiring is the new default, it does not preserve the original (pre-PR) behavior.
- Expand the GetRunOptionsWithChatClientWiring XML doc to spell out that a base AgentRunOptions (not ChatClientAgentRunOptions) is also accepted: it is converted to ChatClientAgentRunOptions with the auto-wire factory installed and base properties copied.
- Tests: extend the source-name normalization Theory with whitespace cases (' ' and '\t'); add end-to-end coverage for plain AgentRunOptions over a real ChatClientAgent (sync + streaming) asserting the inner chat client is invoked and both invoke_agent + chat spans are emitted.
7ed8676 to
2848b66
Compare
westey-m
approved these changes
May 13, 2026
This was referenced May 14, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
OpenTelemetryAgentinstrumented only the agent-levelinvoke_agentspan; the underlyingIChatClientwas untouched, so model-level chat spans, usage metrics, and Azure Monitor traces never flowed unless callers manually wrapped every chat client. End result: telemetry silently absent in Foundry/hosted agent samples even with an exporter configured.Description
OpenTelemetryAgentnow auto-wraps the innerChatClientAgent'sIChatClientwithOpenTelemetryChatClienton each invocation, so chat-level telemetry flows alongsideinvoke_agent.OpenTelemetryAgentctors — the originalOpenTelemetryAgent(AIAgent innerAgent, string? sourceName = null)constructor signature is preserved (binary-compatible) and delegates with auto-wiring enabled. A new overloadOpenTelemetryAgent(AIAgent innerAgent, string? sourceName, bool autoWireChatClient)adds the opt-out. ThesourceNameis normalized once in the constructor (empty string is treated asOpenTelemetryConsts.DefaultSourceName) and reused by both the outerOpenTelemetryChatClientand the auto-wired innerOpenTelemetryChatClient, so agent-level and chat-level spans always land on the sameActivitySource. The opt-out flag lives only on the constructor (not on theAIAgentBuilderextension), keeping chat-client terminology out of the abstract builder surface.ForwardingChatClient.GetResponseAsync/GetStreamingResponseAsyncroute theAgentRunOptionsthrough a newGetRunOptionsWithChatClientWiringhelper that:ChatClientAgentviaInnerAgent.GetService<ChatClientAgent>()(no type assumption onInnerAgent; wrapping agents that surface a nestedChatClientAgentthroughGetServiceare supported). No-ops when the result isnull.chatClientAgent.GetService<ChatClientAgentOptions>()?.UseProvidedChatClientAsIs is true(respects the user's explicit opt-out on the chat client pipeline;ChatClientAgent.GetServicealready exposesChatClientAgentOptions).chatClientAgent.GetService<IChatClient>()?.GetService<OpenTelemetryChatClient>()is already non-null (avoids double-wrap).ChatClientAgentRunOptions(or, when the caller passes a plainAgentRunOptions, creates one and copies the base properties:ContinuationToken,AllowBackgroundResponses,AdditionalProperties,ResponseFormat) and setsChatClientFactoryto chain onto any user-supplied factory rather than replacing it. The factory step also inspects the post-user-factory result viaGetService(typeof(OpenTelemetryChatClient))and skips wrapping when the chat client is already instrumented, so a user factory that itself callsUseOpenTelemetry(...)does not produce duplicate chat spans.UseOpenTelemetryextension — unchanged public surface; the existing extension simply constructs anOpenTelemetryAgent(with auto-wiring on by default), so telemetry now flows automatically for existing call sites.GetRunOptionsWithChatClientWiringandWrapIfNeeded: default-on (sync + streaming), explicit opt-out via the constructor (sync + streaming),UseProvidedChatClientAsIsopt-out, non-ChatClientAgentno-op, no-double-wrap when the underlying chat client is pre-instrumented, chaining of a user-suppliedChatClientFactory, no-double-wrap when the user factory itself returns an OpenTelemetry-instrumented client, plainAgentRunOptionspropagation ofAllowBackgroundResponses/AdditionalProperties/ResponseFormatand (separately)ContinuationToken,ChatClientAgentRunOptionsclone path with no user factory (assertsChatOptionsare preserved and the caller's instance is not mutated), andnull/ emptysourceNamenormalization (Theory: both produce spans onOpenTelemetryConsts.DefaultSourceName).Behavioral breaking change
This PR introduces a behavioral (not API) change. Existing call sites of
new OpenTelemetryAgent(innerAgent)andAIAgentBuilder.UseOpenTelemetry(...)will now begin emitting an additional chat span per invocation when the inner agent is (or surfaces) aChatClientAgentwhoseIChatClientis not already wrapped withOpenTelemetryChatClient.Impact is limited to a specific subset of users:
new OpenTelemetryAgent(innerAgent, sourceName: null, autoWireChatClient: false)ChatClientAgentitself:new ChatClientAgent(chatClient, new ChatClientAgentOptions { UseProvidedChatClientAsIs = true })Source compatibility, binary compatibility, and the
UseOpenTelemetryextension surface are all preserved. Only the runtime telemetry shape changes, and only in the direction of emitting strictly more (and previously missing) signal.Opt-out (constructor-only)
Or via
ChatClientAgentOptions:Contribution Checklist