// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Globalization; using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core.Schema; using OpenTelemetry; namespace Microsoft.Teams.Apps.Diagnostics; /// /// Builds OpenTelemetry baggage for Agent365 export from Microsoft.Teams.Apps turn types. /// /// /// /// This class is independent from Microsoft.Teams.Core.Diagnostics.BaggageBuilder — same name /// in a different namespace, no inheritance. Apps's builder exposes the **superset** of the cert-relevant /// setters: everything Core's builder has plus the keys backed by /// (user.id, user.email, microsoft.agent.user.email, gen_ai.agent.description). /// /// /// Setter bodies are duplicated from Core's class. The duplication is the intentional trade-off for layer /// independence — see core/docs/Observability-Design.md § "Bridging strategy". /// /// public sealed class BaggageBuilder { private readonly Dictionary _pairs = new(StringComparer.Ordinal); /// Sets the Microsoft Entra tenant id (microsoft.tenant.id). Required for cert. public BaggageBuilder TenantId(string? v) => Set(AgentObservabilityKeys.TenantId, v); /// Sets the conversation id (gen_ai.conversation.id). Required for cert. public BaggageBuilder ConversationId(string? v) => Set(AgentObservabilityKeys.ConversationId, v); /// Sets the conversation item link (microsoft.conversation.item.link). Optional. public BaggageBuilder ConversationItemLink(string? v) => Set(AgentObservabilityKeys.ConversationItemLink, v); /// Sets the channel name (microsoft.channel.name). Required for cert. public BaggageBuilder ChannelName(string? v) => Set(AgentObservabilityKeys.ChannelName, v); /// Sets the channel link (microsoft.channel.link). Optional. Not auto-populated by /// — Teams's flat ChannelId string has no SubChannel concept. public BaggageBuilder ChannelLink(string? v) => Set(AgentObservabilityKeys.ChannelLink, v); /// Sets the agent id (gen_ai.agent.id). Required for cert. public BaggageBuilder AgentId(string? v) => Set(AgentObservabilityKeys.AgentId, v); /// Sets the agent display name (gen_ai.agent.name). Required for cert. public BaggageBuilder AgentName(string? v) => Set(AgentObservabilityKeys.AgentName, v); /// Sets the agentic user id (microsoft.agent.user.id). Required for cert. public BaggageBuilder AgenticUserId(string? v) => Set(AgentObservabilityKeys.AgenticUserId, v); /// Sets the agent blueprint id (microsoft.a365.agent.blueprint.id). Required for cert. public BaggageBuilder AgentBlueprintId(string? v) => Set(AgentObservabilityKeys.AgentBlueprintId, v); /// Sets the human user name (user.name). Optional. public BaggageBuilder UserName(string? v) => Set(AgentObservabilityKeys.UserName, v); /// Sets the operation source (service.name). Required for cert on server spans. public BaggageBuilder OperationSource(string source) => Set(AgentObservabilityKeys.ServiceName, source); /// Sets the InvokeAgent server address and (optional) port. Required for InvokeAgentScope cert. /// The port is recorded only when different from the HTTPS default (443). public BaggageBuilder InvokeAgentServer(string? address, int? port = null) { Set(AgentObservabilityKeys.ServerAddress, address); if (port.HasValue && port.Value != 443) { Set(AgentObservabilityKeys.ServerPort, port.Value.ToString(CultureInfo.InvariantCulture)); } return this; } /// Sets the human user id (user.id). Required for cert. Apps-only — backed by /// . public BaggageBuilder UserId(string? v) => Set(AgentObservabilityKeys.UserId, v); /// Sets the human user email (user.email). Required for cert. Apps-only. public BaggageBuilder UserEmail(string? v) => Set(AgentObservabilityKeys.UserEmail, v); /// Sets the agent description (gen_ai.agent.description). Optional. Apps-only — /// backed by . public BaggageBuilder AgentDescription(string? v) => Set(AgentObservabilityKeys.AgentDescription, v); /// Sets the agentic user email (microsoft.agent.user.email). Required for cert. Apps-only. public BaggageBuilder AgenticUserEmail(string? v) => Set(AgentObservabilityKeys.AgenticUserEmail, v); /// Escape hatch for setting any baggage key not exposed by a typed setter /// (e.g. client.address derived in HTTP middleware). public BaggageBuilder Set(string key, string? value) { if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value)) { _pairs[key] = value; } return this; } /// /// Populates every baggage key reachable from ctx.Activity — including the Apps-only keys /// backed by . Tenant fallback uses the typed /// when is null. /// public BaggageBuilder FromTeamsContext(Context ctx) where TActivity : TeamsActivity { ArgumentNullException.ThrowIfNull(ctx); TActivity activity = ctx.Activity; ConversationId(activity.Conversation?.Id); ConversationItemLink(activity.ServiceUrl?.ToString()); ChannelName(activity.ChannelId); UserName(activity.From?.Name); if (activity.From is TeamsConversationAccount fromTcc) { UserId(fromTcc.AadObjectId); UserEmail(fromTcc.Email); } ConversationAccount? recipient = activity.Recipient; if (recipient is not null) { AgentId(string.IsNullOrWhiteSpace(recipient.AgenticAppId) ? recipient.Id : recipient.AgenticAppId); AgentName(recipient.Name); AgenticUserId(recipient.AgenticUserId); AgentBlueprintId(recipient.AgenticAppBlueprintId); TenantId(recipient.TenantId); } if (recipient is TeamsConversationAccount recTcc) { AgenticUserEmail(recTcc.Email); AgentDescription(recTcc.UserRole); } // Tenant fallback: typed channelData on TeamsActivity (no JSON parse needed). if (!_pairs.ContainsKey(AgentObservabilityKeys.TenantId)) { string? channelTenantId = activity.ChannelData?.Tenant?.Id; if (!string.IsNullOrWhiteSpace(channelTenantId)) { TenantId(channelTenantId); } } return this; } /// /// Applies the collected pairs to and returns an /// that restores the previous baggage when disposed. /// public IDisposable Build() { Baggage previous = Baggage.Current; foreach (KeyValuePair kvp in _pairs) { Baggage.Current = Baggage.Current.SetBaggage(kvp.Key, kvp.Value); } return new RestoreScope(previous); } private sealed class RestoreScope(Baggage previous) : IDisposable { private bool _disposed; public void Dispose() { if (_disposed) { return; } Baggage.Current = previous; _disposed = true; } } }