microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v2.1.0-preview-0007

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/docs/Observability-Design.md

371lines · modecode

1# Observability Design
2
3## Overview
4
5The Teams .NET SDK (`Microsoft.Teams.Core`, `Microsoft.Teams.Apps`, `Microsoft.Teams.Apps.BotBuilder`) emits OpenTelemetry-compatible traces, metrics, and logs so that consuming bots can wire observability through the [Microsoft OpenTelemetry distro](https://github.com/microsoft/opentelemetry-distro-dotnet) and ship telemetry to Azure Monitor, an OTLP collector (Aspire Dashboard, Grafana LGTM, Jaeger), or the console.
6
7The SDK uses the BCL `System.Diagnostics.ActivitySource` and `System.Diagnostics.Metrics.Meter` for the trace and metric APIs. The OpenTelemetry SDK and exporters are an application concern: the bot project references `Microsoft.OpenTelemetry`, subscribes to the SDK's source/meter by name, and configures exporters. `Microsoft.Teams.Apps` takes a dependency on `OpenTelemetry.Api` so the `TeamsBaggageBuilder` can write to `OpenTelemetry.Baggage.Current` (see "Dependency impact" below).
8
9```
10Consuming bot Teams SDK (this design)
11───────────── ───────────────────────
12.UseMicrosoftOpenTelemetry(...)
13 ActivitySource("Microsoft.Teams.Core")
14.WithTracing(t => t ├─ "turn" (BotApplication.ProcessAsync)
15 .AddSource(CoreTelemetryNames ├─ "middleware" (TurnMiddleware.RunPipelineAsync)
16 .ActivitySourceName) ├─ "auth.outbound" (BotAuthenticationHandler)
17 .AddSource( └─ "conversation_client" (ConversationClient send/update/delete)
18 TeamsBotApplicationTelemetry
19 .ActivitySourceName)) ActivitySource("Microsoft.Teams.Apps")
20 └─ "handler" (Router.DispatchAsync)
21.WithMetrics(m => m
22 .AddMeter(CoreTelemetryNames Meter("Microsoft.Teams.Core")
23 .MeterName) ├─ teams.activities.received (Counter)
24 .AddMeter( ├─ teams.turn.duration (Histogram, ms)
25 TeamsBotApplicationTelemetry ├─ teams.handler.errors (Counter)
26 .MeterName)); ├─ teams.middleware.duration (Histogram, ms)
27 ├─ teams.outbound.calls (Counter)
28 └─ teams.outbound.errors (Counter)
29
30 Meter("Microsoft.Teams.Apps")
31 ├─ teams.handler.dispatched (Counter)
32 ├─ teams.handler.duration (Histogram, ms)
33 ├─ teams.handler.failures (Counter)
34 └─ teams.handler.unmatched (Counter)
35```
36
37## Layering constraints
38
39The SDK is split across two assemblies that observability must respect:
40
41- `Microsoft.Teams.Core` is the lower layer. It owns `BotApplication`, the turn pipeline (`TurnMiddleware`), the outbound HTTP clients (`ConversationClient`, `UserTokenClient`), and the auth-handler (`BotAuthenticationHandler`). It must **not reference anything in `Microsoft.Teams.Apps`**, including no string literals or constants tied to the Apps brand.
42- `Microsoft.Teams.Apps` depends on Core. It owns the typed activity model, `TeamsBotApplication`, and the `Router` that dispatches to user handlers.
43
44Telemetry follows the same rule: **each assembly publishes its own ActivitySource and Meter, named after the assembly.** A class named `TeamsBotApplicationTelemetry` describes Apps-level telemetry; it lives in Apps. Core's analogue is `CoreTelemetryNames`. Neither references the other.
45
46| Layer | Public name class | Source / Meter name | Spans | Metrics |
47|---|---|---|---|---|
48| `Microsoft.Teams.Core` | `Microsoft.Teams.Core.Diagnostics.CoreTelemetryNames` | `"Microsoft.Teams.Core"` | `turn`, `middleware`, `auth.outbound`, `conversation_client` | `teams.activities.received`, `teams.turn.duration`, `teams.handler.errors`, `teams.middleware.duration`, `teams.outbound.calls`, `teams.outbound.errors` |
49| `Microsoft.Teams.Apps` | `Microsoft.Teams.Apps.Diagnostics.TeamsBotApplicationTelemetry` | `"Microsoft.Teams.Apps"` | `handler` | `teams.handler.dispatched`, `teams.handler.duration`, `teams.handler.failures`, `teams.handler.unmatched` |
50
51Cross-assembly use is one-way: Apps's `Router` may call Core utilities (for example, the public `RecordException` extension on `Activity` defined in `Microsoft.Teams.Core.Diagnostics.ActivityExtensions`), but Core never reaches up into Apps. If a future Core-level helper would need an Apps concept, that helper belongs in Apps, not in Core.
52
53A consumer that uses both layers (the common case) registers both names. A consumer that only references Core (a minimal `BotApplication` bot without the `TeamsBotApplication` router) registers just `CoreTelemetryNames` and gets the full Core-level signal.
54
55## Public surface
56
57```csharp
58namespace Microsoft.Teams.Core.Diagnostics;
59public static class CoreTelemetryNames
60{
61 public const string ActivitySourceName = "Microsoft.Teams.Core";
62 public const string MeterName = "Microsoft.Teams.Core";
63}
64
65namespace Microsoft.Teams.Apps.Diagnostics;
66public static class TeamsBotApplicationTelemetry
67{
68 public const string ActivitySourceName = "Microsoft.Teams.Apps";
69 public const string MeterName = "Microsoft.Teams.Apps";
70}
71```
72
73The matching internal singletons live in each assembly's `Diagnostics/` folder:
74- `Microsoft.Teams.Core/Diagnostics/Telemetry.cs` — owned by Core; internal to `Microsoft.Teams.Core` (Apps has its own `AppsTelemetry` class).
75- `Microsoft.Teams.Apps/Diagnostics/AppsTelemetry.cs` — owned by Apps; the class is named `AppsTelemetry` to avoid collision with the Core `Telemetry` class when both namespaces are imported.
76
77## Spans
78
79The auto-instrumented HTTP-server span (from the OTel distro's ASP.NET Core instrumentation) is the parent of `turn`. Outbound HTTP-client spans (from the distro's HttpClient instrumentation) are children of `auth.outbound` and `conversation_client` automatically because the SDK opens the span before the underlying HTTP call. The `handler` span (from Apps) nests inside `turn` (from Core) via the ambient `Activity.Current`, even though the two spans come from different sources.
80
81| Span | Source | Where | Tags |
82|---|---|---|---|
83| `turn` | Core | `Microsoft.Teams.Core/BotApplication.cs` `ProcessAsync` body, after the request body has been deserialized into a `CoreActivity` | `activity.type`, `activity.id`, `conversation.id`, `channel.id`, `bot.id`, `service.url` |
84| `middleware` | Core | `Microsoft.Teams.Core/TurnMiddleware.cs` `RunPipelineAsync` per-middleware execution | `middleware.name`, `middleware.index` |
85| `handler` | Apps | `Microsoft.Teams.Apps/Routing/Router.cs` `DispatchAsync` matched-route invocation | `handler.type` (activity type or invoke name), `handler.dispatch` (`type` / `invoke` / `catchall`) |
86| `auth.outbound` | Core | `Microsoft.Teams.Core/Hosting/BotAuthenticationHandler.cs` `GetAuthorizationHeaderAsync` | `auth.flow` (`agentic` / `app_only` / `managed_identity`) |
87| `conversation_client` | Core | `Microsoft.Teams.Core/ConversationClient.cs` `SendActivityAsync` / `UpdateActivityAsync` / `DeleteActivityAsync` | `service.url`, `conversation.id`, `activity.type`, `activity.id` (set after response when known), `operation` |
88
89On exception every span sets `Status = Error` with the exception message and adds an `exception` span event with `exception.type`, `exception.message`, and `exception.stacktrace` tags. This is done through the `RecordException` extension method in `Microsoft.Teams.Core.Diagnostics.ActivityExtensions`, which is `public` so the Apps layer (`Router`) can use it too. The extension uses manual event tagging on both `net8.0` and `net10.0` to stay consistent across target frameworks; it intentionally does not delegate to the BCL `Activity.AddException` (added in .NET 9), because that API only adds the event without setting `ActivityStatusCode.Error`.
90
91### `auth.inbound` is intentionally omitted
92
93The `auth.inbound` span belongs to the auth middleware, not the bot pipeline. The SDK uses `Microsoft.AspNetCore.Authentication.JwtBearer` for inbound auth, which is already covered by the OTel distro's ASP.NET Core HTTP-server instrumentation. Adding a separate inbound-auth span would duplicate signal without new information; it is out of scope for this design.
94
95## Metrics
96
97Core-meter instruments cover the turn pipeline, middleware, and outbound HTTP clients. Apps-meter instruments cover router dispatch (one observation per matched route).
98
99### Core meter (`Microsoft.Teams.Core`)
100
101| Metric | Kind | Unit | Tags | Where |
102|---|---|---|---|---|
103| `teams.activities.received` | Counter | — | `activity.type` | top of `BotApplication.ProcessAsync` |
104| `teams.turn.duration` | Histogram | ms | `activity.type` | `finally` of the `turn` span |
105| `teams.handler.errors` | Counter | — | `activity.type` | catch block in `BotApplication.ProcessAsync` |
106| `teams.middleware.duration` | Histogram | ms | `middleware.name` | `finally` of the `middleware` span |
107| `teams.outbound.calls` | Counter | — | `operation` ∈ {`sendActivity`, `updateActivity`, `deleteActivity`} | success branch of `ConversationClient` calls |
108| `teams.outbound.errors` | Counter | — | `operation` | exception branch of `ConversationClient` calls |
109
110### Apps meter (`Microsoft.Teams.Apps`)
111
112| Metric | Kind | Unit | Tags | Where |
113|---|---|---|---|---|
114| `teams.handler.dispatched` | Counter | — | `handler.type`, `handler.dispatch` | `Router.DispatchAsync` / `DispatchWithReturnAsync` before each matched-route invocation |
115| `teams.handler.duration` | Histogram | ms | `handler.type`, `handler.dispatch` | `finally` block around each matched-route invocation (recorded even on exception) |
116| `teams.handler.failures` | Counter | — | `handler.type`, `handler.dispatch` | catch block when a route handler throws |
117| `teams.handler.unmatched` | Counter | — | `activity.type` (DispatchAsync) or `activity.type` + `invoke.name` (DispatchWithReturnAsync) | branch where no route selector matched |
118
119OTLP exposes these names with dots; Prometheus/Mimir maps them to `teams_*_total` (counters) and `teams_*_milliseconds_*` (histograms).
120
121## Logs
122
123The OTel distro's `UseMicrosoftOpenTelemetry()` automatically wires `ILogger` to OTel log records and stamps every record with the active `Activity` trace and span IDs. Existing `BotApplication._logger.BeginActivityScope(...)` already adds `ActivityType` / `ActivityId` / `ServiceUrl` / `MSCV` to the scope dictionary, so those fields ride along on every log record produced inside a turn. **No SDK changes are required for logs.**
124
125The TODO at `Microsoft.Teams.Core/BotApplication.cs:202` (`// TODO: Replace with structured scope data, ensure it works with OpenTelemetry...`) is resolved by this design and is removed.
126
127## Consumer integration
128
129```csharp
130using Microsoft.OpenTelemetry;
131using Microsoft.Teams.Apps.Diagnostics;
132using Microsoft.Teams.Core.Diagnostics;
133
134WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
135
136builder.Services.AddOpenTelemetry()
137 .UseMicrosoftOpenTelemetry(o => o.Exporters = ExportTarget.AzureMonitor | ExportTarget.Otlp)
138 .WithTracing(t => t
139 .AddSource(CoreTelemetryNames.ActivitySourceName)
140 .AddSource(TeamsBotApplicationTelemetry.ActivitySourceName))
141 .WithMetrics(m => m
142 .AddMeter(CoreTelemetryNames.MeterName)
143 .AddMeter(TeamsBotApplicationTelemetry.MeterName));
144
145builder.Logging.AddOpenTelemetry(o => o.IncludeFormattedMessage = true);
146```
147
148Standard OpenTelemetry environment variables (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `APPLICATIONINSIGHTS_CONNECTION_STRING`, `OTEL_TRACES_SAMPLER`, `OTEL_TRACES_SAMPLER_ARG`) are honored by the distro without any SDK code.
149
150A working sample lives at `core/samples/ObservabilityBot/` with a README that documents the local Grafana LGTM container loop.
151
152## Span tree per turn
153
154```
155HTTP server span (auto, OTel ASP.NET Core)
156└─ turn (Microsoft.Teams.Core)
157 ├─ middleware [n times] (Microsoft.Teams.Core)
158 ├─ handler (Microsoft.Teams.Apps)
159 └─ conversation_client (Microsoft.Teams.Core)
160 ├─ auth.outbound (Microsoft.Teams.Core)
161 │ └─ HTTP client span (auto, OTel HttpClient — token endpoint)
162 └─ HTTP client span (auto, OTel HttpClient — Bot Service API)
163```
164
165## Agent365 baggage and the TurnContext mismatch
166
167When the consuming bot also exports to **Agent365** (`ExportTarget.Agent365` in the Microsoft OpenTelemetry distro), the Agent365 SDK certifies on a fixed set of OpenTelemetry baggage entries that decorate every span emitted from a turn. The distro ships three helpers in `Microsoft.Agents.A365.Observability.Hosting.Extensions` that pull these from a turn context — `BaggageBuilderExtensions.FromTurnContext(ITurnContext)`, `InvokeAgentScopeExtensions.FromTurnContext(ITurnContext)`, and `TurnContextExtensions.InjectObservabilityContext(ITurnContext, OpenTelemetryScope)`.
168
169**These helpers take `Microsoft.Agents.Builder.ITurnContext`. The Teams SDK does not produce an `ITurnContext`** — the Apps layer hands handlers a `Microsoft.Teams.Apps.Context<TeamsActivity>`, and the Core layer has no per-turn context type at all. The two activity object models are also subtly different (see field map below). A consumer cannot pass a Teams context into `FromTurnContext(...)` directly.
170
171We deliberately do **not** wrap `ITurnContext`: synthesizing a fake Activity shape (with `ChannelId.Channel` / `ChannelId.SubChannel` sub-properties, `StackState` dictionary, `ServiceUrl` as string) drags in `Microsoft.Agents.Builder` and is brittle to upstream changes. Instead, the Apps layer ships `TeamsBaggageBuilder` that reads directly off Teams types. See "Bridging strategy" below.
172
173### Agent365 certification — crisp definition
174
175Authoritative source: `https://github.com/microsoft/opentelemetry-distro-dotnet/blob/main/docs/agent365-getting-started.md` § "Validate for store publishing".
176
177Two requirements gate Agent365 store publishing:
178
179#### (1) Scope coverage
180
181The agent **must implement** the following scopes via the Agent365 SDK (`Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes`):
182
183| Scope | When to start | Required for publishing? |
184|---|---|---|
185| `InvokeAgentScope` | Top of agent processing | **Yes** |
186| `InferenceScope` | Around each LLM call | **Yes** |
187| `ExecuteToolScope` | Around each tool / function call | **Yes** |
188| `OutputScope` | Optional — for async output capture | No |
189
190Auto-instrumentation (Semantic Kernel, OpenAI, Azure OpenAI, Agent Framework) emits **inference** spans automatically, but `InvokeAgentScope` and `ExecuteToolScope` must be started by the agent.
191
192#### (2) Attribute coverage
193
194Every Required attribute on each scope must be **non-null at scope close**. The bulk of these come from baggage (set once per turn via `TeamsBaggageBuilder`); a handful are scope-specific and come from `ScopeDetails` / `Record*` methods.
195
196**Common Required attributes (all scopes):**
197
198| Key | Where it comes from |
199|---|---|
200| `microsoft.tenant.id` | `TeamsBaggageBuilder.TenantId(...)` |
201| `gen_ai.agent.id` | `TeamsBaggageBuilder.AgentId(...)` |
202| `gen_ai.agent.name` | `TeamsBaggageBuilder.AgentName(...)` |
203| `microsoft.a365.agent.blueprint.id` | `TeamsBaggageBuilder.AgentBlueprintId(...)` |
204| `microsoft.agent.user.id` | `TeamsBaggageBuilder.AgenticUserId(...)` |
205| `microsoft.agent.user.email` | `TeamsBaggageBuilder.AgenticUserEmail(...)` |
206| `client.address` | Caller-supplied (HTTP request remote IP) |
207| `user.id` | `TeamsBaggageBuilder.UserId(...)` |
208| `user.email` | `TeamsBaggageBuilder.UserEmail(...)` |
209| `microsoft.channel.name` | `TeamsBaggageBuilder.ChannelName(...)` |
210| `gen_ai.conversation.id` | `TeamsBaggageBuilder.ConversationId(...)` |
211| `gen_ai.operation.name` | Set by the scope automatically |
212
213**Scope-specific Required attributes:**
214
215| Scope | Additional Required attributes | Source |
216|---|---|---|
217| `InvokeAgentScope` | `gen_ai.input.messages`, `gen_ai.output.messages`, `server.address`, `server.port` | `scope.RecordInputMessages(...)` / `RecordOutputMessages(...)` + `TeamsBaggageBuilder.InvokeAgentServer(host, port)` |
218| `ExecuteToolScope` | `gen_ai.tool.call.arguments`, `gen_ai.tool.call.id`, `gen_ai.tool.call.result`, `gen_ai.tool.name`, `gen_ai.tool.type` | `ToolCallDetails` + `scope.RecordResponse(...)` |
219| `InferenceScope` | `gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.provider.name`, `gen_ai.request.model` | `InferenceCallDetails` + `RecordInputMessages` / `RecordOutputMessages` |
220| `OutputScope` | `gen_ai.output.messages` | `Response` constructor |
221
222**Optional (recommended but not gating):** `gen_ai.agent.description`, `gen_ai.agent.version`, `microsoft.a365.agent.platform.id`, `microsoft.session.id`, `microsoft.session.description`, `microsoft.conversation.item.link`, `microsoft.channel.link`, all `microsoft.a365.caller.agent.*`, `microsoft.a365.agent.thought.process` (InferenceScope only).
223
224**Out of scope of this SDK:** the scope objects themselves (`InvokeAgentScope`, `InferenceScope`, `ExecuteToolScope`, `OutputScope`) ship in `Microsoft.OpenTelemetry`. The Teams SDK only ships the `TeamsBaggageBuilder` (in Apps) that populates the cert-required baggage; agents create the scopes themselves at the appropriate boundaries.
225
226### Required baggage map (Teams activity → Agent365 keys)
227
228| Group | Key (Agent365 wire) | Required for cert? | Source field on the Teams activity |
229|---|---|---|---|
230| Tenant | `microsoft.tenant.id` | **Yes** | `Activity.Recipient.TenantId` (typed on Apps's `TeamsConversationAccount`); fallback `Activity.ChannelData.tenant.id` (typed on Apps's `TeamsChannelData`) |
231| Conversation | `gen_ai.conversation.id` | **Yes** | `Activity.Conversation.Id` |
232| Conversation | `microsoft.conversation.item.link` | Optional | `Activity.ServiceUrl?.ToString()` |
233| Channel | `microsoft.channel.name` | **Yes** | `Activity.ChannelId` (the whole string — `"msteams"`, `"webchat"`, …) |
234| Channel | `microsoft.channel.link` | Optional | No equivalent on the Teams activity — see "Channel / SubChannel mapping" below |
235| Caller (human) | `user.id` | **Yes** | `((TeamsConversationAccount)Activity.From).AadObjectId` (Apps-only) |
236| Caller (human) | `user.name` | Optional | `Activity.From.Name` |
237| Caller (human) | `user.email` | **Yes** | `((TeamsConversationAccount)Activity.From).Email` (Apps-only) |
238| Target agent | `gen_ai.agent.id` | **Yes** | `Activity.Recipient.AgenticAppId ?? Activity.Recipient.Id` |
239| Target agent | `gen_ai.agent.name` | **Yes** | `Activity.Recipient.Name` |
240| Target agent | `microsoft.agent.user.id` | **Yes** | `Activity.Recipient.AgenticUserId` |
241| Target agent | `microsoft.agent.user.email` | **Yes** | `((TeamsConversationAccount)Activity.Recipient).Email` (Apps-only) |
242| Target agent | `gen_ai.agent.description` | Optional | `((TeamsConversationAccount)Activity.Recipient).UserRole` (Apps-only) |
243| Target agent | `microsoft.a365.agent.blueprint.id` | **Yes** | `Activity.Recipient.AgenticAppBlueprintId` |
244| Operation source | `service.name` (set via `TeamsBaggageBuilder.OperationSource`) | **Yes** (server spans) | Caller-supplied constant (e.g. `"teams-bot"`) |
245
246The fields the Agent365 helpers also access that have **no Teams equivalent**:
247
248- `turnContext.StackState[O11ySpanId / O11yTraceId]` — Teams's `Context<TActivity>` has no `StackState` dictionary. Reading the active span/trace id later in the turn must go through `Activity.Current?.SpanId` / `Activity.Current?.TraceId` instead. `InjectObservabilityContext` is therefore not portable as-is.
249
250### Channel / SubChannel mapping
251
252The upstream `BaggageBuilderExtensions.FromTurnContext` (in Agent Builder) reads `Activity.ChannelId.Channel` and `Activity.ChannelId.SubChannel` — Agent Builder's `ChannelId` is a complex object. Teams's `ChannelId` is a **plain string** (`"msteams"`, `"webchat"`, …) and has no `SubChannel` concept. Resolution:
253
254| Agent365 baggage key | Teams source | Auto-populated by `FromTeamsContext`? |
255|---|---|---|
256| `microsoft.channel.name` (Required) | `Activity.ChannelId` (the whole string) | **Yes** |
257| `microsoft.channel.link` (Optional in all four cert scopes) | No equivalent on the Teams activity | **No** — left unset by the extractor |
258
259`microsoft.channel.link` is **Optional** in every cert-scope manifest, so leaving it unset does not block certification. The `ChannelLink(string?)` fluent setter remains on `TeamsBaggageBuilder` for callers who do have a meaningful sub-channel value (for example, derived in HTTP middleware before the bot pipeline runs, or supplied from configuration).
260
261We deliberately avoid synthesizing `ChannelLink` from `TeamsChannelData.Channel.Id` (the Teams team/channel id) or from `ServiceUrl`: the upstream semantics of `microsoft.channel.link` is "the sub-channel within the channel" (`M365CopilotSubChannel`-style routing), which is a different concept from a Teams channel id. Misclassifying these would mis-categorize spans in Agent365 dashboards.
262
263### `TenantId` on `TeamsConversationAccount` (Apps only)
264
265`TenantId` is a typed property on Apps's `TeamsConversationAccount`:
266
267```csharp
268// Microsoft.Teams.Apps/Schema/TeamsConversationAccount.cs
269[JsonPropertyName("tenantId")]
270public string? TenantId { get; set; }
271```
272
273Core's `ConversationAccount` does **not** carry `TenantId` — the property lives only in Apps. A future follow-up could promote it to Core to support Core-only bots, but for now Agent365 baggage is only auto-populated at the Apps layer via `TeamsBaggageBuilder`.
274
275**Wire-format note:** classic Bot Framework Teams traffic carries tenant id in `channelData.tenant.id`, **not** at `from.tenantId` / `recipient.tenantId`. `TeamsBaggageBuilder.FromTeamsContext` falls back to `TeamsActivity.ChannelData?.Tenant?.Id` when the typed `Recipient.TenantId` field is null.
276
277### Bridging strategy
278
279**Single baggage builder in Apps.** After simplification, only `TeamsBaggageBuilder` (in `Microsoft.Teams.Apps`) exists. An earlier design called for a parallel `CoreBaggageBuilder` in Core, but it was descoped: the `OpenTelemetry.Api` dependency lives in Apps (not Core), and all current Agent365-integrated bots use the Apps layer. Core-only bots that need Agent365 baggage can use the `TeamsBaggageBuilder.Set(key, value)` escape hatch or add a Core-level builder in a future iteration.
280
281`TeamsBaggageBuilder` populates all cert-required keys reachable from `TeamsActivity` + `TeamsConversationAccount`:
282
283| Field set | Source |
284|---|---|
285| `microsoft.tenant.id`, `gen_ai.conversation.id`, `microsoft.conversation.item.link`, `microsoft.channel.name`, `gen_ai.agent.id`, `gen_ai.agent.name`, `microsoft.agent.user.id` (from `AgenticUserId`), `microsoft.a365.agent.blueprint.id`, `user.name`, `service.name`, `server.address`, `server.port` | `TeamsActivity` + `ConversationAccount` base fields |
286| `user.id` (from `AadObjectId`), `user.email`, `gen_ai.agent.description` (from `UserRole`), `microsoft.agent.user.email` | `TeamsConversationAccount`-only fields |
287
288#### Public surface
289
290```csharp
291// Microsoft.Teams.Apps/Diagnostics/TeamsBaggageBuilder.cs (public)
292namespace Microsoft.Teams.Apps.Diagnostics;
293
294public sealed class TeamsBaggageBuilder
295{
296 // Keys reachable from ConversationAccount base fields:
297 public TeamsBaggageBuilder TenantId(string? v);
298 public TeamsBaggageBuilder ConversationId(string? v);
299 public TeamsBaggageBuilder ConversationItemLink(string? v);
300 public TeamsBaggageBuilder ChannelName(string? v);
301 public TeamsBaggageBuilder ChannelLink(string? v);
302 public TeamsBaggageBuilder AgentId(string? v);
303 public TeamsBaggageBuilder AgentName(string? v);
304 public TeamsBaggageBuilder AgenticUserId(string? v);
305 public TeamsBaggageBuilder AgentBlueprintId(string? v);
306 public TeamsBaggageBuilder UserName(string? v);
307 public TeamsBaggageBuilder OperationSource(string source);
308 public TeamsBaggageBuilder InvokeAgentServer(string? address, int? port = null);
309 public TeamsBaggageBuilder Set(string key, string? value); // escape hatch
310
311 // Keys whose source field only exists on TeamsConversationAccount:
312 public TeamsBaggageBuilder UserId(string? v); // From.AadObjectId
313 public TeamsBaggageBuilder UserEmail(string? v); // From.Email
314 public TeamsBaggageBuilder AgentDescription(string? v); // Recipient.UserRole
315 public TeamsBaggageBuilder AgenticUserEmail(string? v); // Recipient.Email
316
317 public TeamsBaggageBuilder FromTeamsContext<TActivity>(Context<TActivity> ctx)
318 where TActivity : TeamsActivity;
319
320 public IDisposable Build(); // applies pairs to OpenTelemetry.Baggage.Current; returns restore-scope
321}
322```
323
324The Agent365 wire keys are defined as `internal const` strings in `Microsoft.Teams.Apps.Diagnostics.AgentObservabilityKeys`. They are kept in sync against the upstream Agent365 spec and are not part of the public API.
325
326#### SDK-level wiring
327
328`TeamsBotApplication.OnActivity` creates the baggage scope automatically for every turn:
329
330```csharp
331// Microsoft.Teams.Apps/TeamsBotApplication.cs — inside OnActivity override
332Context<TeamsActivity> defaultContext = new(this, teamsActivity);
333
334using IDisposable baggageScope = new TeamsBaggageBuilder()
335 .FromTeamsContext(defaultContext)
336 .Build();
337```
338
339This auto-populates all activity-derivable keys. Keys that are **not** auto-populated (because they are not on the activity) must be set by the consumer in handler code:
340- `service.name` — via `.OperationSource("my-bot")`
341- `client.address` — via `.Set("client.address", remoteIp)` (typically from HTTP middleware)
342- `server.address` / `server.port` — via `.InvokeAgentServer(host, port)`
343
344#### Consumer site (additional enrichment)
345
346```csharp
347// If the consumer needs to add keys not derivable from the activity,
348// they create an additional baggage scope in their handler:
349using Microsoft.Teams.Apps.Diagnostics;
350
351botApp.OnMessage(async (ctx, ct) =>
352{
353 using IDisposable scope = new TeamsBaggageBuilder()
354 .OperationSource("teams-bot") // required-for-cert; not derivable from the activity
355 .Build();
356
357 // … handler body — every span emitted from here carries Agent365 baggage.
358});
359```
360
361#### Dependency impact
362
363`Microsoft.Teams.Apps` takes a `PackageReference` on **`OpenTelemetry.Api`** (the lightweight API contract package, no SDK, no exporters — already a transitive dep of every `Microsoft.OpenTelemetry` consumer; conventional dep for libraries that publish OTel signals: Azure SDK, gRPC, MongoDB driver). `Build()` writes to `OpenTelemetry.Baggage.Current`, which is the canonical OTel baggage that the distro propagates onto every span emitted in the scope.
364
365`Microsoft.Teams.Core` does **not** depend on `OpenTelemetry.Api` — it uses only the BCL `System.Diagnostics` APIs for traces and metrics.
366
367## Why no DI plumbing
368
369`ActivitySource` and `Meter` are process-global by design — `ActivityListener` and `MeterListener` subscribe by source/meter name, not by instance. The SDK therefore owns the singletons as `static readonly` fields and does not register them in DI. Consuming code never receives an `ActivitySource` parameter; it just registers the source name once at startup.
370
371This keeps the instrumentation completely transparent: a bot that ignores the source name pays no overhead beyond the BCL's already-cheap "no listener attached" fast path. A bot that subscribes gets full traces, metrics, and trace-correlated logs.
372