Skip to content

Wire up A365 observability instrumentation for Agent Framework sample#308

Open
biswapm wants to merge 4 commits into
mainfrom
feature/observability-instrumentation
Open

Wire up A365 observability instrumentation for Agent Framework sample#308
biswapm wants to merge 4 commits into
mainfrom
feature/observability-instrumentation

Conversation

@biswapm
Copy link
Copy Markdown
Contributor

@biswapm biswapm commented May 15, 2026

Summary

  • Adds the minimum code needed for the Microsoft.OpenTelemetry distro Agent365 exporter to actually emit traces that show up in the Microsoft Admin Center Advanced Hunting view for the .NET Agent Framework sample agent.
  • Without these changes, the exporter saw gen_ai spans but dropped them all (missing tenant/agent identity), and even when identity was present no InvokeAgent parent event was emitted, so MAC had nothing to render.
  • All observability types are consumed transitively through Microsoft.OpenTelemetry — no direct package refs to Microsoft.Agents.A365.Observability.* (per distro guidance to avoid CS0433 collisions).

Changes in Agent/MyAgent.cs

  • Inject IExporterTokenCache<AgenticTokenStruct> so the exporter can obtain an OBO token per (agent, tenant) tuple.
  • At the start of OnMessageAsync, set tenant + agent baggage via BaggageBuilder so the distro's ActivityProcessor copies the IDs onto every child span (gen_ai / InferenceCall / ExecuteToolBySDK). Tenant from Activity.Conversation/Recipient.TenantId, agent from Activity.GetAgenticInstanceId() (for agentic requests).
  • Call _agentTokenCache.RegisterObservability(agentId, tenantId, ...) so the exporter has a token resolver for this identity — eliminates "No token obtained" warnings for the primary identity.
  • Wrap the LLM RunStreamingAsync call in InvokeAgentScope.Start(...) with AgentDetails + CallerDetails (from Activity.From). This emits the InvokeAgent event that MAC needs as the parent record for the trace UI; RecordInputMessages / RecordOutputMessages capture the user prompt and assistant reply on the scope.
  • Set ChatClientAgentOptions.Id = chatAgentId and .Name = configured name so the AI SDK's auto-instrumentation tags gen_ai spans with the real agent instance ID instead of a fresh N-format GUID per turn (which previously produced orphan identity groups the exporter couldn't authenticate).

Pattern mirrors A365OtelWrapper.InvokeObservedAgentOperation from the official Microsoft.OpenTelemetry.Agent365.Demo example in the distro repo.

Test plan

  • dotnet build dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj succeeds
  • Run sample agent locally with a fresh tenant + agent provisioned via a365 setup all
  • Send a Teams message to the agent
  • Verify in agent log: exporter shows Sending chunk ... to .../observability/tenants/.../traces and HTTP 200 exporting spans (with a x-ms-correlation-id)
  • Verify Partitioned into N identity groups shows the primary agent instance ID
  • Confirm No token obtained warning is gone for the primary identity
  • After ~5 min ingestion lag, run KQL in MAC Advanced Hunting:
    CloudAppEvents
    | where Timestamp > ago(15m)
    | where ActionType == "InvokeAgent"
    | where RawEventData contains "<your-agent-instance-id>"
    | order by Timestamp desc
    and confirm InvokeAgent rows appear alongside the existing InferenceCall / ExecuteToolBySDK events.

Adds the minimum code needed for the Microsoft.OpenTelemetry distro
Agent365 exporter to actually emit traces that show up in the
Microsoft Admin Center Advanced Hunting view.

Without these changes, the exporter saw gen_ai spans but dropped them
all because they had no tenant/agent identity attributes, and even when
identity was present no InvokeAgent parent event was emitted, so MAC
had nothing to render.

