From c2605501463bc47c38ff3ae94587d66a938e0ec6 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 21 Apr 2026 14:07:32 -0400 Subject: [PATCH] feat(telemetry): add flag for enabling traces specifically (#25343) --- docs/cli/telemetry.md | 29 +++-- docs/reference/configuration.md | 6 + integration-tests/acp-telemetry.test.ts | 1 + .../a2a-server/src/utils/testing_utils.ts | 1 + packages/cli/src/config/settingsSchema.ts | 5 + packages/cli/src/test-utils/mockConfig.ts | 1 + packages/core/src/agents/agent-tool.ts | 1 + packages/core/src/config/config.ts | 6 + packages/core/src/core/geminiChat.test.ts | 1 + .../src/core/geminiChat_network_retry.test.ts | 1 + .../src/core/loggingContentGenerator.test.ts | 1 + .../core/src/core/loggingContentGenerator.ts | 3 + packages/core/src/scheduler/policy.test.ts | 1 + packages/core/src/scheduler/scheduler.test.ts | 2 + packages/core/src/scheduler/scheduler.ts | 1 + .../src/scheduler/scheduler_hooks.test.ts | 1 + .../src/scheduler/scheduler_parallel.test.ts | 1 + packages/core/src/scheduler/tool-executor.ts | 1 + packages/core/src/telemetry/config.ts | 5 + .../core/src/telemetry/conseca-logger.test.ts | 1 + packages/core/src/telemetry/loggers.test.ts | 120 +++++++++++++++++- packages/core/src/telemetry/trace.test.ts | 60 +++++++-- packages/core/src/telemetry/trace.ts | 52 ++++++-- packages/core/src/telemetry/types.ts | 23 +++- schemas/settings.schema.json | 4 + 25 files changed, 282 insertions(+), 46 deletions(-) diff --git a/docs/cli/telemetry.md b/docs/cli/telemetry.md index dd13d5eb82..2f42c16c29 100644 --- a/docs/cli/telemetry.md +++ b/docs/cli/telemetry.md @@ -35,17 +35,18 @@ The observability system provides: You control telemetry behavior through the `.gemini/settings.json` file. Environment variables can override these settings. -| Setting | Environment Variable | Description | Values | Default | -| -------------- | -------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- | -| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` | -| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` | -| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` | -| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` | -| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - | -| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` | -| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` | -| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` | -| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - | +| Setting | Environment Variable | Description | Values | Default | +| -------------- | --------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- | +| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` | +| `traces` | `GEMINI_TELEMETRY_TRACES_ENABLED` | Enable detailed attribute tracing | `true`/`false` | `false` | +| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` | +| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` | +| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` | +| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - | +| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` | +| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` | +| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` | +| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - | **Note on boolean environment variables:** For boolean settings like `enabled`, setting the environment variable to `true` or `1` enables the feature. @@ -1235,6 +1236,12 @@ These metrics follow standard [OpenTelemetry GenAI semantic conventions]. Traces provide an "under-the-hood" view of agent and backend operations. Use traces to debug tool interactions and optimize performance. + +> [!NOTE] +> Detailed trace attributes (like full prompts and tool outputs) are disabled by default +> to minimize overhead. You must explicitly set `telemetry.traces` to `true` (or set +> `GEMINI_TELEMETRY_TRACES_ENABLED=true`) to capture them. + Every trace captures rich metadata via standard span attributes.
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 97ca58be5c..d91ee20fb4 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -2012,6 +2012,8 @@ see [Telemetry](../cli/telemetry.md). - **Properties:** - **`enabled`** (boolean): Whether or not telemetry is enabled. + - **`traces`** (boolean): Whether detailed traces with large attributes (like + tool outputs and file reads) are captured. Defaults to `false`. - **`target`** (string): The destination for collected telemetry. Supported values are `local` and `gcp`. - **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter. @@ -2212,6 +2214,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file. - Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it. - Overrides the `telemetry.enabled` setting. +- **`GEMINI_TELEMETRY_TRACES_ENABLED`**: + - Set to `true` or `1` to enable detailed tracing with large attributes. Any + other value is treated as disabling it. + - Overrides the `telemetry.traces` setting. - **`GEMINI_TELEMETRY_TARGET`**: - Sets the telemetry target (`local` or `gcp`). - Overrides the `telemetry.target` setting. diff --git a/integration-tests/acp-telemetry.test.ts b/integration-tests/acp-telemetry.test.ts index f883b977bf..487dac474d 100644 --- a/integration-tests/acp-telemetry.test.ts +++ b/integration-tests/acp-telemetry.test.ts @@ -70,6 +70,7 @@ describe('ACP telemetry', () => { GEMINI_API_KEY: 'fake-key', GEMINI_CLI_HOME: rig.homeDir!, GEMINI_TELEMETRY_ENABLED: 'true', + GEMINI_TELEMETRY_TRACES_ENABLED: 'true', GEMINI_TELEMETRY_TARGET: 'local', GEMINI_TELEMETRY_OUTFILE: telemetryPath, }, diff --git a/packages/a2a-server/src/utils/testing_utils.ts b/packages/a2a-server/src/utils/testing_utils.ts index 4265805e09..c4575c89fd 100644 --- a/packages/a2a-server/src/utils/testing_utils.ts +++ b/packages/a2a-server/src/utils/testing_utils.ts @@ -98,6 +98,7 @@ export function createMockConfig( getMcpServers: vi.fn().mockReturnValue({}), }), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), getGitService: vi.fn(), validatePathAccess: vi.fn().mockReturnValue(undefined), getShellExecutionConfig: vi.fn().mockReturnValue({ diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 4d8e6f4dde..4ad0472e61 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -3060,6 +3060,11 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record< description: 'Protocol for OTLP exporters.', enum: ['grpc', 'http'], }, + traces: { + type: 'boolean', + description: + 'Whether detailed traces with large attributes are captured.', + }, logPrompts: { type: 'boolean', description: 'Whether prompts are logged in telemetry payloads.', diff --git a/packages/cli/src/test-utils/mockConfig.ts b/packages/cli/src/test-utils/mockConfig.ts index 113dc73156..a62ab0b555 100644 --- a/packages/cli/src/test-utils/mockConfig.ts +++ b/packages/cli/src/test-utils/mockConfig.ts @@ -89,6 +89,7 @@ export const createMockConfig = (overrides: Partial = {}): Config => getAccessibility: vi.fn().mockReturnValue({}), getTelemetryEnabled: vi.fn().mockReturnValue(false), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), getTelemetryOtlpEndpoint: vi.fn().mockReturnValue(''), getTelemetryOtlpProtocol: vi.fn().mockReturnValue('grpc'), getTelemetryTarget: vi.fn().mockReturnValue(''), diff --git a/packages/core/src/agents/agent-tool.ts b/packages/core/src/agents/agent-tool.ts index d24636915c..899266f77f 100644 --- a/packages/core/src/agents/agent-tool.ts +++ b/packages/core/src/agents/agent-tool.ts @@ -194,6 +194,7 @@ class DelegateInvocation extends BaseToolInvocation< { operation: GeminiCliOperation.AgentCall, logPrompts: this.context.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.context.config.getTelemetryTracesEnabled(), sessionId: this.context.config.getSessionId(), attributes: { [GEN_AI_AGENT_NAME]: this.definition.name, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index a23e9bc0b6..781e057d14 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -205,6 +205,7 @@ export interface PlanSettings { export interface TelemetrySettings { enabled?: boolean; + traces?: boolean; target?: TelemetryTarget; otlpEndpoint?: string; otlpProtocol?: 'grpc' | 'http'; @@ -1061,6 +1062,7 @@ export class Config implements McpContext, AgentLoopContext { this.accessibility = params.accessibility ?? {}; this.telemetrySettings = { enabled: params.telemetry?.enabled ?? false, + traces: params.telemetry?.traces ?? false, target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET, otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT, otlpProtocol: params.telemetry?.otlpProtocol, @@ -2732,6 +2734,10 @@ export class Config implements McpContext, AgentLoopContext { return this.telemetrySettings.enabled ?? false; } + getTelemetryTracesEnabled(): boolean { + return this.telemetrySettings.traces ?? false; + } + getTelemetryLogPromptsEnabled(): boolean { return this.telemetrySettings.logPrompts ?? true; } diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index d4a3f40aad..4beb14ea06 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -153,6 +153,7 @@ describe('GeminiChat', () => { promptId: 'test-session-id', getSessionId: () => 'test-session-id', getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getContentGeneratorConfig: vi.fn().mockImplementation(() => ({ diff --git a/packages/core/src/core/geminiChat_network_retry.test.ts b/packages/core/src/core/geminiChat_network_retry.test.ts index 83d5848e75..7d9bf67848 100644 --- a/packages/core/src/core/geminiChat_network_retry.test.ts +++ b/packages/core/src/core/geminiChat_network_retry.test.ts @@ -96,6 +96,7 @@ describe('GeminiChat Network Retries', () => { promptId: 'test-session-id', getSessionId: () => 'test-session-id', getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getContentGeneratorConfig: vi.fn().mockReturnValue({ diff --git a/packages/core/src/core/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator.test.ts index 7f3b1a9f33..2a2580cb84 100644 --- a/packages/core/src/core/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator.test.ts @@ -73,6 +73,7 @@ describe('LoggingContentGenerator', () => { authType: 'API_KEY', }), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), refreshUserQuotaIfStale: vi.fn().mockResolvedValue(undefined), getSessionId: vi.fn().mockReturnValue('test-session-id'), } as unknown as Config; diff --git a/packages/core/src/core/loggingContentGenerator.ts b/packages/core/src/core/loggingContentGenerator.ts index 1c8579df9a..d27b8a8f32 100644 --- a/packages/core/src/core/loggingContentGenerator.ts +++ b/packages/core/src/core/loggingContentGenerator.ts @@ -361,6 +361,7 @@ export class LoggingContentGenerator implements ContentGenerator { { operation: GeminiCliOperation.LLMCall, logPrompts: this.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.config.getTelemetryTracesEnabled(), sessionId: this.config.getSessionId(), attributes: { [GEN_AI_REQUEST_MODEL]: req.model, @@ -452,6 +453,7 @@ export class LoggingContentGenerator implements ContentGenerator { { operation: GeminiCliOperation.LLMCall, logPrompts: this.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.config.getTelemetryTracesEnabled(), sessionId: this.config.getSessionId(), attributes: { [GEN_AI_REQUEST_MODEL]: req.model, @@ -607,6 +609,7 @@ export class LoggingContentGenerator implements ContentGenerator { { operation: GeminiCliOperation.LLMCall, logPrompts: this.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.config.getTelemetryTracesEnabled(), sessionId: this.config.getSessionId(), attributes: { [GEN_AI_REQUEST_MODEL]: req.model, diff --git a/packages/core/src/scheduler/policy.test.ts b/packages/core/src/scheduler/policy.test.ts index 053e3b3833..34491f788c 100644 --- a/packages/core/src/scheduler/policy.test.ts +++ b/packages/core/src/scheduler/policy.test.ts @@ -905,6 +905,7 @@ describe('Plan Mode Denial Consistency', () => { getEnableHooks: vi.fn().mockReturnValue(false), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.PLAN), // Key: Plan Mode getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), setApprovalMode: vi.fn(), getSessionId: vi.fn().mockReturnValue('test-session-id'), getUsageStatisticsEnabled: vi.fn().mockReturnValue(false), diff --git a/packages/core/src/scheduler/scheduler.test.ts b/packages/core/src/scheduler/scheduler.test.ts index aaa5d48f5d..b7b6bbf96a 100644 --- a/packages/core/src/scheduler/scheduler.test.ts +++ b/packages/core/src/scheduler/scheduler.test.ts @@ -178,6 +178,7 @@ describe('Scheduler (Orchestrator)', () => { setApprovalMode: vi.fn(), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), getSessionId: vi.fn().mockReturnValue('test-session-id'), } as unknown as Mocked; @@ -1517,6 +1518,7 @@ describe('Scheduler MCP Progress', () => { setApprovalMode: vi.fn(), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), getSessionId: vi.fn().mockReturnValue('test-session-id'), } as unknown as Mocked; diff --git a/packages/core/src/scheduler/scheduler.ts b/packages/core/src/scheduler/scheduler.ts index fef22968e1..709bdc2bf5 100644 --- a/packages/core/src/scheduler/scheduler.ts +++ b/packages/core/src/scheduler/scheduler.ts @@ -196,6 +196,7 @@ export class Scheduler { { operation: GeminiCliOperation.ScheduleToolCalls, logPrompts: this.context.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.context.config.getTelemetryTracesEnabled(), sessionId: this.context.config.getSessionId(), }, async ({ metadata: spanMetadata }) => { diff --git a/packages/core/src/scheduler/scheduler_hooks.test.ts b/packages/core/src/scheduler/scheduler_hooks.test.ts index 3134ccd701..e3dc824d8b 100644 --- a/packages/core/src/scheduler/scheduler_hooks.test.ts +++ b/packages/core/src/scheduler/scheduler_hooks.test.ts @@ -71,6 +71,7 @@ function createMockConfig(overrides: Partial = {}): Config { getEnableHooks: () => true, getExperiments: () => {}, getTelemetryLogPromptsEnabled: () => false, + getTelemetryTracesEnabled: () => false, getPolicyEngine: () => ({ check: async () => ({ decision: 'allow' }), diff --git a/packages/core/src/scheduler/scheduler_parallel.test.ts b/packages/core/src/scheduler/scheduler_parallel.test.ts index 9229a94550..1f1f5efafd 100644 --- a/packages/core/src/scheduler/scheduler_parallel.test.ts +++ b/packages/core/src/scheduler/scheduler_parallel.test.ts @@ -218,6 +218,7 @@ describe('Scheduler Parallel Execution', () => { setApprovalMode: vi.fn(), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), getSessionId: vi.fn().mockReturnValue('test-session-id'), } as unknown as Mocked; diff --git a/packages/core/src/scheduler/tool-executor.ts b/packages/core/src/scheduler/tool-executor.ts index 3910aaee47..3d9ad1e063 100644 --- a/packages/core/src/scheduler/tool-executor.ts +++ b/packages/core/src/scheduler/tool-executor.ts @@ -84,6 +84,7 @@ export class ToolExecutor { { operation: GeminiCliOperation.ToolCall, logPrompts: this.config.getTelemetryLogPromptsEnabled(), + tracesEnabled: this.config.getTelemetryTracesEnabled(), sessionId: this.config.getSessionId(), attributes: { [GEN_AI_TOOL_NAME]: toolName, diff --git a/packages/core/src/telemetry/config.ts b/packages/core/src/telemetry/config.ts index bd7cbdf09c..9fd4bacfc3 100644 --- a/packages/core/src/telemetry/config.ts +++ b/packages/core/src/telemetry/config.ts @@ -60,6 +60,10 @@ export async function resolveTelemetrySettings(options: { parseBooleanEnvFlag(env['GEMINI_TELEMETRY_ENABLED']) ?? settings.enabled; + const traces = + parseBooleanEnvFlag(env['GEMINI_TELEMETRY_TRACES_ENABLED']) ?? + settings.traces; + const rawTarget = argv.telemetryTarget ?? env['GEMINI_TELEMETRY_TARGET'] ?? @@ -110,6 +114,7 @@ export async function resolveTelemetrySettings(options: { return { enabled, + traces, target, otlpEndpoint, otlpProtocol, diff --git a/packages/core/src/telemetry/conseca-logger.test.ts b/packages/core/src/telemetry/conseca-logger.test.ts index 0eac29276f..0df06f6d80 100644 --- a/packages/core/src/telemetry/conseca-logger.test.ts +++ b/packages/core/src/telemetry/conseca-logger.test.ts @@ -37,6 +37,7 @@ describe('conseca-logger', () => { getTelemetryEnabled: vi.fn().mockReturnValue(true), getSessionId: vi.fn().mockReturnValue('test-session-id'), getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true), + getTelemetryTracesEnabled: vi.fn().mockReturnValue(false), isInteractive: vi.fn().mockReturnValue(true), getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }), getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }), diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index b21fc606e2..f999d72962 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -216,6 +216,7 @@ describe('loggers', () => { getTelemetryEnabled: () => true, getUsageStatisticsEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getFileFilteringRespectGitIgnore: () => true, getFileFilteringAllowBuildArtifacts: () => false, getDebugMode: () => true, @@ -313,6 +314,7 @@ describe('loggers', () => { getSessionId: () => 'test-session-id', getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getUsageStatisticsEnabled: () => true, isInteractive: () => false, getExperiments: () => undefined, @@ -352,6 +354,7 @@ describe('loggers', () => { getSessionId: () => 'test-session-id', getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => false, + getTelemetryTracesEnabled: () => false, getTargetDir: () => 'target-dir', getUsageStatisticsEnabled: () => true, isInteractive: () => false, @@ -392,6 +395,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => true, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -493,10 +497,10 @@ describe('loggers', () => { 'gen_ai.output.messages': '[{"finish_reason":"stop","role":"system","parts":[{"type":"text","content":"candidate 1"}]}]', 'gen_ai.response.finish_reasons': ['stop'], + 'gen_ai.operation.name': 'generate_content', 'gen_ai.response.model': 'test-model', 'gen_ai.usage.input_tokens': 17, 'gen_ai.usage.output_tokens': 50, - 'gen_ai.operation.name': 'generate_content', 'gen_ai.output.type': 'text', 'gen_ai.request.choice.count': 1, 'gen_ai.request.seed': 678, @@ -564,6 +568,57 @@ describe('loggers', () => { }); }); + it('should not log input and output messages when traces are disabled', () => { + const mockConfigNoTraces = { + getSessionId: () => 'test-session-id', + getTargetDir: () => 'target-dir', + getUsageStatisticsEnabled: () => true, + getTelemetryEnabled: () => true, + getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, // Disabled + isInteractive: () => false, + getExperiments: () => undefined, + getExperimentsAsync: async () => undefined, + getContentGeneratorConfig: () => undefined, + } as unknown as Config; + + const event = new ApiResponseEvent( + 'test-model', + 100, + { prompt_id: 'prompt-id-1', contents: [] }, + { candidates: [] }, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'test-response', + ); + + logApiResponse(mockConfigNoTraces, event); + + expect(mockLogger.emit).toHaveBeenCalledWith( + expect.objectContaining({ + body: 'GenAI operation details from test-model. Status: 200. Duration: 100ms.', + attributes: expect.objectContaining({ + 'event.name': 'gen_ai.client.inference.operation.details', + 'gen_ai.operation.name': 'generate_content', + }), + }), + ); + + const emitCalls = mockLogger.emit.mock.calls; + const detailsCall = emitCalls.find( + (call) => + call[0].attributes && + call[0].attributes['event.name'] === + 'gen_ai.client.inference.operation.details', + ); + expect( + detailsCall![0].attributes['gen_ai.input.messages'], + ).toBeUndefined(); + expect( + detailsCall![0].attributes['gen_ai.output.messages'], + ).toBeUndefined(); + }); + it('should log an API response with a role', () => { const event = new ApiResponseEvent( 'test-model', @@ -596,6 +651,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => true, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -674,8 +730,6 @@ describe('loggers', () => { 'gen_ai.request.temperature': 1, 'gen_ai.request.top_p': 2, 'gen_ai.request.top_k': 3, - 'gen_ai.input.messages': - '[{"role":"user","parts":[{"type":"text","content":"Hello"}]}]', 'gen_ai.operation.name': 'generate_content', 'gen_ai.output.type': 'text', 'gen_ai.request.choice.count': 1, @@ -683,6 +737,8 @@ describe('loggers', () => { 'gen_ai.request.frequency_penalty': 10, 'gen_ai.request.presence_penalty': 6, 'gen_ai.request.max_tokens': 8000, + 'gen_ai.input.messages': + '[{"role":"user","parts":[{"type":"text","content":"Hello"}]}]', 'server.address': 'foo.com', 'server.port': 8080, 'gen_ai.request.stop_sequences': ['stop', 'please stop'], @@ -724,6 +780,52 @@ describe('loggers', () => { }); }); + it('should not log input messages when traces are disabled', () => { + const mockConfigNoTraces = { + getSessionId: () => 'test-session-id', + getTargetDir: () => 'target-dir', + getUsageStatisticsEnabled: () => true, + getTelemetryEnabled: () => true, + getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, // Disabled + isInteractive: () => false, + getExperiments: () => undefined, + getExperimentsAsync: async () => undefined, + getContentGeneratorConfig: () => undefined, + } as unknown as Config; + + const event = new ApiErrorEvent( + 'test-model', + 'error', + 100, + { prompt_id: 'prompt-id-1', contents: [] }, + AuthType.LOGIN_WITH_GOOGLE, + 'ApiError', + 500, + ); + + logApiError(mockConfigNoTraces, event); + + expect(mockLogger.emit).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'event.name': 'gen_ai.client.inference.operation.details', + }), + }), + ); + + const emitCalls = mockLogger.emit.mock.calls; + const detailsCall = emitCalls.find( + (call) => + call[0].attributes && + call[0].attributes['event.name'] === + 'gen_ai.client.inference.operation.details', + ); + expect( + detailsCall![0].attributes['gen_ai.input.messages'], + ).toBeUndefined(); + }); + it('should log an API error with a role', () => { const event = new ApiErrorEvent( 'test-model', @@ -756,6 +858,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -833,7 +936,8 @@ describe('loggers', () => { getTargetDir: () => 'target-dir', getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, - getTelemetryLogPromptsEnabled: () => true, // Enabled + getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => true, // Enabled isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -922,7 +1026,8 @@ describe('loggers', () => { getTargetDir: () => 'target-dir', getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, - getTelemetryLogPromptsEnabled: () => false, // Disabled + getTelemetryLogPromptsEnabled: () => false, + getTelemetryTracesEnabled: () => false, // Disabled isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -978,6 +1083,7 @@ describe('loggers', () => { getSessionId: () => 'test-session-id', getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -1140,6 +1246,7 @@ describe('loggers', () => { getCoreTools: () => ['ls', 'read-file'], getApprovalMode: () => 'default', getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, getFileFilteringRespectGitIgnore: () => true, getFileFilteringAllowBuildArtifacts: () => false, getDebugMode: () => true, @@ -1170,6 +1277,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -1829,6 +1937,7 @@ describe('loggers', () => { getUsageStatisticsEnabled: () => true, getTelemetryEnabled: () => true, getTelemetryLogPromptsEnabled: () => true, + getTelemetryTracesEnabled: () => false, isInteractive: () => false, getExperiments: () => undefined, getExperimentsAsync: async () => undefined, @@ -2423,6 +2532,7 @@ describe('loggers', () => { getExperiments: () => undefined, getExperimentsAsync: async () => undefined, getTelemetryLogPromptsEnabled: () => false, + getTelemetryTracesEnabled: () => false, getContentGeneratorConfig: () => undefined, } as unknown as Config; diff --git a/packages/core/src/telemetry/trace.test.ts b/packages/core/src/telemetry/trace.test.ts index 87a1419080..25812cc9e3 100644 --- a/packages/core/src/telemetry/trace.test.ts +++ b/packages/core/src/telemetry/trace.test.ts @@ -115,7 +115,11 @@ describe('runInDevTraceSpan', () => { const fn = vi.fn(async () => 'result'); const result = await runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, fn, ); @@ -123,14 +127,22 @@ describe('runInDevTraceSpan', () => { expect(trace.getTracer).toHaveBeenCalled(); expect(mockTracer.startActiveSpan).toHaveBeenCalledWith( GeminiCliOperation.LLMCall, - {}, + { + attributes: { + [GEN_AI_CONVERSATION_ID]: 'test-session-id', + }, + }, expect.any(Function), ); }); it('should set default attributes on the span metadata', async () => { await runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, async ({ metadata }) => { expect(metadata.attributes[GEN_AI_OPERATION_NAME]).toBe( GeminiCliOperation.LLMCall, @@ -148,7 +160,11 @@ describe('runInDevTraceSpan', () => { it('should set span attributes from metadata on completion', async () => { await runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, async ({ metadata }) => { metadata.input = { query: 'hello' }; metadata.output = { response: 'world' }; @@ -175,7 +191,11 @@ describe('runInDevTraceSpan', () => { const error = new Error('test error'); await expect( runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, async () => { throw error; }, @@ -197,7 +217,11 @@ describe('runInDevTraceSpan', () => { } const resultStream = await runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, async () => testStream(), ); @@ -219,7 +243,11 @@ describe('runInDevTraceSpan', () => { } const resultStream = await runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, async () => testStream(), ); @@ -233,7 +261,11 @@ describe('runInDevTraceSpan', () => { } const resultStream = await runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, async () => testStream(), ); @@ -259,7 +291,11 @@ describe('runInDevTraceSpan', () => { } const resultStream = await runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, async () => errorStream(), ); @@ -278,7 +314,11 @@ describe('runInDevTraceSpan', () => { }); await runInDevTraceSpan( - { operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' }, + { + operation: GeminiCliOperation.LLMCall, + sessionId: 'test-session-id', + tracesEnabled: true, + }, async ({ metadata }) => { metadata.input = 'trigger error'; }, diff --git a/packages/core/src/telemetry/trace.ts b/packages/core/src/telemetry/trace.ts index fd3082c3cd..768dd26060 100644 --- a/packages/core/src/telemetry/trace.ts +++ b/packages/core/src/telemetry/trace.ts @@ -125,10 +125,17 @@ export async function runInDevTraceSpan( operation: GeminiCliOperation; logPrompts?: boolean; sessionId: string; + tracesEnabled?: boolean; }, fn: ({ metadata }: { metadata: SpanMetadata }) => Promise, ): Promise { - const { operation, logPrompts, sessionId, ...restOfSpanOpts } = opts; + const { operation, logPrompts, sessionId, tracesEnabled, ...restOfSpanOpts } = + opts; + + restOfSpanOpts.attributes = { + ...restOfSpanOpts.attributes, + [GEN_AI_CONVERSATION_ID]: sessionId, + }; const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION); return tracer.startActiveSpan(operation, restOfSpanOpts, async (span) => { @@ -148,24 +155,41 @@ export async function runInDevTraceSpan( } spanEnded = true; try { - if (logPrompts !== false) { - if (meta.input !== undefined) { - const truncated = truncateForTelemetry(meta.input); - if (truncated !== undefined) { - span.setAttribute(GEN_AI_INPUT_MESSAGES, truncated); + if (tracesEnabled) { + if (logPrompts !== false) { + if (meta.input !== undefined) { + const truncated = truncateForTelemetry(meta.input); + if (truncated !== undefined) { + span.setAttribute(GEN_AI_INPUT_MESSAGES, truncated); + } + } + if (meta.output !== undefined) { + const truncated = truncateForTelemetry(meta.output); + if (truncated !== undefined) { + span.setAttribute(GEN_AI_OUTPUT_MESSAGES, truncated); + } } } - if (meta.output !== undefined) { - const truncated = truncateForTelemetry(meta.output); + for (const [key, value] of Object.entries(meta.attributes)) { + const truncated = truncateForTelemetry(value); if (truncated !== undefined) { - span.setAttribute(GEN_AI_OUTPUT_MESSAGES, truncated); + span.setAttribute(key, truncated); } } - } - for (const [key, value] of Object.entries(meta.attributes)) { - const truncated = truncateForTelemetry(value); - if (truncated !== undefined) { - span.setAttribute(key, truncated); + } else { + // Add basic attributes even when traces are disabled + for (const [key, value] of Object.entries(meta.attributes)) { + if ( + key === GEN_AI_OPERATION_NAME || + key === GEN_AI_AGENT_NAME || + key === GEN_AI_AGENT_DESCRIPTION || + key === GEN_AI_CONVERSATION_ID + ) { + const truncated = truncateForTelemetry(value); + if (truncated !== undefined) { + span.setAttribute(key, truncated); + } + } } } if (meta.error) { diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 9d6cd08c72..3e91b587a4 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -387,6 +387,13 @@ export class ToolCallEvent implements BaseTelemetryEvent { } export const EVENT_API_REQUEST = 'gemini_cli.api_request'; + +function shouldIncludePayloads(config: Config): boolean { + return ( + config.getTelemetryTracesEnabled() && config.getTelemetryLogPromptsEnabled() + ); +} + export class ApiRequestEvent implements BaseTelemetryEvent { 'event.name': 'api_request'; 'event.timestamp': string; @@ -443,7 +450,7 @@ export class ApiRequestEvent implements BaseTelemetryEvent { attributes['server.port'] = this.prompt.server.port; } - if (config.getTelemetryLogPromptsEnabled() && this.prompt.contents) { + if (shouldIncludePayloads(config) && this.prompt.contents) { attributes['gen_ai.input.messages'] = JSON.stringify( toInputMessages(this.prompt.contents), ); @@ -540,7 +547,7 @@ export class ApiErrorEvent implements BaseTelemetryEvent { attributes['server.port'] = this.prompt.server.port; } - if (config.getTelemetryLogPromptsEnabled() && this.prompt.contents) { + if (shouldIncludePayloads(config) && this.prompt.contents) { attributes['gen_ai.input.messages'] = JSON.stringify( toInputMessages(this.prompt.contents), ); @@ -707,9 +714,13 @@ export class ApiResponseEvent implements BaseTelemetryEvent { 'event.timestamp': this['event.timestamp'], 'gen_ai.response.id': this.response.response_id, 'gen_ai.response.finish_reasons': this.finish_reasons, - 'gen_ai.output.messages': JSON.stringify( - toOutputMessages(this.response.candidates), - ), + ...(shouldIncludePayloads(config) + ? { + 'gen_ai.output.messages': JSON.stringify( + toOutputMessages(this.response.candidates), + ), + } + : {}), ...toGenerateContentConfigAttributes(this.prompt.generate_content_config), ...getConventionAttributes(this), }; @@ -719,7 +730,7 @@ export class ApiResponseEvent implements BaseTelemetryEvent { attributes['server.port'] = this.prompt.server.port; } - if (config.getTelemetryLogPromptsEnabled() && this.prompt.contents) { + if (shouldIncludePayloads(config) && this.prompt.contents) { attributes['gen_ai.input.messages'] = JSON.stringify( toInputMessages(this.prompt.contents), ); diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index e24a7383d8..3efad9a370 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -3620,6 +3620,10 @@ "description": "Protocol for OTLP exporters.", "enum": ["grpc", "http"] }, + "traces": { + "type": "boolean", + "description": "Whether detailed traces with large attributes are captured." + }, "logPrompts": { "type": "boolean", "description": "Whether prompts are logged in telemetry payloads."