Description
OpenTelemetryChatClient.GetStreamingResponseAsync contains the following workaround for dotnet/runtime#47802:
using Activity? activity = CreateAndConfigureActivity(options);
// ... streaming ...
Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
When activity is null, this actively sets Activity.Current = null after each yield return, destroying trace context for all downstream middleware in the streaming pipeline.
Root Cause
The workaround sets Activity.Current unconditionally. However, CreateAndConfigureActivity only creates an Activity when _activitySource.HasListeners() is true:
private Activity? CreateAndConfigureActivity(ChatOptions? options)
{
Activity? activity = null;
if (_activitySource.HasListeners())
{
// activity is only set if there are listeners
}
return activity;
}
This means in any environment where no listener is attached to the ActivitySource, activity is always null, and the workaround unconditionally sets Activity.Current = null on every yield return throughout the entire stream — regardless of what Activity.Current was before entering the middleware.
Relation to Existing Issues
This is the same class of bug reported and fixed for FunctionInvokingChatClient in #7320 / #7321. The fix in PR #7321 was not applied to OpenTelemetryChatClient.cs.
Additional Notes
Specifying a sourceName masks the bug
The bug can be masked by specifying a sourceName that has active listeners, causing HasListeners() to return true and activity to be non-null:
new ChatClientBuilder(inner)
.UseOpenTelemetry(sourceName: "my-source") // masks the bug if "my-source" has listeners
.Build();
This makes the bug environment-dependent and difficult to diagnose, as it may not reproduce in environments where OpenTelemetry is fully configured with a matching source name, but will silently destroy trace context in environments where it is not.
Downstream middleware impact
Any middleware registered after OpenTelemetryChatClient in the pipeline that relies on Activity.Current for logging or tracing will silently lose trace context on every streaming response. This includes scenarios such as:
- Completion logging middleware reading
Activity.Current?.DisplayName for operation names
- Agent tool call chains where trace context must be preserved across multiple streaming round-trips
Proposed Fix
The workaround should be guarded to only restore Activity.Current when an activity was actually started:
// Before — unconditionally sets Activity.Current = null when activity is null
Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
// After — only restores when an activity was actually started
if (activity is not null)
{
Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
}
This ensures Activity.Current is never actively destroyed, regardless of listener configuration or source name.
Expected Behavior
Activity.Current should be preserved across yield points in GetStreamingResponseAsync regardless of whether _activitySource.HasListeners() is true or false, consistent with the fix applied to FunctionInvokingChatClient in #7321.
Actual Behavior
In any streaming scenario where _activitySource.HasListeners() is false, Activity.Current is set to null after each yield return, causing all downstream middleware to lose trace context for the remainder of the stream.
Reproduction Steps
- Register
OpenTelemetryChatClient in a streaming pipeline without specifying a sourceName, or with a sourceName that has no listeners attached
- Set
Activity.Current to a known activity before entering the pipeline
- Stream a response via
GetStreamingResponseAsync
- Observe that
Activity.Current is null after the first yield return
Environment
Microsoft.Extensions.AI 10.4.1
.NET 8.0
References
Description
OpenTelemetryChatClient.GetStreamingResponseAsynccontains the following workaround for dotnet/runtime#47802:When
activityisnull, this actively setsActivity.Current = nullafter eachyield return, destroying trace context for all downstream middleware in the streaming pipeline.Root Cause
The workaround sets
Activity.Currentunconditionally. However,CreateAndConfigureActivityonly creates anActivitywhen_activitySource.HasListeners()istrue:This means in any environment where no listener is attached to the
ActivitySource,activityis alwaysnull, and the workaround unconditionally setsActivity.Current = nullon everyyield returnthroughout the entire stream — regardless of whatActivity.Currentwas before entering the middleware.Relation to Existing Issues
This is the same class of bug reported and fixed for
FunctionInvokingChatClientin #7320 / #7321. The fix in PR #7321 was not applied toOpenTelemetryChatClient.cs.Additional Notes
Specifying a
sourceNamemasks the bugThe bug can be masked by specifying a
sourceNamethat has active listeners, causingHasListeners()to returntrueandactivityto be non-null:This makes the bug environment-dependent and difficult to diagnose, as it may not reproduce in environments where OpenTelemetry is fully configured with a matching source name, but will silently destroy trace context in environments where it is not.
Downstream middleware impact
Any middleware registered after
OpenTelemetryChatClientin the pipeline that relies onActivity.Currentfor logging or tracing will silently lose trace context on every streaming response. This includes scenarios such as:Activity.Current?.DisplayNamefor operation namesProposed Fix
The workaround should be guarded to only restore
Activity.Currentwhen an activity was actually started:This ensures
Activity.Currentis never actively destroyed, regardless of listener configuration or source name.Expected Behavior
Activity.Currentshould be preserved acrossyieldpoints inGetStreamingResponseAsyncregardless of whether_activitySource.HasListeners()istrueorfalse, consistent with the fix applied toFunctionInvokingChatClientin #7321.Actual Behavior
In any streaming scenario where
_activitySource.HasListeners()isfalse,Activity.Currentis set tonullafter eachyield return, causing all downstream middleware to lose trace context for the remainder of the stream.Reproduction Steps
OpenTelemetryChatClientin a streaming pipeline without specifying asourceName, or with asourceNamethat has no listeners attachedActivity.Currentto a known activity before entering the pipelineGetStreamingResponseAsyncActivity.Currentisnullafter the firstyield returnEnvironment
Microsoft.Extensions.AI 10.4.1.NET 8.0References