Changes in Agent/MyAgent.cs:
  - Inject IExporterTokenCache<AgenticTokenStruct> so the exporter can
    obtain an OBO token per (agent, tenant) tuple.
  - At the start of OnMessageAsync, set tenant + agent baggage via
    BaggageBuilder so the distro's ActivityProcessor copies the IDs
    onto every child span (gen_ai/InferenceCall/ExecuteToolBySDK).
    Tenant comes from Activity.Conversation/Recipient.TenantId, agent
    from Activity.GetAgenticInstanceId() (for agentic requests).
  - Call _agentTokenCache.RegisterObservability(agentId, tenantId, ...)
    so the exporter has a token resolver for this identity, eliminating
    "No token obtained" warnings for the primary identity.
  - Wrap the LLM RunStreamingAsync call in InvokeAgentScope.Start(...)
    with AgentDetails + CallerDetails (from Activity.From). This is
    what emits the InvokeAgent event that MAC needs as the parent
    record for the trace UI; RecordInputMessages / RecordOutputMessages
    capture the user prompt and assistant reply on the scope.
  - Set ChatClientAgentOptions.Id = chatAgentId and .Name = configured
    name so the AI SDK's auto-instrumentation tags gen_ai spans with
    the real agent instance ID instead of a fresh N-format GUID per
    turn (which previously produced orphan identity groups the exporter
    couldn't authenticate).

Pattern mirrors A365OtelWrapper.InvokeObservedAgentOperation from the
official Microsoft.OpenTelemetry.Agent365.Demo example.
@biswapm biswapm requested a review from a team as a code owner May 15, 2026 10:25
Copilot AI review requested due to automatic review settings May 15, 2026 10:25
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Comment thread dotnet/agent-framework/sample-agent/Agent/MyAgent.cs Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires Agent365 observability into the .NET Agent Framework sample so A365 exporter spans can be associated with agent/tenant identity and emit an InvokeAgent parent event.

Changes:

  • Adds observability imports, token-cache injection, baggage setup, and token registration in MyAgent.
  • Wraps streaming LLM execution in InvokeAgentScope and records input/output messages.
  • Sets ChatClientAgentOptions.Id and Name to align AI SDK instrumentation tags with configured agent identity.

Agent 365 sample validation: copyright header is present and no Kairo references were found in the changed source file. Two blocking issues were filed around non-agentic identity resolution and deriving a URI from display text.

Comment thread dotnet/agent-framework/sample-agent/Agent/MyAgent.cs Outdated
Comment thread dotnet/agent-framework/sample-agent/Agent/MyAgent.cs Outdated
biswapm added 2 commits May 15, 2026 16:05
…Name

Build the InvokeAgentScope endpoint Uri from the AgentBlueprintId (a
GUID, always URI-safe) under the RFC 2606 reserved `.invalid` TLD,
instead of slugifying the display AgentName. AgentName is free-form
configuration text and may contain characters invalid in a hostname
(apostrophes, parentheses, slashes, etc.), which would throw
UriFormatException during message handling before the LLM call.
…observability when unavailable

Previously, OnMessageAsync fell back to Guid.Empty for non-agentic
requests (Playground / WebChat with the OBO auth handler). That meant
the baggage, token-cache registration, and ChatClientAgentOptions.Id
were all set to a synthetic identity the Agent365 exporter could not
authenticate — exactly the orphan "No token obtained" group we set out
to fix.

Changes in Agent/MyAgent.cs:
  - Resolve agent id via the OBO token (UserAuthorization.GetTurnTokenAsync +
    Utility.ResolveAgentIdentity) for non-agentic requests, mirroring
    A365OtelWrapper.ResolveTenantAndAgentId in the official distro demo.
  - Compute hasObservabilityIdentity = both agent id AND tenant id are
    real (non-empty); skip BaggageBuilder, RegisterObservability and
    InvokeAgentScope when false. This stops polluting traces with
    Guid.Empty-grouped spans the exporter would drop anyway.
  - Make chatAgentId parameter to GetClientAgent nullable; only set
    ChatClientAgentOptions.Id when a real agent id was resolved.
  - Forward cancellationToken to UserAuthorization.GetTurnTokenAsync.
  - Tighten Activity null-state with `?.` to satisfy nullable analysis.
Comment thread dotnet/agent-framework/sample-agent/Agent/MyAgent.cs Dismissed
Comment thread dotnet/agent-framework/sample-agent/Agent/MyAgent.cs Dismissed
…settings template

Adds two changes to the appsettings.json template:

1. `EnableAgent365Exporter: true` at the root. The SDK's
   Microsoft.Agents.A365.Observability.Runtime.Builder defaults this
   to false when the key is absent — without setting it to true, all
   the observability code added in this PR is wired up but no traces
   are POSTed to the Agent365 backend. Required for the sample to
   actually export traces out of the box.

2. Debug-level logging for the four observability-relevant categories:
   - OpenTelemetry — SDK pipeline diagnostics
   - Microsoft.OpenTelemetry — distro startup and instrumentation
   - Microsoft.Agents.A365.Observability — exporter activity: span
     partitioning, token resolution, HTTP export status codes.
     The key signal for verifying traces reach the backend.
   - Microsoft.Agents.A365.Runtime — auth identity resolution,
     tool dispatch, and related runtime diagnostics.

   These make the sample observability-ready out of the box — running
   it shows exporter batches, identity groups, and HTTP 200 responses
   in the console, so developers can immediately confirm whether
   traces are flowing.

Skipped: System.Net.Http.HttpClient.Default at Trace level. That setting
logs bearer tokens and response bodies in plain text and is not
appropriate as a template default — devs can opt in when diagnosing
specific HTTP issues.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants