Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export interface OpenAIAgentsInstrumentationConfig extends InstrumentationConfig
enabled?: boolean;
tracerName?: string;
tracerVersion?: string;
/**
* When false, the gen_ai.prompt attribute containing LLM input messages
* will not be attached to spans in InvokeAgent scopes.
* Defaults to true.
*/
sendPromptInInvokeAgentScopes?: boolean;
Comment thread
fpfp100 marked this conversation as resolved.
Outdated
}

/**
Expand Down Expand Up @@ -93,7 +99,9 @@ export class OpenAIAgentsTraceInstrumentor extends InstrumentationBase<OpenAIAge
// Get tracer provider
trace.getTracerProvider();

this.processor = new OpenAIAgentsTraceProcessor(agent365Tracer);
this.processor = new OpenAIAgentsTraceProcessor(agent365Tracer, {
sendPromptInInvokeAgentScopes: this._config.sendPromptInInvokeAgentScopes !== false
});

// Register the processor directly using the imported setTraceProcessors function
// This bypasses the OpenTelemetry instrumentation patching mechanism
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor {
private static readonly MAX_HANDOFFS_IN_FLIGHT = 1000;

private readonly tracer: OtelTracer;
private readonly sendPromptInInvokeAgentScopes: boolean;
private readonly rootSpans: Map<string, OtelSpan> = new Map();
private readonly otelSpans: Map<string, OtelSpan> = new Map();
private readonly tokens: Map<string, ContextToken> = new Map();
Expand All @@ -47,8 +48,9 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor {
['generation' + Constants.GEN_AI_REQUEST_CONTENT_KEY, OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY],
]);

constructor(tracer: OtelTracer) {
constructor(tracer: OtelTracer, options?: { sendPromptInInvokeAgentScopes?: boolean }) {
this.tracer = tracer;
this.sendPromptInInvokeAgentScopes = options?.sendPromptInInvokeAgentScopes ?? true;
}

private getNewKey(spanType: string, key: string): string | null {
Expand Down Expand Up @@ -244,27 +246,28 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor {

const modelName = attrs[OpenTelemetryConstants.GEN_AI_REQUEST_MODEL_KEY] ?? '';
otelSpan.updateName(`${InferenceOperationType.CHAT} ${modelName}`);

}

if (inputObj) {
if (typeof inputObj === 'string') {
otelSpan.setAttribute(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, inputObj);
} else if (Array.isArray(inputObj)) {
// Store the complete _input structure as JSON
otelSpan.setAttribute(
OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY,
JSON.stringify(inputObj)
);
if (this.sendPromptInInvokeAgentScopes) {
if (typeof inputObj === 'string') {
otelSpan.setAttribute(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, inputObj);
} else if (Array.isArray(inputObj)) {
// Store the complete _input structure as JSON
otelSpan.setAttribute(
OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY,
JSON.stringify(inputObj)
);

// Get attributes but filter out unwanted ones
const attrs = Utils.getAttributesFromInput(inputObj);
Object.entries(attrs).forEach(([key, value]) => {
if (value !== null && value !== undefined &&
key !== Constants.GEN_AI_REQUEST_CONTENT_KEY) {
otelSpan.setAttribute(key, value as string | number | boolean);
}
});
}
});
}
}
}
}
Expand All @@ -279,7 +282,9 @@ export class OpenAIAgentsTraceProcessor implements TracingProcessor {
|| key === Constants.GEN_AI_EXECUTION_PAYLOAD_KEY;
if (value !== null && value !== undefined && !shouldExcludeKey) {
const newKey = this.getNewKey(data.type, key);
otelSpan.setAttribute(newKey || key, value as string | number | boolean);
if (newKey !== OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY || this.sendPromptInInvokeAgentScopes) {
otelSpan.setAttribute(newKey || key, value as string | number | boolean);
}
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { Tracer } from '@opentelemetry/api';
import { OpenTelemetryConstants } from '@microsoft/agents-a365-observability';
import { OpenAIAgentsTraceProcessor } from '@microsoft/agents-a365-observability-extensions-openai';
import { ObservabilityManager } from '@microsoft/agents-a365-observability';
import { trace } from '@opentelemetry/api';
Expand Down Expand Up @@ -447,4 +448,107 @@ describe('OpenAIAgentsTraceProcessor', () => {
});
});
});

describe('Prompt Suppression in InvokeAgent traces', () => {
let spansByName: Record<string, any>;
let tracerSpy: jest.SpyInstance;

const createMockSpan = (name: string) => {
const attrs: Array<[string, unknown]> = [];
return {
setAttribute: jest.fn((k: string, v: unknown) => { attrs.push([k, v]); }),
updateName: jest.fn(),
setStatus: jest.fn(),
end: jest.fn(),
spanContext: jest.fn(() => ({ traceId: 'tid-' + name, spanId: 'sid-' + name })),
_attrs: attrs,
};
};

beforeEach(() => {
spansByName = {};
tracerSpy = jest.spyOn(tracer as any, 'startSpan').mockImplementation((...args: unknown[]) => {
const name = args[0] as string;
const s = createMockSpan(name);
spansByName[name] = s;
return s;
});
});

afterEach(() => {
tracerSpy.mockRestore();
});

it('does not record GEN_AI_INPUT_MESSAGES when disabled', async () => {
const processor = new OpenAIAgentsTraceProcessor(tracer, { sendPromptInInvokeAgentScopes: false });
const traceData = { traceId: 'trace-suppress', name: 'Agent' } as any;
await processor.onTraceStart(traceData);

const agentSpan = {
spanId: 'agent-span', traceId: 'trace-suppress', startedAt: new Date().toISOString(),
spanData: { type: 'agent' as const, name: 'agent-node' },
} as any;
await processor.onSpanStart(agentSpan);
await processor.onSpanEnd(agentSpan);

const genSpan = {
spanId: 'gen-span', traceId: 'trace-suppress', startedAt: new Date().toISOString(),
spanData: { type: 'generation' as const, name: 'Generate', model: 'gpt-4', input: 'Hello prompt' },
} as any;
await processor.onSpanStart(genSpan);
await processor.onSpanEnd(genSpan);

const genMock = spansByName['Generate'];
const keys = (genMock._attrs as Array<[string, unknown]>).map(([k]) => k);
expect(keys).not.toContain(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY);
});

it('records GEN_AI_INPUT_MESSAGES when enabled (default)', async () => {
const processor = new OpenAIAgentsTraceProcessor(tracer);
const traceData = { traceId: 'trace-allow', name: 'Agent' } as any;
await processor.onTraceStart(traceData);

const agentSpan = {
spanId: 'agent-span-2', traceId: 'trace-allow', startedAt: new Date().toISOString(),
spanData: { type: 'agent' as const, name: 'agent-node-2' },
} as any;
await processor.onSpanStart(agentSpan);
await processor.onSpanEnd(agentSpan);

const genSpan = {
spanId: 'gen-span-2', traceId: 'trace-allow', startedAt: new Date().toISOString(),
spanData: { type: 'generation' as const, name: 'Generate2', model: 'gpt-4', input: 'Hello prompt' },
} as any;
await processor.onSpanStart(genSpan);
await processor.onSpanEnd(genSpan);

const genMock = spansByName['Generate2'];
const keys = (genMock._attrs as Array<[string, unknown]>).map(([k]) => k);
expect(keys).toContain(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY);
});

it('suppresses input on response spans when disabled', async () => {
const processor = new OpenAIAgentsTraceProcessor(tracer, { sendPromptInInvokeAgentScopes: false });
const traceData = { traceId: 'trace-resp', name: 'Agent' } as any;
await processor.onTraceStart(traceData);

const agentSpan = {
spanId: 'agent-span-3', traceId: 'trace-resp', startedAt: new Date().toISOString(),
spanData: { type: 'agent' as const, name: 'agent-node-3' },
} as any;
await processor.onSpanStart(agentSpan);
await processor.onSpanEnd(agentSpan);

const respSpan = {
spanId: 'resp-span', traceId: 'trace-resp', startedAt: new Date().toISOString(),
spanData: { type: 'response' as const, name: 'Response', _input: 'Prompt text', _response: { model: 'gpt-4', output: 'ok' } },
} as any;
await processor.onSpanStart(respSpan);
await processor.onSpanEnd(respSpan);

const respMock = spansByName['Response'];
const keys = (respMock._attrs as Array<[string, unknown]>).map(([k]) => k);
expect(keys).not.toContain(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY);
});
});
Comment thread
fpfp100 marked this conversation as resolved.
});