microsoft/teams.net
Publicmirrored from https://github.com/microsoft/teams.netAvailable
core/src/Microsoft.Teams.Apps/Diagnostics/TeamsBaggageBuilder.cs
170lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | using System.Globalization; |
| 5 | using Microsoft.Teams.Apps.Schema; |
| 6 | using OpenTelemetry; |
| 7 | |
| 8 | namespace Microsoft.Teams.Apps.Diagnostics; |
| 9 | |
| 10 | /// <summary> |
| 11 | /// Builds OpenTelemetry baggage for Agent365 export from <c>Microsoft.Teams.Apps</c> turn types. |
| 12 | /// </summary> |
| 13 | /// <remarks> |
| 14 | /// <para> |
| 15 | /// Populates the cert-required keys (<c>microsoft.tenant.id</c>, <c>gen_ai.conversation.id</c>, |
| 16 | /// <c>microsoft.channel.name</c>, etc.) plus the Apps-only keys backed by |
| 17 | /// <see cref="TeamsChannelAccount"/> (<c>user.id</c>, <c>user.email</c>, |
| 18 | /// <c>microsoft.agent.user.email</c>, <c>gen_ai.agent.description</c>). |
| 19 | /// </para> |
| 20 | /// </remarks> |
| 21 | public sealed class TeamsBaggageBuilder |
| 22 | { |
| 23 | private readonly Dictionary<string, string?> _pairs = new(StringComparer.Ordinal); |
| 24 | |
| 25 | /// <summary>Sets the Microsoft Entra tenant id (<c>microsoft.tenant.id</c>). Required for cert.</summary> |
| 26 | public TeamsBaggageBuilder TenantId(string? v) => Set(AgentObservabilityKeys.TenantId, v); |
| 27 | |
| 28 | /// <summary>Sets the conversation id (<c>gen_ai.conversation.id</c>). Required for cert.</summary> |
| 29 | public TeamsBaggageBuilder ConversationId(string? v) => Set(AgentObservabilityKeys.ConversationId, v); |
| 30 | |
| 31 | /// <summary>Sets the conversation item link (<c>microsoft.conversation.item.link</c>). Optional.</summary> |
| 32 | public TeamsBaggageBuilder ConversationItemLink(string? v) => Set(AgentObservabilityKeys.ConversationItemLink, v); |
| 33 | |
| 34 | /// <summary>Sets the channel name (<c>microsoft.channel.name</c>). Required for cert.</summary> |
| 35 | public TeamsBaggageBuilder ChannelName(string? v) => Set(AgentObservabilityKeys.ChannelName, v); |
| 36 | |
| 37 | /// <summary>Sets the channel link (<c>microsoft.channel.link</c>). Optional. Not auto-populated by |
| 38 | /// <see cref="FromTeamsContext"/> — Teams's flat <c>ChannelId</c> string has no SubChannel concept.</summary> |
| 39 | public TeamsBaggageBuilder ChannelLink(string? v) => Set(AgentObservabilityKeys.ChannelLink, v); |
| 40 | |
| 41 | /// <summary>Sets the agent id (<c>gen_ai.agent.id</c>). Required for cert.</summary> |
| 42 | public TeamsBaggageBuilder AgentId(string? v) => Set(AgentObservabilityKeys.AgentId, v); |
| 43 | |
| 44 | /// <summary>Sets the agent display name (<c>gen_ai.agent.name</c>). Required for cert.</summary> |
| 45 | public TeamsBaggageBuilder AgentName(string? v) => Set(AgentObservabilityKeys.AgentName, v); |
| 46 | |
| 47 | /// <summary>Sets the agentic user id (<c>microsoft.agent.user.id</c>). Required for cert.</summary> |
| 48 | public TeamsBaggageBuilder AgenticUserId(string? v) => Set(AgentObservabilityKeys.AgenticUserId, v); |
| 49 | |
| 50 | /// <summary>Sets the agent blueprint id (<c>microsoft.a365.agent.blueprint.id</c>). Required for cert.</summary> |
| 51 | public TeamsBaggageBuilder AgentBlueprintId(string? v) => Set(AgentObservabilityKeys.AgentBlueprintId, v); |
| 52 | |
| 53 | /// <summary>Sets the human user name (<c>user.name</c>). Optional.</summary> |
| 54 | public TeamsBaggageBuilder UserName(string? v) => Set(AgentObservabilityKeys.UserName, v); |
| 55 | |
| 56 | /// <summary>Sets the operation source (<c>service.name</c>). Required for cert on server spans.</summary> |
| 57 | public TeamsBaggageBuilder OperationSource(string source) => Set(AgentObservabilityKeys.ServiceName, source); |
| 58 | |
| 59 | /// <summary>Sets the InvokeAgent server address and (optional) port. Required for InvokeAgentScope cert. |
| 60 | /// The port is recorded only when different from the HTTPS default (443).</summary> |
| 61 | public TeamsBaggageBuilder InvokeAgentServer(string? address, int? port = null) |
| 62 | { |
| 63 | Set(AgentObservabilityKeys.ServerAddress, address); |
| 64 | if (port.HasValue && port.Value != 443) |
| 65 | { |
| 66 | Set(AgentObservabilityKeys.ServerPort, port.Value.ToString(CultureInfo.InvariantCulture)); |
| 67 | } |
| 68 | return this; |
| 69 | } |
| 70 | |
| 71 | /// <summary>Sets the human user id (<c>user.id</c>). Required for cert. Apps-only — backed by |
| 72 | /// <see cref="TeamsChannelAccount.AadObjectId"/>.</summary> |
| 73 | public TeamsBaggageBuilder UserId(string? v) => Set(AgentObservabilityKeys.UserId, v); |
| 74 | |
| 75 | /// <summary>Sets the human user email (<c>user.email</c>). Required for cert. Apps-only.</summary> |
| 76 | public TeamsBaggageBuilder UserEmail(string? v) => Set(AgentObservabilityKeys.UserEmail, v); |
| 77 | |
| 78 | /// <summary>Sets the agent description (<c>gen_ai.agent.description</c>). Optional. Apps-only — |
| 79 | /// backed by <see cref="TeamsChannelAccount.UserRole"/>.</summary> |
| 80 | public TeamsBaggageBuilder AgentDescription(string? v) => Set(AgentObservabilityKeys.AgentDescription, v); |
| 81 | |
| 82 | /// <summary>Sets the agentic user email (<c>microsoft.agent.user.email</c>). Required for cert. Apps-only.</summary> |
| 83 | public TeamsBaggageBuilder AgenticUserEmail(string? v) => Set(AgentObservabilityKeys.AgenticUserEmail, v); |
| 84 | |
| 85 | /// <summary>Escape hatch for setting any baggage key not exposed by a typed setter |
| 86 | /// (e.g. <c>client.address</c> derived in HTTP middleware).</summary> |
| 87 | public TeamsBaggageBuilder Set(string key, string? value) |
| 88 | { |
| 89 | if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value)) |
| 90 | { |
| 91 | _pairs[key] = value; |
| 92 | } |
| 93 | return this; |
| 94 | } |
| 95 | |
| 96 | /// <summary> |
| 97 | /// Populates every baggage key reachable from <c>ctx.Activity</c> — including the Apps-only keys |
| 98 | /// backed by <see cref="TeamsChannelAccount"/>. Tenant fallback uses the typed |
| 99 | /// <see cref="TeamsChannelData"/> when <see cref="TeamsChannelAccount.TenantId"/> is null. |
| 100 | /// </summary> |
| 101 | public TeamsBaggageBuilder FromTeamsContext<TActivity>(Context<TActivity> ctx) where TActivity : TeamsActivity |
| 102 | { |
| 103 | ArgumentNullException.ThrowIfNull(ctx); |
| 104 | TActivity activity = ctx.Activity; |
| 105 | |
| 106 | ConversationId(activity.Conversation?.Id); |
| 107 | ConversationItemLink(activity.ServiceUrl?.ToString()); |
| 108 | ChannelName(activity.ChannelId); |
| 109 | |
| 110 | UserName(activity.From?.Name); |
| 111 | if (activity.From is TeamsChannelAccount fromAccount) |
| 112 | { |
| 113 | UserId(fromAccount.AadObjectId); |
| 114 | UserEmail(fromAccount.Email); |
| 115 | } |
| 116 | |
| 117 | TeamsChannelAccount? recipient = activity.Recipient; |
| 118 | if (recipient is not null) |
| 119 | { |
| 120 | AgentId(string.IsNullOrWhiteSpace(recipient.AgenticAppId) ? recipient.Id : recipient.AgenticAppId); |
| 121 | AgentName(recipient.Name); |
| 122 | AgenticUserId(recipient.AgenticUserId); |
| 123 | AgentBlueprintId(recipient.AgenticAppBlueprintId); |
| 124 | TenantId(recipient.TenantId); |
| 125 | AgenticUserEmail(recipient.Email); |
| 126 | AgentDescription(recipient.UserRole); |
| 127 | } |
| 128 | |
| 129 | // Tenant fallback: typed channelData on TeamsActivity (no JSON parse needed). |
| 130 | if (!_pairs.ContainsKey(AgentObservabilityKeys.TenantId)) |
| 131 | { |
| 132 | string? channelTenantId = activity.ChannelData?.Tenant?.Id; |
| 133 | if (!string.IsNullOrWhiteSpace(channelTenantId)) |
| 134 | { |
| 135 | TenantId(channelTenantId); |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | return this; |
| 140 | } |
| 141 | |
| 142 | /// <summary> |
| 143 | /// Applies the collected pairs to <see cref="Baggage.Current"/> and returns an |
| 144 | /// <see cref="IDisposable"/> that restores the previous baggage when disposed. |
| 145 | /// </summary> |
| 146 | public IDisposable Build() |
| 147 | { |
| 148 | Baggage previous = Baggage.Current; |
| 149 | foreach (KeyValuePair<string, string?> kvp in _pairs) |
| 150 | { |
| 151 | Baggage.Current = Baggage.Current.SetBaggage(kvp.Key, kvp.Value); |
| 152 | } |
| 153 | return new RestoreScope(previous); |
| 154 | } |
| 155 | |
| 156 | private sealed class RestoreScope(Baggage previous) : IDisposable |
| 157 | { |
| 158 | private bool _disposed; |
| 159 | |
| 160 | public void Dispose() |
| 161 | { |
| 162 | if (_disposed) |
| 163 | { |
| 164 | return; |
| 165 | } |
| 166 | Baggage.Current = previous; |
| 167 | _disposed = true; |
| 168 | } |
| 169 | } |
| 170 | } |
| 171 | |