diff --git a/packages/agents-a365-observability/src/tracing/constants.ts b/packages/agents-a365-observability/src/tracing/constants.ts index c1539d8b..42646711 100644 --- a/packages/agents-a365-observability/src/tracing/constants.ts +++ b/packages/agents-a365-observability/src/tracing/constants.ts @@ -70,6 +70,7 @@ export class OpenTelemetryConstants { 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'; // Agent to Agent caller agent dimensions public static readonly GEN_AI_CALLER_AGENT_USER_ID_KEY = 'gen_ai.caller.agent.userid'; @@ -78,7 +79,7 @@ export class OpenTelemetryConstants { 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_APPLICATION_ID_KEY = 'gen_ai.caller.agent.applicationid'; - + public static readonly GEN_AI_CALLER_AGENT_CLIENT_IP_KEY = 'gen_ai.caller.agent.user.client.ip'; // 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'; diff --git a/packages/agents-a365-observability/src/tracing/contracts.ts b/packages/agents-a365-observability/src/tracing/contracts.ts index a2db33cd..94afcd67 100644 --- a/packages/agents-a365-observability/src/tracing/contracts.ts +++ b/packages/agents-a365-observability/src/tracing/contracts.ts @@ -152,6 +152,9 @@ export interface CallerDetails { /** The tenant ID of the caller */ tenantId?: string; + + /** The client IP address for the caller */ + callerClientIp?: string; } /** @@ -169,6 +172,9 @@ export interface EnhancedAgentDetails extends AgentDetails { /** The tenant ID for the agent */ tenantId?: string; + + /** The client IP address for the agent user */ + agentClientIP?: string; } /** diff --git a/packages/agents-a365-observability/src/tracing/middleware/BaggageBuilder.ts b/packages/agents-a365-observability/src/tracing/middleware/BaggageBuilder.ts index f0640fd4..5df88300 100644 --- a/packages/agents-a365-observability/src/tracing/middleware/BaggageBuilder.ts +++ b/packages/agents-a365-observability/src/tracing/middleware/BaggageBuilder.ts @@ -179,6 +179,11 @@ export class BaggageBuilder { return this; } + callerClientIp(value: string | null | undefined): BaggageBuilder { + this.set(OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY, value); + return this; + } + /** * Set the conversation ID baggage value. * @param value The conversation ID diff --git a/packages/agents-a365-observability/src/tracing/processors/util.ts b/packages/agents-a365-observability/src/tracing/processors/util.ts index 3738842c..1e0bc1b3 100644 --- a/packages/agents-a365-observability/src/tracing/processors/util.ts +++ b/packages/agents-a365-observability/src/tracing/processors/util.ts @@ -36,6 +36,7 @@ export const INVOKE_AGENT_ATTRIBUTES: readonly string[] = [ consts.GEN_AI_CALLER_UPN_KEY, consts.GEN_AI_CALLER_USER_ID_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, consts.GEN_AI_CALLER_AGENT_NAME_KEY, @@ -43,6 +44,7 @@ export const INVOKE_AGENT_ATTRIBUTES: readonly string[] = [ consts.GEN_AI_CALLER_AGENT_UPN_KEY, consts.GEN_AI_CALLER_AGENT_TENANT_ID_KEY, consts.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, + consts.GEN_AI_CALLER_AGENT_CLIENT_IP_KEY, // Execution context consts.GEN_AI_EXECUTION_TYPE_KEY, consts.GEN_AI_EXECUTION_SOURCE_ID_KEY, diff --git a/packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts b/packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts index 0d6b2807..60b5104a 100644 --- a/packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts +++ b/packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts @@ -83,6 +83,7 @@ export class InvokeAgentScope extends OpenTelemetryScope { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY, callerDetails.callerName); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_USER_ID_KEY, callerDetails.callerUserId); 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 @@ -93,6 +94,7 @@ export class InvokeAgentScope extends OpenTelemetryScope { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_AGENT_USER_ID_KEY, callerAgentDetails.agentAUID); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_AGENT_UPN_KEY, callerAgentDetails.agentUPN); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_AGENT_TENANT_ID_KEY, callerAgentDetails.tenantId); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CALLER_AGENT_CLIENT_IP_KEY, callerAgentDetails.agentClientIP); } } diff --git a/tests/observability/core/BaggageBuilder.test.ts b/tests/observability/core/BaggageBuilder.test.ts index 43f255c5..c91610a7 100644 --- a/tests/observability/core/BaggageBuilder.test.ts +++ b/tests/observability/core/BaggageBuilder.test.ts @@ -62,12 +62,18 @@ describe('BaggageBuilder', () => { const builder = new BaggageBuilder(); const pairs: Array<[string, string]> = [ [OpenTelemetryConstants.TENANT_ID_KEY, 'tenant-123'], - [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, 'agent-456'] + [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, 'agent-456'], + [OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY, '10.0.0.5'] ]; builder.setPairs(pairs); const scope = builder.build(); expect(scope).toBeInstanceOf(BaggageScope); + + const bag = propagation.getBaggage((scope as any).contextWithBaggage); + expect(bag?.getEntry(OpenTelemetryConstants.TENANT_ID_KEY)?.value).toBe('tenant-123'); + expect(bag?.getEntry(OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY)?.value).toBe('agent-456'); + expect(bag?.getEntry(OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY)?.value).toBe('10.0.0.5'); }); it('should ignore null values', () => { diff --git a/tests/observability/core/agent365-exporter.test.ts b/tests/observability/core/agent365-exporter.test.ts index 7f1fc265..8a043666 100644 --- a/tests/observability/core/agent365-exporter.test.ts +++ b/tests/observability/core/agent365-exporter.test.ts @@ -77,7 +77,9 @@ describe('Agent365Exporter', () => { const spans = [ makeSpan({ [OpenTelemetryConstants.TENANT_ID_KEY]: tenantId, - [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: agentId + [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: agentId, + [OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY]: '10.0.0.5', + [OpenTelemetryConstants.GEN_AI_CALLER_AGENT_CLIENT_IP_KEY]: '1.0.0.5' }) ]; @@ -96,6 +98,8 @@ describe('Agent365Exporter', () => { expect(exportedSpan.attributes).toBeDefined(); expect(exportedSpan.attributes[OpenTelemetryConstants.TENANT_ID_KEY]).toBe(tenantId); expect(exportedSpan.attributes[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBe(agentId); + expect(exportedSpan.attributes[OpenTelemetryConstants.GEN_AI_CALLER_CLIENT_IP_KEY]).toBe('10.0.0.5'); + expect(exportedSpan.attributes[OpenTelemetryConstants.GEN_AI_CALLER_AGENT_CLIENT_IP_KEY]).toBe('1.0.0.5'); }); it('requires a tokenResolver and fails export when missing', async () => { diff --git a/tests/observability/core/scopes.test.ts b/tests/observability/core/scopes.test.ts index f99ce01d..430eb4ad 100644 --- a/tests/observability/core/scopes.test.ts +++ b/tests/observability/core/scopes.test.ts @@ -12,6 +12,8 @@ import { InferenceOperationType, CallerDetails, } from '@microsoft/agents-a365-observability'; +import { OpenTelemetryConstants } from '@microsoft/agents-a365-observability/dist/cjs/tracing/constants'; +import { OpenTelemetryScope } from '@microsoft/agents-a365-observability/dist/cjs/tracing/scopes/OpenTelemetryScope'; // Mock console to avoid cluttering test output const originalConsoleWarn = console.warn; @@ -122,6 +124,41 @@ describe('Scopes', () => { expect(() => scope?.recordError(error)).not.toThrow(); scope?.dispose(); }); + + it('should set caller and caller-agent IP tags', () => { + const spy = jest.spyOn(OpenTelemetryScope.prototype as any, 'setTagMaybe'); + const invokeAgentDetails: InvokeAgentDetails = { + agentId: 'test-agent', + agentName: 'Test Agent' + }; + const callerDetails: CallerDetails = { + callerId: 'user-123', + tenantId: 'test-tenant', + callerClientIp: '10.0.0.5' + }; + const callerAgentDetails: AgentDetails = { + agentId: 'caller-agent', + agentName: 'Caller Agent', + agentDescription: 'desc', + conversationId: 'conv', + agentClientIP: '192.168.1.100' + } as any; + + const scope1 = InvokeAgentScope.start(invokeAgentDetails, testTenantDetails, undefined, callerDetails); + expect(scope1).toBeInstanceOf(InvokeAgentScope); + const scope2 = InvokeAgentScope.start(invokeAgentDetails, testTenantDetails, callerAgentDetails, undefined); + expect(scope2).toBeInstanceOf(InvokeAgentScope); + + 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_CLIENT_IP_KEY, val: '10.0.0.5' }), + expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CALLER_AGENT_CLIENT_IP_KEY, val: '192.168.1.100' }) + ])); + + scope1?.dispose(); + scope2?.dispose(); + spy.mockRestore(); + }); }); describe('ExecuteToolScope', () => { @@ -194,6 +231,7 @@ describe('Scopes', () => { expect(() => scope?.recordFinishReasons(['stop', 'length'])).not.toThrow(); scope?.dispose(); }); + }); describe('Dispose pattern', () => {