Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 true, the gen_ai.input.messages attribute containing LLM input messages
* will be suppressed and not attached to spans in InvokeAgent scopes.
* Defaults to false.
*/
suppressInvokeAgentInput?: boolean;
Comment thread
fpfp100 marked this conversation as resolved.
}

/**
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, {
suppressInvokeAgentInput: this._config.suppressInvokeAgentInput ?? 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 suppressInvokeAgentInput: 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?: { suppressInvokeAgentInput?: boolean }) {
this.tracer = tracer;
this.suppressInvokeAgentInput = options?.suppressInvokeAgentInput ?? false;
}

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

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

}

if (inputObj) {
if (inputObj && !this.suppressInvokeAgentInput) {
if (typeof inputObj === 'string') {
otelSpan.setAttribute(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, inputObj);
} else if (Array.isArray(inputObj)) {
Expand Down Expand Up @@ -279,7 +280,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.suppressInvokeAgentInput) {
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, { suppressInvokeAgentInput: true });
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, { suppressInvokeAgentInput: true });
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.
});