// 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;
}
}
}