From 7564849e8168a0dab3fd19d259e73a3e9e31d386 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 18 Feb 2026 17:02:15 -0800 Subject: [PATCH 1/5] Support new telemetry schema attribute names gated by A365_USE_NEW_TELEMETRY_SCHEMA env var Co-Authored-By: Claude Opus 4.6 --- .../src/utils/TurnContextUtils.ts | 1 - .../src/tracing/constants.ts | 57 +++++++++------ .../src/tracing/contracts.ts | 3 + .../src/tracing/middleware/BaggageBuilder.ts | 8 +- .../src/tracing/processors/SpanProcessor.ts | 14 +++- .../src/tracing/processors/util.ts | 11 +-- .../src/tracing/scopes/ExecuteToolScope.ts | 14 ++-- .../src/tracing/scopes/InferenceScope.ts | 18 +++-- .../src/tracing/scopes/InvokeAgentScope.ts | 21 +++--- .../src/tracing/scopes/OpenTelemetryScope.ts | 20 ++++- .../observability/core/SpanProcessor.test.ts | 8 +- tests/observability/core/scopes.test.ts | 58 ++++++++++++++- .../hosting/BaggageBuilderUtils.test.ts | 1 - .../extension/hosting/scope-utils.test.ts | 73 ++++++++++--------- .../tracing/exporter-utils.test.ts | 5 +- 15 files changed, 210 insertions(+), 102 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 981d4ead..567782e2 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -33,7 +33,6 @@ export function getCallerBaggagePairs(turnContext: TurnContext): Array<[string, [OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY, from.aadObjectId], [OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, from.name], [OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY, upn], - [OpenTelemetryConstants.GEN_AI_CALLER_TENANT_ID_KEY, from.tenantId], [OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, from.agenticAppBlueprintId] ]; return normalizePairs(pairs); diff --git a/packages/agents-a365-observability/src/tracing/constants.ts b/packages/agents-a365-observability/src/tracing/constants.ts index c7e88b19..4f3a371e 100644 --- a/packages/agents-a365-observability/src/tracing/constants.ts +++ b/packages/agents-a365-observability/src/tracing/constants.ts @@ -6,6 +6,11 @@ * OpenTelemetry constants for Agent 365 */ export class OpenTelemetryConstants { + // New telemetry schema feature flag + public static readonly USE_NEW_TELEMETRY_SCHEMA_ENV = 'A365_USE_NEW_TELEMETRY_SCHEMA'; + // eslint-disable-next-line no-restricted-properties + public static readonly isNewTelemetrySchemaEnabled = process.env['A365_USE_NEW_TELEMETRY_SCHEMA'] === 'true'; + // Span operation names public static readonly INVOKE_AGENT_OPERATION_NAME = 'invoke_agent'; public static readonly EXECUTE_TOOL_OPERATION_NAME = 'execute_tool'; @@ -46,9 +51,10 @@ export class OpenTelemetryConstants { public static readonly GEN_AI_AGENT_NAME_KEY = 'gen_ai.agent.name'; public static readonly GEN_AI_AGENT_TYPE_KEY = 'gen_ai.agent.type'; public static readonly GEN_AI_AGENT_DESCRIPTION_KEY = 'gen_ai.agent.description'; - public static readonly GEN_AI_AGENT_PLATFORM_ID_KEY = 'gen_ai.agent.platformid'; + public static readonly GEN_AI_AGENT_PLATFORM_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.agent.platform.id' : 'gen_ai.agent.platformid'; + public static readonly GEN_AI_AGENT_THOUGHT_PROCESS_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.agent.thought.process' : 'gen_ai.agent.thought.process'; public static readonly GEN_AI_CONVERSATION_ID_KEY = 'gen_ai.conversation.id'; - public static readonly GEN_AI_CONVERSATION_ITEM_LINK_KEY = 'gen_ai.conversation.item.link'; + public static readonly GEN_AI_CONVERSATION_ITEM_LINK_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.conversation.item.link' : 'gen_ai.conversation.item.link'; public static readonly GEN_AI_TOKEN_TYPE_KEY = 'gen_ai.token.type'; public static readonly GEN_AI_USAGE_INPUT_TOKENS_KEY = 'gen_ai.usage.input_tokens'; public static readonly GEN_AI_USAGE_OUTPUT_TOKENS_KEY = 'gen_ai.usage.output_tokens'; @@ -69,36 +75,35 @@ export class OpenTelemetryConstants { public static readonly GEN_AI_TOOL_TYPE_KEY = 'gen_ai.tool.type'; // Agent user (user tied to agent instance during creation) or caller dimensions - public static readonly GEN_AI_AGENT_USER_ID_KEY = 'gen_ai.agent.userid'; - public static readonly GEN_AI_CALLER_TENANT_ID_KEY = 'gen_ai.caller.tenantid'; - public static readonly GEN_AI_CALLER_ID_KEY = 'gen_ai.caller.id'; - public static readonly GEN_AI_CALLER_NAME_KEY = 'gen_ai.caller.name'; - public static readonly GEN_AI_CALLER_UPN_KEY = 'gen_ai.caller.upn'; - public static readonly GEN_AI_CALLER_CLIENT_IP_KEY = 'gen_ai.caller.client.ip'; + public static readonly GEN_AI_AGENT_USER_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.agent.user.id' : 'gen_ai.agent.userid'; + public static readonly GEN_AI_CALLER_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.caller.id' : 'gen_ai.caller.id'; + public static readonly GEN_AI_CALLER_NAME_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.caller.name' : 'gen_ai.caller.name'; + public static readonly GEN_AI_CALLER_UPN_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.caller.upn' : 'gen_ai.caller.upn'; + public static readonly GEN_AI_CALLER_CLIENT_IP_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'client.address' : 'gen_ai.caller.client.ip'; // Agent to Agent caller agent dimensions - public static readonly GEN_AI_CALLER_AGENT_USER_ID_KEY = 'gen_ai.caller.agent.userid'; + public static readonly GEN_AI_CALLER_AGENT_USER_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.caller.agent.user.id' : 'gen_ai.caller.agent.userid'; public static readonly GEN_AI_CALLER_AGENT_UPN_KEY = 'gen_ai.caller.agent.upn'; public static readonly GEN_AI_CALLER_AGENT_TENANT_ID_KEY = 'gen_ai.caller.agent.tenantid'; - public static readonly GEN_AI_CALLER_AGENT_NAME_KEY = 'gen_ai.caller.agent.name'; - public static readonly GEN_AI_CALLER_AGENT_ID_KEY = 'gen_ai.caller.agent.id'; - public static readonly GEN_AI_CALLER_AGENT_TYPE_KEY = 'gen_ai.caller.agent.type'; - public static readonly GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY = 'gen_ai.caller.agent.applicationid'; + public static readonly GEN_AI_CALLER_AGENT_NAME_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.caller.agent.name' : 'gen_ai.caller.agent.name'; + public static readonly GEN_AI_CALLER_AGENT_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.caller.agent.id' : 'gen_ai.caller.agent.id'; + public static readonly GEN_AI_CALLER_AGENT_TYPE_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.caller.agent.type' : 'gen_ai.caller.agent.type'; + public static readonly GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.caller.agent.blueprint.id' : 'gen_ai.caller.agent.applicationid'; public static readonly GEN_AI_CALLER_AGENT_CLIENT_IP_KEY = 'gen_ai.caller.agent.user.client.ip'; - public static readonly GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY = 'gen_ai.caller.agent.platformid'; + public static readonly GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.caller.agent.platform.id' : 'gen_ai.caller.agent.platformid'; // Agent-specific dimensions public static readonly AGENT_ID_KEY = 'gen_ai.agent.id'; public static readonly GEN_AI_TASK_ID_KEY = 'gen_ai.task.id'; - public static readonly SESSION_ID_KEY = 'session.id'; - public static readonly SESSION_DESCRIPTION_KEY = 'session.description'; + public static readonly SESSION_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.session.id' : 'session.id'; + public static readonly SESSION_DESCRIPTION_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.session.description' : 'session.description'; public static readonly GEN_AI_ICON_URI_KEY = 'gen_ai.agent365.icon_uri'; - public static readonly TENANT_ID_KEY = 'tenant.id'; + public static readonly TENANT_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.tenant.id' : 'tenant.id'; // Baggage keys public static readonly OPERATION_SOURCE_KEY = 'operation.source'; - public static readonly GEN_AI_AGENT_AUID_KEY = 'gen_ai.agent.user.id'; - public static readonly GEN_AI_AGENT_UPN_KEY = 'gen_ai.agent.upn'; - public static readonly GEN_AI_AGENT_BLUEPRINT_ID_KEY = 'gen_ai.agent.applicationid'; + public static readonly GEN_AI_AGENT_AUID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.agent.user.id' : 'gen_ai.agent.user.id'; + public static readonly GEN_AI_AGENT_UPN_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.agent.user.upn' : 'gen_ai.agent.upn'; + public static readonly GEN_AI_AGENT_BLUEPRINT_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.agent.blueprint.id' : 'gen_ai.agent.applicationid'; public static readonly CORRELATION_ID_KEY = 'correlation.id'; public static readonly HIRING_MANAGER_ID_KEY = 'hiring.manager.id'; @@ -108,12 +113,20 @@ export class OpenTelemetryConstants { // Source metadata dimensions public static readonly GEN_AI_EXECUTION_SOURCE_ID_KEY = 'gen_ai.execution.sourceMetadata.id'; - public static readonly GEN_AI_EXECUTION_SOURCE_NAME_KEY = 'gen_ai.channel.name'; - public static readonly GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY = 'gen_ai.channel.link'; + public static readonly GEN_AI_EXECUTION_SOURCE_NAME_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.channel.name' : 'gen_ai.channel.name'; + public static readonly GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.channel.link' : 'gen_ai.channel.link'; // Custom parent id and parent name key public static readonly CUSTOM_PARENT_SPAN_ID_KEY = 'custom.parent.span.id'; public static readonly CUSTOM_SPAN_NAME_KEY = 'custom.span.name'; + + // Telemetry SDK attributes (replace operation.source when isNewTelemetrySchemaEnabled) + public static readonly TELEMETRY_SDK_NAME_KEY = 'telemetry.sdk.name'; + public static readonly TELEMETRY_SDK_LANGUAGE_KEY = 'telemetry.sdk.language'; + public static readonly TELEMETRY_SDK_VERSION_KEY = 'telemetry.sdk.version'; + public static readonly TELEMETRY_SDK_NAME_VALUE = 'Agent365Sdk'; + public static readonly TELEMETRY_SDK_LANGUAGE_VALUE = 'nodejs'; + public static readonly TELEMETRY_SDK_VERSION_VALUE = '0.0.0-placeholder'; } /** diff --git a/packages/agents-a365-observability/src/tracing/contracts.ts b/packages/agents-a365-observability/src/tracing/contracts.ts index bef4b494..6d0e6736 100644 --- a/packages/agents-a365-observability/src/tracing/contracts.ts +++ b/packages/agents-a365-observability/src/tracing/contracts.ts @@ -236,6 +236,9 @@ export interface InferenceDetails { /** Response ID from the model provider */ responseId?: string; + + /** The thought process used by the agent */ + thoughtProcess?: string; } /** diff --git a/packages/agents-a365-observability/src/tracing/middleware/BaggageBuilder.ts b/packages/agents-a365-observability/src/tracing/middleware/BaggageBuilder.ts index 7a6f49f4..39548bcb 100644 --- a/packages/agents-a365-observability/src/tracing/middleware/BaggageBuilder.ts +++ b/packages/agents-a365-observability/src/tracing/middleware/BaggageBuilder.ts @@ -95,7 +95,9 @@ export class BaggageBuilder { * @returns Self for method chaining */ correlationId(value: string | null | undefined): BaggageBuilder { - this.set(OpenTelemetryConstants.CORRELATION_ID_KEY, value); + if (!OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + this.set(OpenTelemetryConstants.CORRELATION_ID_KEY, value); + } return this; } @@ -125,7 +127,9 @@ export class BaggageBuilder { * @returns Self for method chaining */ hiringManagerId(value: string | null | undefined): BaggageBuilder { - this.set(OpenTelemetryConstants.HIRING_MANAGER_ID_KEY, value); + if (!OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + this.set(OpenTelemetryConstants.HIRING_MANAGER_ID_KEY, value); + } return this; } diff --git a/packages/agents-a365-observability/src/tracing/processors/SpanProcessor.ts b/packages/agents-a365-observability/src/tracing/processors/SpanProcessor.ts index cd48f57e..b08b16af 100644 --- a/packages/agents-a365-observability/src/tracing/processors/SpanProcessor.ts +++ b/packages/agents-a365-observability/src/tracing/processors/SpanProcessor.ts @@ -69,8 +69,18 @@ export class SpanProcessor implements BaseSpanProcessor { INVOKE_AGENT_ATTRIBUTES.forEach(key => targetKeys.add(key)); } - // Set operation source - coalesce baggage value with SDK default - if (!existingAttrs.has(OpenTelemetryConstants.OPERATION_SOURCE_KEY)) { + // Set operation source or telemetry SDK attributes + if (OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + if (!existingAttrs.has(OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY)) { + span.setAttribute(OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY, OpenTelemetryConstants.TELEMETRY_SDK_NAME_VALUE); + } + if (!existingAttrs.has(OpenTelemetryConstants.TELEMETRY_SDK_LANGUAGE_KEY)) { + span.setAttribute(OpenTelemetryConstants.TELEMETRY_SDK_LANGUAGE_KEY, OpenTelemetryConstants.TELEMETRY_SDK_LANGUAGE_VALUE); + } + if (!existingAttrs.has(OpenTelemetryConstants.TELEMETRY_SDK_VERSION_KEY)) { + span.setAttribute(OpenTelemetryConstants.TELEMETRY_SDK_VERSION_KEY, OpenTelemetryConstants.TELEMETRY_SDK_VERSION_VALUE); + } + } else if (!existingAttrs.has(OpenTelemetryConstants.OPERATION_SOURCE_KEY)) { const operationSource = baggageMap.get(OpenTelemetryConstants.OPERATION_SOURCE_KEY) || OperationSource.SDK; diff --git a/packages/agents-a365-observability/src/tracing/processors/util.ts b/packages/agents-a365-observability/src/tracing/processors/util.ts index 6c70fe06..e736ed58 100644 --- a/packages/agents-a365-observability/src/tracing/processors/util.ts +++ b/packages/agents-a365-observability/src/tracing/processors/util.ts @@ -11,19 +11,19 @@ export const GENERIC_ATTRIBUTES: readonly string[] = [ consts.TENANT_ID_KEY, consts.CUSTOM_PARENT_SPAN_ID_KEY, consts.CUSTOM_SPAN_NAME_KEY, - consts.CORRELATION_ID_KEY, + ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.CORRELATION_ID_KEY]), consts.SESSION_ID_KEY, consts.GEN_AI_CONVERSATION_ID_KEY, consts.GEN_AI_CONVERSATION_ITEM_LINK_KEY, consts.GEN_AI_OPERATION_NAME_KEY, consts.GEN_AI_AGENT_ID_KEY, consts.GEN_AI_AGENT_NAME_KEY, - consts.GEN_AI_AGENT_TYPE_KEY, + ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_AGENT_TYPE_KEY]), consts.GEN_AI_AGENT_DESCRIPTION_KEY, consts.SESSION_DESCRIPTION_KEY, consts.GEN_AI_AGENT_USER_ID_KEY, consts.GEN_AI_AGENT_UPN_KEY, - consts.GEN_AI_AGENT_BLUEPRINT_ID_KEY, + ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_AGENT_BLUEPRINT_ID_KEY]), consts.GEN_AI_AGENT_AUID_KEY, consts.GEN_AI_AGENT_PLATFORM_ID_KEY, ]; @@ -36,7 +36,6 @@ export const INVOKE_AGENT_ATTRIBUTES: readonly string[] = [ consts.GEN_AI_CALLER_ID_KEY, consts.GEN_AI_CALLER_NAME_KEY, consts.GEN_AI_CALLER_UPN_KEY, - consts.GEN_AI_CALLER_TENANT_ID_KEY, consts.GEN_AI_CALLER_CLIENT_IP_KEY, // Caller Agent (A2A) attributes consts.GEN_AI_CALLER_AGENT_ID_KEY, @@ -49,8 +48,10 @@ export const INVOKE_AGENT_ATTRIBUTES: readonly string[] = [ consts.GEN_AI_CALLER_AGENT_CLIENT_IP_KEY, consts.GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY, // Execution context - consts.GEN_AI_EXECUTION_TYPE_KEY, + ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_EXECUTION_TYPE_KEY]), consts.GEN_AI_EXECUTION_SOURCE_ID_KEY, consts.GEN_AI_EXECUTION_SOURCE_NAME_KEY, consts.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, + // New schema: agent type and blueprint ID are InvokeAgent-only + ...(consts.isNewTelemetrySchemaEnabled ? [consts.GEN_AI_AGENT_TYPE_KEY, consts.GEN_AI_AGENT_BLUEPRINT_ID_KEY] : []), ]; diff --git a/packages/agents-a365-observability/src/tracing/scopes/ExecuteToolScope.ts b/packages/agents-a365-observability/src/tracing/scopes/ExecuteToolScope.ts index ed2c3a48..9cfdf0bf 100644 --- a/packages/agents-a365-observability/src/tracing/scopes/ExecuteToolScope.ts +++ b/packages/agents-a365-observability/src/tracing/scopes/ExecuteToolScope.ts @@ -3,7 +3,7 @@ import { SpanKind, TimeInput } from '@opentelemetry/api'; import { OpenTelemetryScope } from './OpenTelemetryScope'; -import { ToolCallDetails, AgentDetails, TenantDetails, SourceMetadata } from '../contracts'; +import { ToolCallDetails, AgentDetails, TenantDetails, SourceMetadata, CallerDetails } from '../contracts'; import { ParentContext } from '../context/trace-context-propagation'; import { OpenTelemetryConstants } from '../constants'; @@ -24,6 +24,7 @@ export class ExecuteToolScope extends OpenTelemetryScope { * tool call after execution has already completed. * @param endTime Optional explicit end time (ms epoch, Date, or HrTime). When provided, the span will * use this timestamp when disposed instead of the current wall-clock time. + * @param callerDetails Optional caller details. * @returns A new ExecuteToolScope instance. */ public static start( @@ -34,9 +35,10 @@ export class ExecuteToolScope extends OpenTelemetryScope { sourceMetadata?: Pick, parentContext?: ParentContext, startTime?: TimeInput, - endTime?: TimeInput + endTime?: TimeInput, + callerDetails?: CallerDetails ): ExecuteToolScope { - return new ExecuteToolScope(details, agentDetails, tenantDetails, conversationId, sourceMetadata, parentContext, startTime, endTime); + return new ExecuteToolScope(details, agentDetails, tenantDetails, conversationId, sourceMetadata, parentContext, startTime, endTime, callerDetails); } private constructor( @@ -47,7 +49,8 @@ export class ExecuteToolScope extends OpenTelemetryScope { sourceMetadata?: Pick, parentContext?: ParentContext, startTime?: TimeInput, - endTime?: TimeInput + endTime?: TimeInput, + callerDetails?: CallerDetails ) { super( SpanKind.INTERNAL, @@ -57,7 +60,8 @@ export class ExecuteToolScope extends OpenTelemetryScope { tenantDetails, parentContext, startTime, - endTime + endTime, + callerDetails ); // Destructure the details object to match C# pattern diff --git a/packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts b/packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts index 3973cc91..fc4df2a7 100644 --- a/packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts +++ b/packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts @@ -8,7 +8,8 @@ import { InferenceDetails, AgentDetails, TenantDetails, - SourceMetadata + SourceMetadata, + CallerDetails } from '../contracts'; import { ParentContext } from '../context/trace-context-propagation'; @@ -27,6 +28,7 @@ export class InferenceScope extends OpenTelemetryScope { * Accepts a ParentSpanRef (manual traceId/spanId) or an OTel Context (e.g. from extractTraceContext). * @param startTime Optional explicit start time (ms epoch, Date, or HrTime). * @param endTime Optional explicit end time (ms epoch, Date, or HrTime). + * @param callerDetails Optional caller details. * @returns A new InferenceScope instance */ public static start( @@ -37,9 +39,10 @@ export class InferenceScope extends OpenTelemetryScope { sourceMetadata?: Pick, parentContext?: ParentContext, startTime?: TimeInput, - endTime?: TimeInput + endTime?: TimeInput, + callerDetails?: CallerDetails ): InferenceScope { - return new InferenceScope(details, agentDetails, tenantDetails, conversationId, sourceMetadata, parentContext, startTime, endTime); + return new InferenceScope(details, agentDetails, tenantDetails, conversationId, sourceMetadata, parentContext, startTime, endTime, callerDetails); } private constructor( @@ -50,7 +53,8 @@ export class InferenceScope extends OpenTelemetryScope { sourceMetadata?: Pick, parentContext?: ParentContext, startTime?: TimeInput, - endTime?: TimeInput + endTime?: TimeInput, + callerDetails?: CallerDetails ) { super( SpanKind.CLIENT, @@ -60,7 +64,8 @@ export class InferenceScope extends OpenTelemetryScope { tenantDetails, parentContext, startTime, - endTime + endTime, + callerDetails ); // Set core inference information matching C# implementation @@ -71,9 +76,10 @@ export class InferenceScope extends OpenTelemetryScope { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_USAGE_OUTPUT_TOKENS_KEY, details.outputTokens?.toString()); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_RESPONSE_FINISH_REASONS_KEY, details.finishReasons?.join(',')); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_RESPONSE_ID_KEY, details.responseId); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_THOUGHT_PROCESS_KEY, details.thoughtProcess); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, conversationId); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, sourceMetadata?.name); - this.setTagMaybe(OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, sourceMetadata?.description); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, sourceMetadata?.description); } /** diff --git a/packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts b/packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts index cc161466..a5359663 100644 --- a/packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts +++ b/packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts @@ -59,12 +59,19 @@ export class InvokeAgentScope extends OpenTelemetryScope { tenantDetails, parentContext, startTime, - endTime + endTime, + callerDetails ); // Set session ID and endpoint information this.setTagMaybe(OpenTelemetryConstants.SESSION_ID_KEY, invokeAgentDetails.sessionId); + // New schema: agent type and blueprint ID are InvokeAgent-only (base class skips them) + if (OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_TYPE_KEY, invokeAgentDetails.agentType); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, invokeAgentDetails.agentBlueprintId); + } + if (invokeAgentDetails.endpoint) { this.setTagMaybe(OpenTelemetryConstants.SERVER_ADDRESS_KEY, invokeAgentDetails.endpoint.host); @@ -77,7 +84,8 @@ export class InvokeAgentScope extends OpenTelemetryScope { // Set request-related tags const requestToUse = invokeAgentDetails.request; if (requestToUse) { - if (requestToUse.executionType) { + // gen_ai.execution.type is removed in new schema + if (!OpenTelemetryConstants.isNewTelemetrySchemaEnabled && requestToUse.executionType) { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_EXECUTION_TYPE_KEY, requestToUse.executionType.toString()); } if (requestToUse.sourceMetadata) { @@ -89,15 +97,6 @@ export class InvokeAgentScope extends OpenTelemetryScope { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, invokeAgentDetails.conversationId); - // Set caller details tags - if (callerDetails) { - this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY, callerDetails.callerId); - this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY, callerDetails.callerUpn); - this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, callerDetails.callerName); - this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_TENANT_ID_KEY, callerDetails.tenantId); - this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY, callerDetails.callerClientIp); - } - // Set caller agent details tags if (callerAgentDetails) { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_AGENT_NAME_KEY, callerAgentDetails.agentName); diff --git a/packages/agents-a365-observability/src/tracing/scopes/OpenTelemetryScope.ts b/packages/agents-a365-observability/src/tracing/scopes/OpenTelemetryScope.ts index b539af14..141cadef 100644 --- a/packages/agents-a365-observability/src/tracing/scopes/OpenTelemetryScope.ts +++ b/packages/agents-a365-observability/src/tracing/scopes/OpenTelemetryScope.ts @@ -3,7 +3,7 @@ import { trace, SpanKind, Span, SpanStatusCode, Attributes, context, AttributeValue, SpanContext, TimeInput } from '@opentelemetry/api'; import { OpenTelemetryConstants } from '../constants'; -import { AgentDetails, TenantDetails } from '../contracts'; +import { AgentDetails, TenantDetails, CallerDetails } from '../contracts'; import { createContextWithParentSpanRef } from '../context/parent-span-context'; import { ParentContext, isParentSpanRef } from '../context/trace-context-propagation'; import logger from '../../utils/logging'; @@ -46,7 +46,8 @@ export abstract class OpenTelemetryScope implements Disposable { tenantDetails?: TenantDetails, parentContext?: ParentContext, startTime?: TimeInput, - endTime?: TimeInput + endTime?: TimeInput, + callerDetails?: CallerDetails ) { // Determine the context to use for span creation let currentContext = context.active(); @@ -85,20 +86,31 @@ export abstract class OpenTelemetryScope implements Disposable { if (agentDetails) { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, agentDetails.agentId); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY, agentDetails.agentName); - this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_TYPE_KEY, agentDetails.agentType); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_DESCRIPTION_KEY, agentDetails.agentDescription); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_PLATFORM_ID_KEY, agentDetails.platformId); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, agentDetails.conversationId); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_ICON_URI_KEY, agentDetails.iconUri); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY, agentDetails.agentAUID); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_UPN_KEY, agentDetails.agentUPN); - this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, agentDetails.agentBlueprintId); + + if (!OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_TYPE_KEY, agentDetails.agentType); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, agentDetails.agentBlueprintId); + } } // Set tenant details if provided if (tenantDetails) { this.setTagMaybe(OpenTelemetryConstants.TENANT_ID_KEY, tenantDetails.tenantId); } + + // Set caller details if provided + if (callerDetails) { + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY, callerDetails.callerId); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY, callerDetails.callerUpn); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, callerDetails.callerName); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY, callerDetails.callerClientIp); + } } /** diff --git a/tests/observability/core/SpanProcessor.test.ts b/tests/observability/core/SpanProcessor.test.ts index eb4444d0..c0ace2d5 100644 --- a/tests/observability/core/SpanProcessor.test.ts +++ b/tests/observability/core/SpanProcessor.test.ts @@ -162,14 +162,18 @@ describe('SpanProcessor', () => { describe('attribute registry application', () => { it('should apply all generic attributes', () => { expect(GENERIC_ATTRIBUTES).toContain(OpenTelemetryConstants.TENANT_ID_KEY); - expect(GENERIC_ATTRIBUTES).toContain(OpenTelemetryConstants.CORRELATION_ID_KEY); + if (!OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + expect(GENERIC_ATTRIBUTES).toContain(OpenTelemetryConstants.CORRELATION_ID_KEY); + } expect(GENERIC_ATTRIBUTES).toContain(OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY); expect(GENERIC_ATTRIBUTES).toContain(OpenTelemetryConstants.SESSION_ID_KEY); }); it('should apply invoke agent specific attributes', () => { expect(INVOKE_AGENT_ATTRIBUTES).toContain(OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY); - expect(INVOKE_AGENT_ATTRIBUTES).toContain(OpenTelemetryConstants.GEN_AI_EXECUTION_TYPE_KEY); + if (!OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + expect(INVOKE_AGENT_ATTRIBUTES).toContain(OpenTelemetryConstants.GEN_AI_EXECUTION_TYPE_KEY); + } }); }); diff --git a/tests/observability/core/scopes.test.ts b/tests/observability/core/scopes.test.ts index 14d7fad1..32b0a78b 100644 --- a/tests/observability/core/scopes.test.ts +++ b/tests/observability/core/scopes.test.ts @@ -224,16 +224,42 @@ describe('Scopes', () => { describe('ExecuteToolScope', () => { it('should create scope with tool details', () => { + const spy = jest.spyOn(OpenTelemetryScope.prototype as any, 'setTagMaybe'); + const callerDetails: CallerDetails = { + callerId: 'caller-tool-1', + callerUpn: 'tool.user@contoso.com', + callerName: 'Tool User', + tenantId: 'tool-tenant', + callerClientIp: '10.0.0.10' + }; const scope = ExecuteToolScope.start({ toolName: 'test-tool', arguments: '{"param": "value"}', toolCallId: 'call-123', description: 'A test tool', toolType: 'test' - }, testAgentDetails, testTenantDetails); + }, testAgentDetails, testTenantDetails, undefined, undefined, undefined, undefined, undefined, callerDetails); expect(scope).toBeInstanceOf(ExecuteToolScope); + const calls = spy.mock.calls.map(args => ({ key: args[0], val: args[1] })); + expect(calls).toEqual(expect.arrayContaining([ + expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY, val: 'caller-tool-1' }), + expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, val: 'Tool User' }), + expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY, val: '10.0.0.10' }) + ])); + // Validate raw attribute key strings for schema correctness + const keySet = new Set(calls.map(c => c.key)); + if (OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + expect(keySet).toContain('microsoft.caller.id'); + expect(keySet).toContain('microsoft.caller.name'); + expect(keySet).toContain('client.address'); + } else { + expect(keySet).toContain('gen_ai.caller.id'); + expect(keySet).toContain('gen_ai.caller.name'); + expect(keySet).toContain('gen_ai.caller.client.ip'); + } scope?.dispose(); + spy.mockRestore(); }); it('should record response', () => { @@ -277,6 +303,14 @@ describe('Scopes', () => { describe('InferenceScope', () => { it('should create scope with inference details', () => { + const spy = jest.spyOn(OpenTelemetryScope.prototype as any, 'setTagMaybe'); + const callerDetails: CallerDetails = { + callerId: 'caller-inf-1', + callerUpn: 'inf.user@contoso.com', + callerName: 'Inf User', + tenantId: 'inf-tenant', + callerClientIp: '10.0.0.20' + }; const inferenceDetails: InferenceDetails = { operationName: InferenceOperationType.CHAT, model: 'gpt-4', @@ -286,11 +320,29 @@ describe('Scopes', () => { responseId: 'resp-123', finishReasons: ['stop'] }; - - const scope = InferenceScope.start(inferenceDetails, testAgentDetails, testTenantDetails); + + const scope = InferenceScope.start(inferenceDetails, testAgentDetails, testTenantDetails, undefined, undefined, undefined, undefined, undefined, callerDetails); expect(scope).toBeInstanceOf(InferenceScope); + const calls = spy.mock.calls.map(args => ({ key: args[0], val: args[1] })); + expect(calls).toEqual(expect.arrayContaining([ + expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY, val: 'caller-inf-1' }), + expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, val: 'Inf User' }), + expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY, val: '10.0.0.20' }) + ])); + // Validate raw attribute key strings for schema correctness + const keySet = new Set(calls.map(c => c.key)); + if (OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + expect(keySet).toContain('microsoft.caller.id'); + expect(keySet).toContain('microsoft.caller.name'); + expect(keySet).toContain('client.address'); + } else { + expect(keySet).toContain('gen_ai.caller.id'); + expect(keySet).toContain('gen_ai.caller.name'); + expect(keySet).toContain('gen_ai.caller.client.ip'); + } scope?.dispose(); + spy.mockRestore(); }); it('should create scope with minimal details', () => { diff --git a/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts b/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts index f6d65821..37e117e7 100644 --- a/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts +++ b/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts @@ -44,7 +44,6 @@ describe('BaggageBuilderUtils', () => { expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY]).toBe('aad-object-1'); expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY]).toBe('User One'); expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY]).toBe('agentic-user-1'); - expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_TENANT_ID_KEY]).toBe('tenant1'); expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBe('agent-app-1'); expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY]).toBe('Agent One'); expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY]).toBe('aad-object-2'); diff --git a/tests/observability/extension/hosting/scope-utils.test.ts b/tests/observability/extension/hosting/scope-utils.test.ts index a5ad82f7..f706f962 100644 --- a/tests/observability/extension/hosting/scope-utils.test.ts +++ b/tests/observability/extension/hosting/scope-utils.test.ts @@ -61,21 +61,22 @@ describe('ScopeUtils.populateFromTurnContext', () => { const scope = ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx) as InferenceScope; expect(scope).toBeInstanceOf(InferenceScope); const calls = spy.mock.calls.map(args => [args[0], args[1]]); - expect(calls).toEqual( - expect.arrayContaining([ - [OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, 'conv-A'], - [OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, 'web'], - [OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, 'https://web'], - [OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY, 'Agent One'], - [OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY, 'agent-oid'], - [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, 'agent-1'], - [OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, 'agent-blueprint-1'], - [OpenTelemetryConstants.GEN_AI_AGENT_UPN_KEY, 'agent-upn@contoso.com'], - [OpenTelemetryConstants.GEN_AI_AGENT_DESCRIPTION_KEY, 'assistant'], - [OpenTelemetryConstants.TENANT_ID_KEY, 'tenant-123'], - [OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, 'input text'] - ]) - ); + const expected = [ + [OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, 'conv-A'], + [OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, 'web'], + [OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, 'https://web'], + [OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY, 'Agent One'], + [OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY, 'agent-oid'], + [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, 'agent-1'], + [OpenTelemetryConstants.GEN_AI_AGENT_UPN_KEY, 'agent-upn@contoso.com'], + [OpenTelemetryConstants.GEN_AI_AGENT_DESCRIPTION_KEY, 'assistant'], + [OpenTelemetryConstants.TENANT_ID_KEY, 'tenant-123'], + [OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, 'input text'] + ]; + if (!OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + expected.push([OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, 'agent-blueprint-1']); + } + expect(calls).toEqual(expect.arrayContaining(expected)); scope?.dispose(); }); @@ -124,27 +125,27 @@ describe('ScopeUtils.populateFromTurnContext', () => { const scope = ScopeUtils.populateInvokeAgentScopeFromTurnContext(details, ctx) as InvokeAgentScope; expect(scope).toBeInstanceOf(InvokeAgentScope); const calls = spy.mock.calls.map(args => [args[0], args[1]]); - expect(calls).toEqual( - expect.arrayContaining([ - [OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, 'conv-B'], - [OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, 'teams'], - [OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, 'https://teams'], - [OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY, 'user-oid'], - [OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, 'Test User'], - [OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY, 'user@contoso.com'], - [OpenTelemetryConstants.GEN_AI_CALLER_TENANT_ID_KEY, 'tenant-xyz'], - [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_USER_ID_KEY, 'user-oid'], - [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_NAME_KEY, 'Test User'], - [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_ID_KEY, 'callerAgent-1'], - [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, 'caller-agentBlueprintId'], - [OpenTelemetryConstants.TENANT_ID_KEY, 'tenant-123'], - [OpenTelemetryConstants.GEN_AI_EXECUTION_TYPE_KEY, ExecutionType.Agent2Agent.toString()], - [OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, 'invoke message'], - [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, 'agent-1'], - [OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY, 'Agent One'], - [OpenTelemetryConstants.GEN_AI_AGENT_DESCRIPTION_KEY, 'assistant'] - ]) - ); + const expected = [ + [OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, 'conv-B'], + [OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, 'teams'], + [OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, 'https://teams'], + [OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY, 'user-oid'], + [OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, 'Test User'], + [OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY, 'user@contoso.com'], + [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_USER_ID_KEY, 'user-oid'], + [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_NAME_KEY, 'Test User'], + [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_ID_KEY, 'callerAgent-1'], + [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, 'caller-agentBlueprintId'], + [OpenTelemetryConstants.TENANT_ID_KEY, 'tenant-123'], + [OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, 'invoke message'], + [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, 'agent-1'], + [OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY, 'Agent One'], + [OpenTelemetryConstants.GEN_AI_AGENT_DESCRIPTION_KEY, 'assistant'] + ]; + if (!OpenTelemetryConstants.isNewTelemetrySchemaEnabled) { + expected.push([OpenTelemetryConstants.GEN_AI_EXECUTION_TYPE_KEY, ExecutionType.Agent2Agent.toString()]); + } + expect(calls).toEqual(expect.arrayContaining(expected)); scope?.dispose(); }); diff --git a/tests/observability/tracing/exporter-utils.test.ts b/tests/observability/tracing/exporter-utils.test.ts index 6dd9b365..4d743cd3 100644 --- a/tests/observability/tracing/exporter-utils.test.ts +++ b/tests/observability/tracing/exporter-utils.test.ts @@ -6,6 +6,7 @@ import { SpanKind, SpanStatusCode } from '@opentelemetry/api'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { ClusterCategory, DefaultConfigurationProvider } from '@microsoft/agents-a365-runtime'; import { ObservabilityConfiguration } from '@microsoft/agents-a365-observability/src/configuration/ObservabilityConfiguration'; +import { OpenTelemetryConstants } from '@microsoft/agents-a365-observability/src/tracing/constants'; describe('exporter/utils', () => { const originalEnv = process.env; @@ -454,8 +455,8 @@ describe('exporter/utils', () => { ended: true, status: { code: SpanStatusCode.OK }, attributes: { - ...(tenantId !== undefined && { 'tenant.id': tenantId }), - ...(agentId !== undefined && { 'gen_ai.agent.id': agentId }), + ...(tenantId !== undefined && { [OpenTelemetryConstants.TENANT_ID_KEY]: tenantId }), + ...(agentId !== undefined && { [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: agentId }), }, links: [], events: [], From c79dd4f84c0fed82b0d57f3d537c01ee1103c329 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 18 Feb 2026 17:47:21 -0800 Subject: [PATCH 2/5] Derive TELEMETRY_SDK_VERSION_VALUE from package.json version Add prebuild script to generate src/version.ts from package.json (same pattern as agents-a365-runtime) so the telemetry SDK version stays correct automatically instead of being hardcoded. Co-Authored-By: Claude Opus 4.6 --- packages/agents-a365-observability/package.json | 1 + packages/agents-a365-observability/src/tracing/constants.ts | 4 +++- packages/agents-a365-observability/src/version.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/agents-a365-observability/src/version.ts diff --git a/packages/agents-a365-observability/package.json b/packages/agents-a365-observability/package.json index 4c6028c8..74e02e11 100644 --- a/packages/agents-a365-observability/package.json +++ b/packages/agents-a365-observability/package.json @@ -6,6 +6,7 @@ "module": "dist/esm/index.js", "types": "dist/esm/index.d.ts", "scripts": { + "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", "build:cjs": "npx tsc --project tsconfig.cjs.json", "build:esm": "npx tsc --project tsconfig.esm.json", "build": "npm run build:cjs && npm run build:esm", diff --git a/packages/agents-a365-observability/src/tracing/constants.ts b/packages/agents-a365-observability/src/tracing/constants.ts index 4f3a371e..48aec795 100644 --- a/packages/agents-a365-observability/src/tracing/constants.ts +++ b/packages/agents-a365-observability/src/tracing/constants.ts @@ -2,6 +2,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------------ +import { LIB_VERSION } from '../version'; + /** * OpenTelemetry constants for Agent 365 */ @@ -126,7 +128,7 @@ export class OpenTelemetryConstants { public static readonly TELEMETRY_SDK_VERSION_KEY = 'telemetry.sdk.version'; public static readonly TELEMETRY_SDK_NAME_VALUE = 'Agent365Sdk'; public static readonly TELEMETRY_SDK_LANGUAGE_VALUE = 'nodejs'; - public static readonly TELEMETRY_SDK_VERSION_VALUE = '0.0.0-placeholder'; + public static readonly TELEMETRY_SDK_VERSION_VALUE = LIB_VERSION; } /** diff --git a/packages/agents-a365-observability/src/version.ts b/packages/agents-a365-observability/src/version.ts new file mode 100644 index 00000000..da17395a --- /dev/null +++ b/packages/agents-a365-observability/src/version.ts @@ -0,0 +1 @@ +export const LIB_VERSION = "0.0.0-placeholder"; From f3fdb15258f57f99e923a9d944f218536cb6a6c1 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 18 Feb 2026 18:11:43 -0800 Subject: [PATCH 3/5] Add isolated-module tests for caller attribute key schema mappings Use jest.isolateModules to validate raw old/new schema key strings for caller attributes, catching regressions in constant definitions regardless of which schema is active at runtime. Also skip GEN_AI_AGENT_USER_ID_KEY from GENERIC_ATTRIBUTES in new schema to avoid duplicate propagation (collides with GEN_AI_AGENT_AUID_KEY). Co-Authored-By: Claude Opus 4.6 --- .../src/tracing/processors/util.ts | 4 ++- tests/observability/core/scopes.test.ts | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/agents-a365-observability/src/tracing/processors/util.ts b/packages/agents-a365-observability/src/tracing/processors/util.ts index e736ed58..2dab89c1 100644 --- a/packages/agents-a365-observability/src/tracing/processors/util.ts +++ b/packages/agents-a365-observability/src/tracing/processors/util.ts @@ -21,7 +21,9 @@ export const GENERIC_ATTRIBUTES: readonly string[] = [ ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_AGENT_TYPE_KEY]), consts.GEN_AI_AGENT_DESCRIPTION_KEY, consts.SESSION_DESCRIPTION_KEY, - consts.GEN_AI_AGENT_USER_ID_KEY, + // In new schema, GEN_AI_AGENT_USER_ID_KEY and GEN_AI_AGENT_AUID_KEY both resolve to + // 'microsoft.a365.agent.user.id', so skip USER_ID_KEY to avoid duplicate propagation. + ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_AGENT_USER_ID_KEY]), consts.GEN_AI_AGENT_UPN_KEY, ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_AGENT_BLUEPRINT_ID_KEY]), consts.GEN_AI_AGENT_AUID_KEY, diff --git a/tests/observability/core/scopes.test.ts b/tests/observability/core/scopes.test.ts index 32b0a78b..bd3d952b 100644 --- a/tests/observability/core/scopes.test.ts +++ b/tests/observability/core/scopes.test.ts @@ -563,3 +563,38 @@ describe('Scopes', () => { }); }); }); + +// Validate raw caller attribute key strings for both old and new schemas +// using jest.isolateModules to re-evaluate constants with different env var values. +describe('Caller attribute key schema mappings', () => { + const loadConstants = (useNewSchema: boolean) => { + let consts: typeof OpenTelemetryConstants; + const original = process.env['A365_USE_NEW_TELEMETRY_SCHEMA']; + process.env['A365_USE_NEW_TELEMETRY_SCHEMA'] = useNewSchema ? 'true' : ''; + jest.isolateModules(() => { + consts = require('@microsoft/agents-a365-observability').OpenTelemetryConstants; + }); + if (original === undefined) { + delete process.env['A365_USE_NEW_TELEMETRY_SCHEMA']; + } else { + process.env['A365_USE_NEW_TELEMETRY_SCHEMA'] = original; + } + return consts!; + }; + + it('old schema uses gen_ai.* caller key names', () => { + const consts = loadConstants(false); + expect(consts.GEN_AI_CALLER_ID_KEY).toBe('gen_ai.caller.id'); + expect(consts.GEN_AI_CALLER_NAME_KEY).toBe('gen_ai.caller.name'); + expect(consts.GEN_AI_CALLER_UPN_KEY).toBe('gen_ai.caller.upn'); + expect(consts.GEN_AI_CALLER_CLIENT_IP_KEY).toBe('gen_ai.caller.client.ip'); + }); + + it('new schema uses microsoft.* / client.* caller key names', () => { + const consts = loadConstants(true); + expect(consts.GEN_AI_CALLER_ID_KEY).toBe('microsoft.caller.id'); + expect(consts.GEN_AI_CALLER_NAME_KEY).toBe('microsoft.caller.name'); + expect(consts.GEN_AI_CALLER_UPN_KEY).toBe('microsoft.caller.upn'); + expect(consts.GEN_AI_CALLER_CLIENT_IP_KEY).toBe('client.address'); + }); +}); From 64c28a7b713a0bf98aeba79c9c3e56c370371cbd Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 18 Feb 2026 18:45:25 -0800 Subject: [PATCH 4/5] Add back gen_ai.caller.tenantid, add isolated schema tests for caller agent keys - Restore GEN_AI_CALLER_TENANT_ID_KEY and wire it through base scope, INVOKE_AGENT_ATTRIBUTES, and TurnContextUtils baggage pairs - Add isolated-module tests validating caller agent key schema mappings - Skip GEN_AI_AGENT_USER_ID_KEY from GENERIC_ATTRIBUTES in new schema to avoid duplicate propagation with GEN_AI_AGENT_AUID_KEY Co-Authored-By: Claude Opus 4.6 --- .../src/utils/TurnContextUtils.ts | 1 + .../src/tracing/constants.ts | 1 + .../src/tracing/processors/util.ts | 1 + .../src/tracing/scopes/OpenTelemetryScope.ts | 1 + tests/observability/core/scopes.test.ts | 14 ++++++++++++++ 5 files changed, 18 insertions(+) diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 567782e2..981d4ead 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -33,6 +33,7 @@ export function getCallerBaggagePairs(turnContext: TurnContext): Array<[string, [OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY, from.aadObjectId], [OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, from.name], [OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY, upn], + [OpenTelemetryConstants.GEN_AI_CALLER_TENANT_ID_KEY, from.tenantId], [OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, from.agenticAppBlueprintId] ]; return normalizePairs(pairs); diff --git a/packages/agents-a365-observability/src/tracing/constants.ts b/packages/agents-a365-observability/src/tracing/constants.ts index 48aec795..5d450e72 100644 --- a/packages/agents-a365-observability/src/tracing/constants.ts +++ b/packages/agents-a365-observability/src/tracing/constants.ts @@ -82,6 +82,7 @@ export class OpenTelemetryConstants { public static readonly GEN_AI_CALLER_NAME_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.caller.name' : 'gen_ai.caller.name'; public static readonly GEN_AI_CALLER_UPN_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.caller.upn' : 'gen_ai.caller.upn'; public static readonly GEN_AI_CALLER_CLIENT_IP_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'client.address' : 'gen_ai.caller.client.ip'; + public static readonly GEN_AI_CALLER_TENANT_ID_KEY = 'gen_ai.caller.tenantid'; // Agent to Agent caller agent dimensions public static readonly GEN_AI_CALLER_AGENT_USER_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.caller.agent.user.id' : 'gen_ai.caller.agent.userid'; diff --git a/packages/agents-a365-observability/src/tracing/processors/util.ts b/packages/agents-a365-observability/src/tracing/processors/util.ts index 2dab89c1..3fe4a857 100644 --- a/packages/agents-a365-observability/src/tracing/processors/util.ts +++ b/packages/agents-a365-observability/src/tracing/processors/util.ts @@ -39,6 +39,7 @@ export const INVOKE_AGENT_ATTRIBUTES: readonly string[] = [ consts.GEN_AI_CALLER_NAME_KEY, consts.GEN_AI_CALLER_UPN_KEY, consts.GEN_AI_CALLER_CLIENT_IP_KEY, + consts.GEN_AI_CALLER_TENANT_ID_KEY, // Caller Agent (A2A) attributes consts.GEN_AI_CALLER_AGENT_ID_KEY, consts.GEN_AI_CALLER_AGENT_NAME_KEY, diff --git a/packages/agents-a365-observability/src/tracing/scopes/OpenTelemetryScope.ts b/packages/agents-a365-observability/src/tracing/scopes/OpenTelemetryScope.ts index 141cadef..0e1033e6 100644 --- a/packages/agents-a365-observability/src/tracing/scopes/OpenTelemetryScope.ts +++ b/packages/agents-a365-observability/src/tracing/scopes/OpenTelemetryScope.ts @@ -110,6 +110,7 @@ export abstract class OpenTelemetryScope implements Disposable { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY, callerDetails.callerUpn); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, callerDetails.callerName); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY, callerDetails.callerClientIp); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_TENANT_ID_KEY, callerDetails.tenantId); } } diff --git a/tests/observability/core/scopes.test.ts b/tests/observability/core/scopes.test.ts index bd3d952b..532399dd 100644 --- a/tests/observability/core/scopes.test.ts +++ b/tests/observability/core/scopes.test.ts @@ -597,4 +597,18 @@ describe('Caller attribute key schema mappings', () => { expect(consts.GEN_AI_CALLER_UPN_KEY).toBe('microsoft.caller.upn'); expect(consts.GEN_AI_CALLER_CLIENT_IP_KEY).toBe('client.address'); }); + + it('old schema uses gen_ai.* caller agent key names', () => { + const consts = loadConstants(false); + expect(consts.GEN_AI_CALLER_AGENT_ID_KEY).toBe('gen_ai.caller.agent.id'); + expect(consts.GEN_AI_CALLER_AGENT_NAME_KEY).toBe('gen_ai.caller.agent.name'); + expect(consts.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY).toBe('gen_ai.caller.agent.applicationid'); + }); + + it('new schema uses microsoft.a365.* caller agent key names', () => { + const consts = loadConstants(true); + expect(consts.GEN_AI_CALLER_AGENT_ID_KEY).toBe('microsoft.a365.caller.agent.id'); + expect(consts.GEN_AI_CALLER_AGENT_NAME_KEY).toBe('microsoft.a365.caller.agent.name'); + expect(consts.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY).toBe('microsoft.a365.caller.agent.blueprint.id'); + }); }); From 00cd584f200c1e99a500229ff36aa91c1b4b2793 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Wed, 18 Feb 2026 18:58:15 -0800 Subject: [PATCH 5/5] Remove GEN_AI_AGENT_USER_ID_KEY constant This constant duplicated GEN_AI_AGENT_AUID_KEY functionality. The .NET SDK uses gen_ai.agent.userid as GenAiAgentAUIDKey, so only GEN_AI_AGENT_AUID_KEY is needed. Co-Authored-By: Claude Opus 4.6 --- packages/agents-a365-observability/src/tracing/constants.ts | 1 - .../agents-a365-observability/src/tracing/processors/util.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/packages/agents-a365-observability/src/tracing/constants.ts b/packages/agents-a365-observability/src/tracing/constants.ts index 5d450e72..0852f789 100644 --- a/packages/agents-a365-observability/src/tracing/constants.ts +++ b/packages/agents-a365-observability/src/tracing/constants.ts @@ -77,7 +77,6 @@ export class OpenTelemetryConstants { public static readonly GEN_AI_TOOL_TYPE_KEY = 'gen_ai.tool.type'; // Agent user (user tied to agent instance during creation) or caller dimensions - public static readonly GEN_AI_AGENT_USER_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.a365.agent.user.id' : 'gen_ai.agent.userid'; public static readonly GEN_AI_CALLER_ID_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.caller.id' : 'gen_ai.caller.id'; public static readonly GEN_AI_CALLER_NAME_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.caller.name' : 'gen_ai.caller.name'; public static readonly GEN_AI_CALLER_UPN_KEY = OpenTelemetryConstants.isNewTelemetrySchemaEnabled ? 'microsoft.caller.upn' : 'gen_ai.caller.upn'; diff --git a/packages/agents-a365-observability/src/tracing/processors/util.ts b/packages/agents-a365-observability/src/tracing/processors/util.ts index 3fe4a857..567d84ac 100644 --- a/packages/agents-a365-observability/src/tracing/processors/util.ts +++ b/packages/agents-a365-observability/src/tracing/processors/util.ts @@ -21,9 +21,6 @@ export const GENERIC_ATTRIBUTES: readonly string[] = [ ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_AGENT_TYPE_KEY]), consts.GEN_AI_AGENT_DESCRIPTION_KEY, consts.SESSION_DESCRIPTION_KEY, - // In new schema, GEN_AI_AGENT_USER_ID_KEY and GEN_AI_AGENT_AUID_KEY both resolve to - // 'microsoft.a365.agent.user.id', so skip USER_ID_KEY to avoid duplicate propagation. - ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_AGENT_USER_ID_KEY]), consts.GEN_AI_AGENT_UPN_KEY, ...(consts.isNewTelemetrySchemaEnabled ? [] : [consts.GEN_AI_AGENT_BLUEPRINT_ID_KEY]), consts.GEN_AI_AGENT_AUID_KEY,