fix: correct OTEL instrumentation wiring for agent traces

- Fix ActivitySource name from 'Microsoft.Agents.AI' to
  'Experimental.Microsoft.Agents.AI' matching the Agent Framework default
- Wrap agents with UseOpenTelemetry() via AIAgentBuilder so agent
  invocation spans are emitted to the configured OTLP collector
- Add proper disposal of OpenTelemetryAgent wrappers via
  SyncDisposableAdapter bridging IDisposable to IAsyncDisposable
- Enable workflow-level telemetry on WorkflowRunner custom graph builds
  via WithOpenTelemetry() (HandoffWorkflowBuilder and
  GroupChatWorkflowBuilder do not expose this API — framework limitation)
- Fix settings panel help text to be user-facing instead of dev jargon
- Update test assertion for corrected ActivitySource name

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-16 11:52:13 +02:00
co-authored by Copilot
parent 67fb950e61
commit 3a3e1d5eab
5 changed files with 24 additions and 6 deletions
@@ -14,7 +14,7 @@ internal static class OpenTelemetrySetup
private static readonly string[] ActivitySourceNames =
[
"Microsoft.Agents.AI",
"Experimental.Microsoft.Agents.AI",
"Microsoft.Agents.AI.Workflows",
];
@@ -87,8 +87,13 @@ internal sealed class CopilotAgentBundle : ProviderAgentBundle
name: definition.GetAgentName(),
description: NormalizeOptionalString(definition.Config.Description));
agents.Add(agent);
AIAgent instrumentedAgent = new AIAgentBuilder(agent).UseOpenTelemetry().Build();
agents.Add(instrumentedAgent);
disposables.Add(agent);
if (instrumentedAgent is IDisposable instrumentedDisposable)
{
disposables.Add(new SyncDisposableAdapter(instrumentedDisposable));
}
}
// The bundle owns the shared client — disposed after all agents.
@@ -328,4 +333,17 @@ internal sealed class CopilotAgentBundle : ProviderAgentBundle
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
/// <summary>
/// Adapts a synchronous <see cref="IDisposable"/> to <see cref="IAsyncDisposable"/>
/// so it can be tracked in the async disposal pipeline.
/// </summary>
private sealed class SyncDisposableAdapter(IDisposable inner) : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
inner.Dispose();
return ValueTask.CompletedTask;
}
}
}
@@ -112,7 +112,7 @@ internal sealed class WorkflowRunner
builder = builder.WithName(workflowDefinition.Name);
}
return builder.WithOutputFrom(routes[endNode.Id].Exit).Build();
return builder.WithOutputFrom(routes[endNode.Id].Exit).WithOpenTelemetry().Build();
}
private WorkflowNodeRoute CreateNodeRoute(
@@ -27,7 +27,7 @@ public sealed class OpenTelemetrySetupTests
Assert.Equal(OtlpExportProtocol.Grpc, configuration.Protocol);
Assert.Collection(
configuration.ActivitySourceNames,
source => Assert.Equal("Microsoft.Agents.AI", source),
source => Assert.Equal("Experimental.Microsoft.Agents.AI", source),
source => Assert.Equal("Microsoft.Agents.AI.Workflows", source));
}
+2 -2
View File
@@ -614,8 +614,8 @@ function TelemetrySection({
placeholder={DEFAULT_OTEL_ENDPOINT}
/>
<p className="mt-1.5 text-[11px] text-[var(--color-text-muted)]">
Use <span className="font-mono">bun run aspire</span> to launch the Aspire Dashboard locally at this default endpoint.
Changes take effect on the next sidecar session.
The URL of any OTLP-compatible collector (e.g. Jaeger, Aspire Dashboard, Grafana Alloy).
Changes take effect on the next session.
</p>
</div>
</div>