From 83075b280054457f8f36b8224f86a88a906742ac Mon Sep 17 00:00:00 2001 From: "Christie Warwick (Wilson)" Date: Thu, 9 Oct 2025 16:02:58 -0700 Subject: [PATCH] refactor: make log/event structure clear (#10467) --- packages/cli/src/gemini.tsx | 18 +- .../cli/src/zed-integration/zedIntegration.ts | 45 +- packages/core/src/telemetry/constants.ts | 42 - packages/core/src/telemetry/loggers.test.ts | 38 +- packages/core/src/telemetry/loggers.ts | 458 ++--------- packages/core/src/telemetry/metrics.ts | 3 +- .../core/src/telemetry/telemetryAttributes.ts | 18 + packages/core/src/telemetry/types.ts | 722 +++++++++++++++++- .../core/src/telemetry/uiTelemetry.test.ts | 2 +- packages/core/src/telemetry/uiTelemetry.ts | 2 +- 10 files changed, 831 insertions(+), 517 deletions(-) create mode 100644 packages/core/src/telemetry/telemetryAttributes.ts diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index f18ed15c96..1182d2de6a 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -39,6 +39,7 @@ import { logUserPrompt, AuthType, getOauthClient, + UserPromptEvent, } from '@google/gemini-cli-core'; import { initializeApp, @@ -436,14 +437,15 @@ export async function main() { } const prompt_id = Math.random().toString(16).slice(2); - logUserPrompt(config, { - 'event.name': 'user_prompt', - 'event.timestamp': new Date().toISOString(), - prompt: input, - prompt_id, - auth_type: config.getContentGeneratorConfig()?.authType, - prompt_length: input.length, - }); + logUserPrompt( + config, + new UserPromptEvent( + input.length, + prompt_id, + config.getContentGeneratorConfig()?.authType, + input, + ), + ); const nonInteractiveConfig = await validateNonInteractiveAuth( settings.merged.security?.auth?.selectedType, diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index 34acec161c..2235f9dc66 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -26,6 +26,7 @@ import { MCPServerConfig, DiscoveredMCPTool, StreamEventType, + ToolCallEvent, DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_MODEL_AUTO, DEFAULT_GEMINI_FLASH_MODEL, @@ -369,20 +370,21 @@ class Session { const errorResponse = (error: Error) => { const durationMs = Date.now() - startTime; - logToolCall(this.config, { - 'event.name': 'tool_call', - 'event.timestamp': new Date().toISOString(), - prompt_id: promptId, - function_name: fc.name ?? '', - function_args: args, - duration_ms: durationMs, - success: false, - error: error.message, - tool_type: + logToolCall( + this.config, + new ToolCallEvent( + undefined, + fc.name ?? '', + args, + durationMs, + false, + promptId, typeof tool !== 'undefined' && tool instanceof DiscoveredMCPTool ? 'mcp' : 'native', - }); + error.message, + ), + ); return [ { @@ -488,19 +490,20 @@ class Session { }); const durationMs = Date.now() - startTime; - logToolCall(this.config, { - 'event.name': 'tool_call', - 'event.timestamp': new Date().toISOString(), - function_name: fc.name, - function_args: args, - duration_ms: durationMs, - success: true, - prompt_id: promptId, - tool_type: + logToolCall( + this.config, + new ToolCallEvent( + undefined, + fc.name ?? '', + args, + durationMs, + true, + promptId, typeof tool !== 'undefined' && tool instanceof DiscoveredMCPTool ? 'mcp' : 'native', - }); + ), + ); return convertToFunctionResponse(fc.name, callId, toolResult.llmContent); } catch (e) { diff --git a/packages/core/src/telemetry/constants.ts b/packages/core/src/telemetry/constants.ts index f93871e5d7..1d2b7751a1 100644 --- a/packages/core/src/telemetry/constants.ts +++ b/packages/core/src/telemetry/constants.ts @@ -5,45 +5,3 @@ */ export const SERVICE_NAME = 'gemini-cli'; - -export const EVENT_USER_PROMPT = 'gemini_cli.user_prompt'; -export const EVENT_TOOL_CALL = 'gemini_cli.tool_call'; -export const EVENT_API_REQUEST = 'gemini_cli.api_request'; -export const EVENT_API_ERROR = 'gemini_cli.api_error'; -export const EVENT_API_RESPONSE = 'gemini_cli.api_response'; -export const EVENT_CLI_CONFIG = 'gemini_cli.config'; -export const EVENT_EXTENSION_DISABLE = 'gemini_cli.extension_disable'; -export const EVENT_EXTENSION_ENABLE = 'gemini_cli.extension_enable'; -export const EVENT_EXTENSION_INSTALL = 'gemini_cli.extension_install'; -export const EVENT_EXTENSION_UNINSTALL = 'gemini_cli.extension_uninstall'; -export const EVENT_FLASH_FALLBACK = 'gemini_cli.flash_fallback'; -export const EVENT_RIPGREP_FALLBACK = 'gemini_cli.ripgrep_fallback'; -export const EVENT_NEXT_SPEAKER_CHECK = 'gemini_cli.next_speaker_check'; -export const EVENT_SLASH_COMMAND = 'gemini_cli.slash_command'; -export const EVENT_IDE_CONNECTION = 'gemini_cli.ide_connection'; -export const EVENT_CONVERSATION_FINISHED = 'gemini_cli.conversation_finished'; -export const EVENT_CHAT_COMPRESSION = 'gemini_cli.chat_compression'; -export const EVENT_MALFORMED_JSON_RESPONSE = - 'gemini_cli.malformed_json_response'; -export const EVENT_INVALID_CHUNK = 'gemini_cli.chat.invalid_chunk'; -export const EVENT_CONTENT_RETRY = 'gemini_cli.chat.content_retry'; -export const EVENT_CONTENT_RETRY_FAILURE = - 'gemini_cli.chat.content_retry_failure'; -export const EVENT_FILE_OPERATION = 'gemini_cli.file_operation'; -export const EVENT_TOOL_OUTPUT_TRUNCATED = 'gemini_cli.tool_output_truncated'; -export const EVENT_MODEL_SLASH_COMMAND = 'gemini_cli.slash_command.model'; -export const EVENT_SMART_EDIT_STRATEGY = 'gemini_cli.smart_edit.strategy'; -export const EVENT_MODEL_ROUTING = 'gemini_cli.model_routing'; -export const EVENT_SMART_EDIT_CORRECTION = 'gemini_cli.smart_edit.correction'; -export const EVENT_WEB_FETCH_FALLBACK_ATTEMPT = - 'gemini_cli.web_fetch_fallback_attempt'; - -// Agent Events -export const EVENT_AGENT_START = 'gemini_cli.agent.start'; -export const EVENT_AGENT_FINISH = 'gemini_cli.agent.finish'; - -// Performance Events -export const EVENT_STARTUP_PERFORMANCE = 'gemini_cli.startup.performance'; -export const EVENT_MEMORY_USAGE = 'gemini_cli.memory.usage'; -export const EVENT_PERFORMANCE_BASELINE = 'gemini_cli.performance.baseline'; -export const EVENT_PERFORMANCE_REGRESSION = 'gemini_cli.performance.regression'; diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 1413081cb4..4320f5df28 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -22,26 +22,6 @@ import { OutputFormat } from '../output/types.js'; import { logs } from '@opentelemetry/api-logs'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import type { Config } from '../config/config.js'; -import { - EVENT_API_REQUEST, - EVENT_API_RESPONSE, - EVENT_CLI_CONFIG, - EVENT_TOOL_CALL, - EVENT_USER_PROMPT, - EVENT_FLASH_FALLBACK, - EVENT_MALFORMED_JSON_RESPONSE, - EVENT_FILE_OPERATION, - EVENT_RIPGREP_FALLBACK, - EVENT_MODEL_ROUTING, - EVENT_EXTENSION_ENABLE, - EVENT_EXTENSION_DISABLE, - EVENT_EXTENSION_INSTALL, - EVENT_EXTENSION_UNINSTALL, - EVENT_TOOL_OUTPUT_TRUNCATED, - EVENT_AGENT_START, - EVENT_AGENT_FINISH, - EVENT_WEB_FETCH_FALLBACK_ATTEMPT, -} from './constants.js'; import { logApiRequest, logApiResponse, @@ -65,6 +45,24 @@ import { } from './loggers.js'; import { ToolCallDecision } from './tool-call-decision.js'; import { + EVENT_API_REQUEST, + EVENT_API_RESPONSE, + EVENT_CLI_CONFIG, + EVENT_TOOL_CALL, + EVENT_USER_PROMPT, + EVENT_FLASH_FALLBACK, + EVENT_MALFORMED_JSON_RESPONSE, + EVENT_FILE_OPERATION, + EVENT_RIPGREP_FALLBACK, + EVENT_MODEL_ROUTING, + EVENT_EXTENSION_ENABLE, + EVENT_EXTENSION_DISABLE, + EVENT_EXTENSION_INSTALL, + EVENT_EXTENSION_UNINSTALL, + EVENT_TOOL_OUTPUT_TRUNCATED, + EVENT_AGENT_START, + EVENT_AGENT_FINISH, + EVENT_WEB_FETCH_FALLBACK_ATTEMPT, ApiRequestEvent, ApiResponseEvent, StartSessionEvent, diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 0d0b270bbe..93b9e2446d 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -4,43 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { LogRecord, LogAttributes } from '@opentelemetry/api-logs'; +import type { LogRecord } from '@opentelemetry/api-logs'; import { logs } from '@opentelemetry/api-logs'; -import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import type { Config } from '../config/config.js'; +import { SERVICE_NAME } from './constants.js'; import { EVENT_API_ERROR, - EVENT_API_REQUEST, EVENT_API_RESPONSE, - EVENT_CLI_CONFIG, - EVENT_EXTENSION_UNINSTALL, - EVENT_EXTENSION_ENABLE, - EVENT_IDE_CONNECTION, EVENT_TOOL_CALL, - EVENT_USER_PROMPT, - EVENT_FLASH_FALLBACK, - EVENT_NEXT_SPEAKER_CHECK, - SERVICE_NAME, - EVENT_SLASH_COMMAND, - EVENT_CONVERSATION_FINISHED, - EVENT_CHAT_COMPRESSION, - EVENT_MALFORMED_JSON_RESPONSE, - EVENT_INVALID_CHUNK, - EVENT_CONTENT_RETRY, - EVENT_CONTENT_RETRY_FAILURE, - EVENT_FILE_OPERATION, - EVENT_TOOL_OUTPUT_TRUNCATED, - EVENT_RIPGREP_FALLBACK, - EVENT_MODEL_ROUTING, - EVENT_EXTENSION_INSTALL, - EVENT_MODEL_SLASH_COMMAND, - EVENT_EXTENSION_DISABLE, - EVENT_SMART_EDIT_STRATEGY, - EVENT_SMART_EDIT_CORRECTION, - EVENT_AGENT_START, - EVENT_AGENT_FINISH, - EVENT_WEB_FETCH_FALLBACK_ATTEMPT, -} from './constants.js'; +} from './types.js'; import type { ApiErrorEvent, ApiRequestEvent, @@ -95,20 +67,6 @@ import { isTelemetrySdkInitialized } from './sdk.js'; import type { UiEvent } from './uiTelemetry.js'; import { uiTelemetryService } from './uiTelemetry.js'; import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js'; -import { safeJsonStringify } from '../utils/safeJsonStringify.js'; -import { UserAccountManager } from '../utils/userAccountManager.js'; - -const shouldLogUserPrompts = (config: Config): boolean => - config.getTelemetryLogPromptsEnabled(); - -function getCommonAttributes(config: Config): LogAttributes { - const userAccountManager = new UserAccountManager(); - const email = userAccountManager.getCachedGoogleAccount(); - return { - 'session.id': config.getSessionId(), - ...(email && { 'user.email': email }), - }; -} export function logCliConfiguration( config: Config, @@ -117,31 +75,10 @@ export function logCliConfiguration( ClearcutLogger.getInstance(config)?.logStartSessionEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - 'event.name': EVENT_CLI_CONFIG, - 'event.timestamp': new Date().toISOString(), - model: event.model, - embedding_model: event.embedding_model, - sandbox_enabled: event.sandbox_enabled, - core_tools_enabled: event.core_tools_enabled, - approval_mode: event.approval_mode, - api_key_enabled: event.api_key_enabled, - vertex_ai_enabled: event.vertex_ai_enabled, - log_user_prompts_enabled: event.telemetry_log_user_prompts_enabled, - file_filtering_respect_git_ignore: event.file_filtering_respect_git_ignore, - debug_mode: event.debug_enabled, - mcp_servers: event.mcp_servers, - mcp_servers_count: event.mcp_servers_count, - mcp_tools: event.mcp_tools, - mcp_tools_count: event.mcp_tools_count, - output_format: event.output_format, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: 'CLI configuration loaded.', - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -150,26 +87,10 @@ export function logUserPrompt(config: Config, event: UserPromptEvent): void { ClearcutLogger.getInstance(config)?.logNewPromptEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - 'event.name': EVENT_USER_PROMPT, - 'event.timestamp': new Date().toISOString(), - prompt_length: event.prompt_length, - prompt_id: event.prompt_id, - }; - - if (event.auth_type) { - attributes['auth_type'] = event.auth_type; - } - - if (shouldLogUserPrompts(config)) { - attributes['prompt'] = event.prompt; - } - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `User prompt. Length: ${event.prompt_length}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -184,24 +105,10 @@ export function logToolCall(config: Config, event: ToolCallEvent): void { ClearcutLogger.getInstance(config)?.logToolCallEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_TOOL_CALL, - 'event.timestamp': new Date().toISOString(), - function_args: safeJsonStringify(event.function_args, 2), - }; - if (event.error) { - attributes['error.message'] = event.error; - if (event.error_type) { - attributes['error.type'] = event.error_type; - } - } - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Tool call: ${event.function_name}${event.decision ? `. Decision: ${event.decision}` : ''}. Success: ${event.success}. Duration: ${event.duration_ms}ms.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); recordToolCallMetrics(config, event.duration_ms, { @@ -227,17 +134,10 @@ export function logToolOutputTruncated( ClearcutLogger.getInstance(config)?.logToolOutputTruncatedEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_TOOL_OUTPUT_TRUNCATED, - 'event.timestamp': new Date().toISOString(), - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Tool output truncated for ${event.tool_name}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -249,31 +149,10 @@ export function logFileOperation( ClearcutLogger.getInstance(config)?.logFileOperationEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - 'event.name': EVENT_FILE_OPERATION, - 'event.timestamp': new Date().toISOString(), - tool_name: event.tool_name, - operation: event.operation, - }; - - if (event.lines) { - attributes['lines'] = event.lines; - } - if (event.mimetype) { - attributes['mimetype'] = event.mimetype; - } - if (event.extension) { - attributes['extension'] = event.extension; - } - if (event.programming_language) { - attributes['programming_language'] = event.programming_language; - } - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `File operation: ${event.operation}. Lines: ${event.lines}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); @@ -290,17 +169,10 @@ export function logApiRequest(config: Config, event: ApiRequestEvent): void { ClearcutLogger.getInstance(config)?.logApiRequestEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_API_REQUEST, - 'event.timestamp': new Date().toISOString(), - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `API request to ${event.model}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -312,17 +184,10 @@ export function logFlashFallback( ClearcutLogger.getInstance(config)?.logFlashFallbackEvent(); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_FLASH_FALLBACK, - 'event.timestamp': new Date().toISOString(), - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Switching to flash as Fallback.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -334,17 +199,10 @@ export function logRipgrepFallback( ClearcutLogger.getInstance(config)?.logRipgrepFallbackEvent(); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_RIPGREP_FALLBACK, - 'event.timestamp': new Date().toISOString(), - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Switching to grep as fallback.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -359,27 +217,10 @@ export function logApiError(config: Config, event: ApiErrorEvent): void { ClearcutLogger.getInstance(config)?.logApiErrorEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_API_ERROR, - 'event.timestamp': new Date().toISOString(), - ['error.message']: event.error, - model_name: event.model, - duration: event.duration_ms, - }; - - if (event.error_type) { - attributes['error.type'] = event.error_type; - } - if (typeof event.status_code === 'number') { - attributes[SemanticAttributes.HTTP_STATUS_CODE] = event.status_code; - } - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `API error for ${event.model}. Error: ${event.error}. Duration: ${event.duration_ms}ms.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); recordApiErrorMetrics(config, event.duration_ms, { @@ -409,25 +250,11 @@ export function logApiResponse(config: Config, event: ApiResponseEvent): void { uiTelemetryService.addEvent(uiEvent); ClearcutLogger.getInstance(config)?.logApiResponseEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_API_RESPONSE, - 'event.timestamp': new Date().toISOString(), - }; - if (event.response_text) { - attributes['response_text'] = event.response_text; - } - if (event.status_code) { - if (typeof event.status_code === 'number') { - attributes[SemanticAttributes.HTTP_STATUS_CODE] = event.status_code; - } - } const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `API response from ${event.model}. Status: ${event.status_code || 'N/A'}. Duration: ${event.duration_ms}ms.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); @@ -463,24 +290,27 @@ export function logLoopDetected( ClearcutLogger.getInstance(config)?.logLoopDetectedEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Loop detected. Type: ${event.loop_type}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } export function logLoopDetectionDisabled( config: Config, - _event: LoopDetectionDisabledEvent, + event: LoopDetectionDisabledEvent, ): void { ClearcutLogger.getInstance(config)?.logLoopDetectionDisabledEvent(); + if (!isTelemetrySdkInitialized()) return; + + const logger = logs.getLogger(SERVICE_NAME); + const logRecord: LogRecord = { + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), + }; + logger.emit(logRecord); } export function logNextSpeakerCheck( @@ -490,16 +320,10 @@ export function logNextSpeakerCheck( ClearcutLogger.getInstance(config)?.logNextSpeakerCheck(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_NEXT_SPEAKER_CHECK, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Next speaker check.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -511,16 +335,10 @@ export function logSlashCommand( ClearcutLogger.getInstance(config)?.logSlashCommandEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_SLASH_COMMAND, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Slash command: ${event.command}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -532,16 +350,10 @@ export function logIdeConnection( ClearcutLogger.getInstance(config)?.logIdeConnectionEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_IDE_CONNECTION, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Ide connection. Type: ${event.connection_type}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -553,16 +365,10 @@ export function logConversationFinishedEvent( ClearcutLogger.getInstance(config)?.logConversationFinishedEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_CONVERSATION_FINISHED, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Conversation finished.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -573,16 +379,10 @@ export function logChatCompression( ): void { ClearcutLogger.getInstance(config)?.logChatCompressionEvent(event); - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_CHAT_COMPRESSION, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Chat compression (Saved ${event.tokens_before - event.tokens_after} tokens)`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); @@ -598,14 +398,10 @@ export function logKittySequenceOverflow( ): void { ClearcutLogger.getInstance(config)?.logKittySequenceOverflowEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - }; const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Kitty sequence buffer overflow: ${event.sequence_length} bytes`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -617,16 +413,10 @@ export function logMalformedJsonResponse( ClearcutLogger.getInstance(config)?.logMalformedJsonResponseEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_MALFORMED_JSON_RESPONSE, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Malformed JSON response from ${event.model}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -638,20 +428,10 @@ export function logInvalidChunk( ClearcutLogger.getInstance(config)?.logInvalidChunkEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - 'event.name': EVENT_INVALID_CHUNK, - 'event.timestamp': event['event.timestamp'], - }; - - if (event.error_message) { - attributes['error.message'] = event.error_message; - } - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Invalid chunk received from stream.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); recordInvalidChunk(config); @@ -664,16 +444,10 @@ export function logContentRetry( ClearcutLogger.getInstance(config)?.logContentRetryEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_CONTENT_RETRY, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Content retry attempt ${event.attempt_number} due to ${event.error_type}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); recordContentRetry(config); @@ -686,16 +460,10 @@ export function logContentRetryFailure( ClearcutLogger.getInstance(config)?.logContentRetryFailureEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_CONTENT_RETRY_FAILURE, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `All content retries failed after ${event.total_attempts} attempts.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); recordContentRetryFailure(config); @@ -708,16 +476,10 @@ export function logModelRouting( ClearcutLogger.getInstance(config)?.logModelRoutingEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_MODEL_ROUTING, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Model routing decision. Model: ${event.decision_model}, Source: ${event.decision_source}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); recordModelRoutingMetrics(config, event); @@ -730,16 +492,10 @@ export function logModelSlashCommand( ClearcutLogger.getInstance(config)?.logModelSlashCommandEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_MODEL_SLASH_COMMAND, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Model slash command. Model: ${event.model_name}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); recordModelSlashCommand(config, event); @@ -752,21 +508,10 @@ export function logExtensionInstallEvent( ClearcutLogger.getInstance(config)?.logExtensionInstallEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_EXTENSION_INSTALL, - 'event.timestamp': new Date().toISOString(), - extension_name: event.extension_name, - extension_version: event.extension_version, - extension_source: event.extension_source, - status: event.status, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Installed extension ${event.extension_name}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -778,17 +523,10 @@ export function logExtensionUninstall( ClearcutLogger.getInstance(config)?.logExtensionUninstallEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_EXTENSION_UNINSTALL, - 'event.timestamp': new Date().toISOString(), - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Uninstalled extension ${event.extension_name}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -800,17 +538,10 @@ export function logExtensionEnable( ClearcutLogger.getInstance(config)?.logExtensionEnableEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_EXTENSION_ENABLE, - 'event.timestamp': new Date().toISOString(), - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Enabled extension ${event.extension_name}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -822,17 +553,10 @@ export function logExtensionDisable( ClearcutLogger.getInstance(config)?.logExtensionDisableEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_EXTENSION_DISABLE, - 'event.timestamp': new Date().toISOString(), - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Disabled extension ${event.extension_name}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -844,16 +568,10 @@ export function logSmartEditStrategy( ClearcutLogger.getInstance(config)?.logSmartEditStrategyEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_SMART_EDIT_STRATEGY, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Smart Edit Tool Strategy: ${event.strategy}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -865,16 +583,10 @@ export function logSmartEditCorrectionEvent( ClearcutLogger.getInstance(config)?.logSmartEditCorrectionEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_SMART_EDIT_CORRECTION, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Smart Edit Correction`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -883,16 +595,10 @@ export function logAgentStart(config: Config, event: AgentStartEvent): void { ClearcutLogger.getInstance(config)?.logAgentStartEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_AGENT_START, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Agent ${event.agent_name} started. ID: ${event.agent_id}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } @@ -901,16 +607,10 @@ export function logAgentFinish(config: Config, event: AgentFinishEvent): void { ClearcutLogger.getInstance(config)?.logAgentFinishEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_AGENT_FINISH, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Agent ${event.agent_name} finished. Reason: ${event.terminate_reason}. Duration: ${event.duration_ms}ms. Turns: ${event.turn_count}.`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); @@ -924,16 +624,10 @@ export function logWebFetchFallbackAttempt( ClearcutLogger.getInstance(config)?.logWebFetchFallbackAttemptEvent(event); if (!isTelemetrySdkInitialized()) return; - const attributes: LogAttributes = { - ...getCommonAttributes(config), - ...event, - 'event.name': EVENT_WEB_FETCH_FALLBACK_ATTEMPT, - }; - const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Web fetch fallback attempt. Reason: ${event.reason}`, - attributes, + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), }; logger.emit(logRecord); } diff --git a/packages/core/src/telemetry/metrics.ts b/packages/core/src/telemetry/metrics.ts index e768a89248..f6de7e47bc 100644 --- a/packages/core/src/telemetry/metrics.ts +++ b/packages/core/src/telemetry/metrics.ts @@ -6,7 +6,8 @@ import type { Attributes, Meter, Counter, Histogram } from '@opentelemetry/api'; import { diag, metrics, ValueType } from '@opentelemetry/api'; -import { SERVICE_NAME, EVENT_CHAT_COMPRESSION } from './constants.js'; +import { SERVICE_NAME } from './constants.js'; +import { EVENT_CHAT_COMPRESSION } from './types.js'; import type { Config } from '../config/config.js'; import type { ModelRoutingEvent, diff --git a/packages/core/src/telemetry/telemetryAttributes.ts b/packages/core/src/telemetry/telemetryAttributes.ts new file mode 100644 index 0000000000..fc609bdb45 --- /dev/null +++ b/packages/core/src/telemetry/telemetryAttributes.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { LogAttributes } from '@opentelemetry/api-logs'; +import type { Config } from '../config/config.js'; +import { UserAccountManager } from '../utils/userAccountManager.js'; + +export function getCommonAttributes(config: Config): LogAttributes { + const userAccountManager = new UserAccountManager(); + const email = userAccountManager.getCachedGoogleAccount(); + return { + 'session.id': config.getSessionId(), + ...(email && { 'user.email': email }), + }; +} diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index ba8a1924ab..8a9e68f67b 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -11,6 +11,7 @@ import type { CompletedToolCall } from '../core/coreToolScheduler.js'; import { DiscoveredMCPTool } from '../tools/mcp-tool.js'; import type { FileDiff } from '../tools/tools.js'; import { AuthType } from '../core/contentGenerator.js'; +import type { LogAttributes } from '@opentelemetry/api-logs'; import { getDecisionFromOutcome, ToolCallDecision, @@ -21,6 +22,10 @@ import type { ToolRegistry } from '../tools/tool-registry.js'; import type { OutputFormat } from '../output/types.js'; import type { AgentTerminateMode } from '../agents/types.js'; +import { getCommonAttributes } from './telemetryAttributes.js'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { safeJsonStringify } from '../utils/safeJsonStringify.js'; + export interface BaseTelemetryEvent { 'event.name': string; /** Current timestamp in ISO 8601 format */ @@ -29,6 +34,7 @@ export interface BaseTelemetryEvent { type CommonFields = keyof BaseTelemetryEvent; +export const EVENT_CLI_CONFIG = 'gemini_cli.config'; export class StartSessionEvent implements BaseTelemetryEvent { 'event.name': 'cli_config'; 'event.timestamp': string; @@ -61,6 +67,7 @@ export class StartSessionEvent implements BaseTelemetryEvent { } this['event.name'] = 'cli_config'; + this['event.timestamp'] = new Date().toISOString(); this.model = config.getModel(); this.embedding_model = config.getEmbeddingModel(); this.sandbox_enabled = @@ -88,6 +95,33 @@ export class StartSessionEvent implements BaseTelemetryEvent { .join(','); } } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_CLI_CONFIG, + 'event.timestamp': this['event.timestamp'], + model: this.model, + embedding_model: this.embedding_model, + sandbox_enabled: this.sandbox_enabled, + core_tools_enabled: this.core_tools_enabled, + approval_mode: this.approval_mode, + api_key_enabled: this.api_key_enabled, + vertex_ai_enabled: this.vertex_ai_enabled, + log_user_prompts_enabled: this.telemetry_log_user_prompts_enabled, + file_filtering_respect_git_ignore: this.file_filtering_respect_git_ignore, + debug_mode: this.debug_enabled, + mcp_servers: this.mcp_servers, + mcp_servers_count: this.mcp_servers_count, + mcp_tools: this.mcp_tools, + mcp_tools_count: this.mcp_tools_count, + output_format: this.output_format, + }; + } + + toLogBody(): string { + return 'CLI configuration loaded.'; + } } export class EndSessionEvent implements BaseTelemetryEvent { @@ -102,6 +136,7 @@ export class EndSessionEvent implements BaseTelemetryEvent { } } +export const EVENT_USER_PROMPT = 'gemini_cli.user_prompt'; export class UserPromptEvent implements BaseTelemetryEvent { 'event.name': 'user_prompt'; 'event.timestamp': string; @@ -123,8 +158,32 @@ export class UserPromptEvent implements BaseTelemetryEvent { this.auth_type = auth_type; this.prompt = prompt; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + const attributes: LogAttributes = { + ...getCommonAttributes(config), + 'event.name': EVENT_USER_PROMPT, + 'event.timestamp': this['event.timestamp'], + prompt_length: this.prompt_length, + prompt_id: this.prompt_id, + }; + + if (this.auth_type) { + attributes['auth_type'] = this.auth_type; + } + + if (config.getTelemetryLogPromptsEnabled()) { + attributes['prompt'] = this.prompt; + } + return attributes; + } + + toLogBody(): string { + return `User prompt. Length: ${this.prompt_length}.`; + } } +export const EVENT_TOOL_CALL = 'gemini_cli.tool_call'; export class ToolCallEvent implements BaseTelemetryEvent { 'event.name': 'tool_call'; 'event.timestamp': string; @@ -142,53 +201,117 @@ export class ToolCallEvent implements BaseTelemetryEvent { // eslint-disable-next-line @typescript-eslint/no-explicit-any metadata?: { [key: string]: any }; - constructor(call: CompletedToolCall) { + constructor(call: CompletedToolCall); + constructor( + call: undefined, + function_name: string, + function_args: Record, + duration_ms: number, + success: boolean, + prompt_id: string, + tool_type: 'native' | 'mcp', + error?: string, + ); + constructor( + call?: CompletedToolCall, + function_name?: string, + function_args?: Record, + duration_ms?: number, + success?: boolean, + prompt_id?: string, + tool_type?: 'native' | 'mcp', + error?: string, + ) { this['event.name'] = 'tool_call'; this['event.timestamp'] = new Date().toISOString(); - this.function_name = call.request.name; - this.function_args = call.request.args; - this.duration_ms = call.durationMs ?? 0; - this.success = call.status === 'success'; - this.decision = call.outcome - ? getDecisionFromOutcome(call.outcome) - : undefined; - this.error = call.response.error?.message; - this.error_type = call.response.errorType; - this.prompt_id = call.request.prompt_id; - this.content_length = call.response.contentLength; - if ( - typeof call.tool !== 'undefined' && - call.tool instanceof DiscoveredMCPTool - ) { - this.tool_type = 'mcp'; - this.mcp_server_name = call.tool.serverName; - } else { - this.tool_type = 'native'; - } - if ( - call.status === 'success' && - typeof call.response.resultDisplay === 'object' && - call.response.resultDisplay !== null && - 'diffStat' in call.response.resultDisplay - ) { - const diffStat = (call.response.resultDisplay as FileDiff).diffStat; - if (diffStat) { - this.metadata = { - model_added_lines: diffStat.model_added_lines, - model_removed_lines: diffStat.model_removed_lines, - model_added_chars: diffStat.model_added_chars, - model_removed_chars: diffStat.model_removed_chars, - user_added_lines: diffStat.user_added_lines, - user_removed_lines: diffStat.user_removed_lines, - user_added_chars: diffStat.user_added_chars, - user_removed_chars: diffStat.user_removed_chars, - }; + if (call) { + this.function_name = call.request.name; + this.function_args = call.request.args; + this.duration_ms = call.durationMs ?? 0; + this.success = call.status === 'success'; + this.decision = call.outcome + ? getDecisionFromOutcome(call.outcome) + : undefined; + this.error = call.response.error?.message; + this.error_type = call.response.errorType; + this.prompt_id = call.request.prompt_id; + this.content_length = call.response.contentLength; + if ( + typeof call.tool !== 'undefined' && + call.tool instanceof DiscoveredMCPTool + ) { + this.tool_type = 'mcp'; + this.mcp_server_name = call.tool.serverName; + } else { + this.tool_type = 'native'; + } + + if ( + call.status === 'success' && + typeof call.response.resultDisplay === 'object' && + call.response.resultDisplay !== null && + 'diffStat' in call.response.resultDisplay + ) { + const diffStat = (call.response.resultDisplay as FileDiff).diffStat; + if (diffStat) { + this.metadata = { + model_added_lines: diffStat.model_added_lines, + model_removed_lines: diffStat.model_removed_lines, + model_added_chars: diffStat.model_added_chars, + model_removed_chars: diffStat.model_removed_chars, + user_added_lines: diffStat.user_added_lines, + user_removed_lines: diffStat.user_removed_lines, + user_added_chars: diffStat.user_added_chars, + user_removed_chars: diffStat.user_removed_chars, + }; + } + } + } else { + this.function_name = function_name!; + this.function_args = function_args!; + this.duration_ms = duration_ms!; + this.success = success!; + this.prompt_id = prompt_id!; + this.tool_type = tool_type!; + this.error = error; + } + } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + const attributes: LogAttributes = { + ...getCommonAttributes(config), + 'event.name': EVENT_TOOL_CALL, + 'event.timestamp': this['event.timestamp'], + function_name: this.function_name, + function_args: safeJsonStringify(this.function_args, 2), + duration_ms: this.duration_ms, + success: this.success, + decision: this.decision, + prompt_id: this.prompt_id, + tool_type: this.tool_type, + content_length: this.content_length, + mcp_server_name: this.mcp_server_name, + metadata: this.metadata, + }; + + if (this.error) { + attributes['error'] = this.error; + attributes['error.message'] = this.error; + if (this.error_type) { + attributes['error_type'] = this.error_type; + attributes['error.type'] = this.error_type; } } + return attributes; + } + + toLogBody(): string { + return `Tool call: ${this.function_name}${this.decision ? `. Decision: ${this.decision}` : ''}. Success: ${this.success}. Duration: ${this.duration_ms}ms.`; } } +export const EVENT_API_REQUEST = 'gemini_cli.api_request'; export class ApiRequestEvent implements BaseTelemetryEvent { 'event.name': 'api_request'; 'event.timestamp': string; @@ -203,8 +326,24 @@ export class ApiRequestEvent implements BaseTelemetryEvent { this.prompt_id = prompt_id; this.request_text = request_text; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_API_REQUEST, + 'event.timestamp': this['event.timestamp'], + model: this.model, + prompt_id: this.prompt_id, + request_text: this.request_text, + }; + } + + toLogBody(): string { + return `API request to ${this.model}.`; + } } +export const EVENT_API_ERROR = 'gemini_cli.api_error'; export class ApiErrorEvent implements BaseTelemetryEvent { 'event.name': 'api_error'; 'event.timestamp': string; @@ -235,8 +374,38 @@ export class ApiErrorEvent implements BaseTelemetryEvent { this.prompt_id = prompt_id; this.auth_type = auth_type; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + const attributes: LogAttributes = { + ...getCommonAttributes(config), + 'event.name': EVENT_API_ERROR, + 'event.timestamp': this['event.timestamp'], + ['error.message']: this.error, + model_name: this.model, + duration: this.duration_ms, + model: this.model, + error: this.error, + status_code: this.status_code, + duration_ms: this.duration_ms, + prompt_id: this.prompt_id, + auth_type: this.auth_type, + }; + + if (this.error_type) { + attributes['error.type'] = this.error_type; + } + if (typeof this.status_code === 'number') { + attributes[SemanticAttributes.HTTP_STATUS_CODE] = this.status_code; + } + return attributes; + } + + toLogBody(): string { + return `API error for ${this.model}. Error: ${this.error}. Duration: ${this.duration_ms}ms.`; + } } +export const EVENT_API_RESPONSE = 'gemini_cli.api_response'; export class ApiResponseEvent implements BaseTelemetryEvent { 'event.name': 'api_response'; 'event.timestamp': string; @@ -276,8 +445,41 @@ export class ApiResponseEvent implements BaseTelemetryEvent { this.prompt_id = prompt_id; this.auth_type = auth_type; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + const attributes: LogAttributes = { + ...getCommonAttributes(config), + 'event.name': EVENT_API_RESPONSE, + 'event.timestamp': this['event.timestamp'], + model: this.model, + duration_ms: this.duration_ms, + input_token_count: this.input_token_count, + output_token_count: this.output_token_count, + cached_content_token_count: this.cached_content_token_count, + thoughts_token_count: this.thoughts_token_count, + tool_token_count: this.tool_token_count, + total_token_count: this.total_token_count, + prompt_id: this.prompt_id, + auth_type: this.auth_type, + status_code: this.status_code, + }; + if (this.response_text) { + attributes['response_text'] = this.response_text; + } + if (this.status_code) { + if (typeof this.status_code === 'number') { + attributes[SemanticAttributes.HTTP_STATUS_CODE] = this.status_code; + } + } + return attributes; + } + + toLogBody(): string { + return `API response from ${this.model}. Status: ${this.status_code || 'N/A'}. Duration: ${this.duration_ms}ms.`; + } } +export const EVENT_FLASH_FALLBACK = 'gemini_cli.flash_fallback'; export class FlashFallbackEvent implements BaseTelemetryEvent { 'event.name': 'flash_fallback'; 'event.timestamp': string; @@ -288,8 +490,22 @@ export class FlashFallbackEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.auth_type = auth_type; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_FLASH_FALLBACK, + 'event.timestamp': this['event.timestamp'], + auth_type: this.auth_type, + }; + } + + toLogBody(): string { + return `Switching to flash as Fallback.`; + } } +export const EVENT_RIPGREP_FALLBACK = 'gemini_cli.ripgrep_fallback'; export class RipgrepFallbackEvent implements BaseTelemetryEvent { 'event.name': 'ripgrep_fallback'; 'event.timestamp': string; @@ -298,6 +514,19 @@ export class RipgrepFallbackEvent implements BaseTelemetryEvent { this['event.name'] = 'ripgrep_fallback'; this['event.timestamp'] = new Date().toISOString(); } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_RIPGREP_FALLBACK, + 'event.timestamp': this['event.timestamp'], + error: this.error, + }; + } + + toLogBody(): string { + return `Switching to grep as fallback.`; + } } export enum LoopType { @@ -318,6 +547,20 @@ export class LoopDetectedEvent implements BaseTelemetryEvent { this.loop_type = loop_type; this.prompt_id = prompt_id; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': this['event.name'], + 'event.timestamp': this['event.timestamp'], + loop_type: this.loop_type, + prompt_id: this.prompt_id, + }; + } + + toLogBody(): string { + return `Loop detected. Type: ${this.loop_type}.`; + } } export class LoopDetectionDisabledEvent implements BaseTelemetryEvent { @@ -330,8 +573,22 @@ export class LoopDetectionDisabledEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.prompt_id = prompt_id; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': this['event.name'], + 'event.timestamp': this['event.timestamp'], + prompt_id: this.prompt_id, + }; + } + + toLogBody(): string { + return `Loop detection disabled.`; + } } +export const EVENT_NEXT_SPEAKER_CHECK = 'gemini_cli.next_speaker_check'; export class NextSpeakerCheckEvent implements BaseTelemetryEvent { 'event.name': 'next_speaker_check'; 'event.timestamp': string; @@ -346,27 +603,61 @@ export class NextSpeakerCheckEvent implements BaseTelemetryEvent { this.finish_reason = finish_reason; this.result = result; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_NEXT_SPEAKER_CHECK, + 'event.timestamp': this['event.timestamp'], + prompt_id: this.prompt_id, + finish_reason: this.finish_reason, + result: this.result, + }; + } + + toLogBody(): string { + return `Next speaker check.`; + } } +export const EVENT_SLASH_COMMAND = 'gemini_cli.slash_command'; export interface SlashCommandEvent extends BaseTelemetryEvent { 'event.name': 'slash_command'; 'event.timestamp': string; command: string; subcommand?: string; status?: SlashCommandStatus; + toOpenTelemetryAttributes(config: Config): LogAttributes; + toLogBody(): string; } export function makeSlashCommandEvent({ command, subcommand, status, -}: Omit): SlashCommandEvent { +}: Omit< + SlashCommandEvent, + CommonFields | 'toOpenTelemetryAttributes' | 'toLogBody' +>): SlashCommandEvent { return { 'event.name': 'slash_command', 'event.timestamp': new Date().toISOString(), command, subcommand, status, + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_SLASH_COMMAND, + 'event.timestamp': this['event.timestamp'], + command: this.command, + subcommand: this.subcommand, + status: this.status, + }; + }, + toLogBody(): string { + return `Slash command: ${this.command}.`; + }, }; } @@ -375,25 +666,45 @@ export enum SlashCommandStatus { ERROR = 'error', } +export const EVENT_CHAT_COMPRESSION = 'gemini_cli.chat_compression'; export interface ChatCompressionEvent extends BaseTelemetryEvent { 'event.name': 'chat_compression'; 'event.timestamp': string; tokens_before: number; tokens_after: number; + toOpenTelemetryAttributes(config: Config): LogAttributes; + toLogBody(): string; } export function makeChatCompressionEvent({ tokens_before, tokens_after, -}: Omit): ChatCompressionEvent { +}: Omit< + ChatCompressionEvent, + CommonFields | 'toOpenTelemetryAttributes' | 'toLogBody' +>): ChatCompressionEvent { return { 'event.name': 'chat_compression', 'event.timestamp': new Date().toISOString(), tokens_before, tokens_after, + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_CHAT_COMPRESSION, + 'event.timestamp': this['event.timestamp'], + tokens_before: this.tokens_before, + tokens_after: this.tokens_after, + }; + }, + toLogBody(): string { + return `Chat compression (Saved ${this.tokens_before - this.tokens_after} tokens)`; + }, }; } +export const EVENT_MALFORMED_JSON_RESPONSE = + 'gemini_cli.malformed_json_response'; export class MalformedJsonResponseEvent implements BaseTelemetryEvent { 'event.name': 'malformed_json_response'; 'event.timestamp': string; @@ -404,6 +715,19 @@ export class MalformedJsonResponseEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.model = model; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_MALFORMED_JSON_RESPONSE, + 'event.timestamp': this['event.timestamp'], + model: this.model, + }; + } + + toLogBody(): string { + return `Malformed JSON response from ${this.model}.`; + } } export enum IdeConnectionType { @@ -411,6 +735,7 @@ export enum IdeConnectionType { SESSION = 'session', } +export const EVENT_IDE_CONNECTION = 'gemini_cli.ide_connection'; export class IdeConnectionEvent { 'event.name': 'ide_connection'; 'event.timestamp': string; @@ -421,8 +746,22 @@ export class IdeConnectionEvent { this['event.timestamp'] = new Date().toISOString(); this.connection_type = connection_type; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_IDE_CONNECTION, + 'event.timestamp': this['event.timestamp'], + connection_type: this.connection_type, + }; + } + + toLogBody(): string { + return `Ide connection. Type: ${this.connection_type}.`; + } } +export const EVENT_CONVERSATION_FINISHED = 'gemini_cli.conversation_finished'; export class ConversationFinishedEvent { 'event_name': 'conversation_finished'; 'event.timestamp': string; // ISO 8601; @@ -435,6 +774,20 @@ export class ConversationFinishedEvent { this.approvalMode = approvalMode; this.turnCount = turnCount; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_CONVERSATION_FINISHED, + 'event.timestamp': this['event.timestamp'], + approvalMode: this.approvalMode, + turnCount: this.turnCount, + }; + } + + toLogBody(): string { + return `Conversation finished.`; + } } export class KittySequenceOverflowEvent { @@ -449,8 +802,23 @@ export class KittySequenceOverflowEvent { // Truncate to first 20 chars for logging (avoid logging sensitive data) this.truncated_sequence = truncated_sequence.substring(0, 20); } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': this['event.name'], + 'event.timestamp': this['event.timestamp'], + sequence_length: this.sequence_length, + truncated_sequence: this.truncated_sequence, + }; + } + + toLogBody(): string { + return `Kitty sequence buffer overflow: ${this.sequence_length} bytes`; + } } +export const EVENT_FILE_OPERATION = 'gemini_cli.file_operation'; export class FileOperationEvent implements BaseTelemetryEvent { 'event.name': 'file_operation'; 'event.timestamp': string; @@ -478,8 +846,37 @@ export class FileOperationEvent implements BaseTelemetryEvent { this.extension = extension; this.programming_language = programming_language; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + const attributes: LogAttributes = { + ...getCommonAttributes(config), + 'event.name': EVENT_FILE_OPERATION, + 'event.timestamp': this['event.timestamp'], + tool_name: this.tool_name, + operation: this.operation, + }; + + if (this.lines) { + attributes['lines'] = this.lines; + } + if (this.mimetype) { + attributes['mimetype'] = this.mimetype; + } + if (this.extension) { + attributes['extension'] = this.extension; + } + if (this.programming_language) { + attributes['programming_language'] = this.programming_language; + } + return attributes; + } + + toLogBody(): string { + return `File operation: ${this.operation}. Lines: ${this.lines}.`; + } } +export const EVENT_INVALID_CHUNK = 'gemini_cli.chat.invalid_chunk'; // Add these new event interfaces export class InvalidChunkEvent implements BaseTelemetryEvent { 'event.name': 'invalid_chunk'; @@ -491,8 +888,26 @@ export class InvalidChunkEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.error_message = error_message; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + const attributes: LogAttributes = { + ...getCommonAttributes(config), + 'event.name': EVENT_INVALID_CHUNK, + 'event.timestamp': this['event.timestamp'], + }; + + if (this.error_message) { + attributes['error.message'] = this.error_message; + } + return attributes; + } + + toLogBody(): string { + return `Invalid chunk received from stream.`; + } } +export const EVENT_CONTENT_RETRY = 'gemini_cli.chat.content_retry'; export class ContentRetryEvent implements BaseTelemetryEvent { 'event.name': 'content_retry'; 'event.timestamp': string; @@ -514,8 +929,26 @@ export class ContentRetryEvent implements BaseTelemetryEvent { this.retry_delay_ms = retry_delay_ms; this.model = model; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_CONTENT_RETRY, + 'event.timestamp': this['event.timestamp'], + attempt_number: this.attempt_number, + error_type: this.error_type, + retry_delay_ms: this.retry_delay_ms, + model: this.model, + }; + } + + toLogBody(): string { + return `Content retry attempt ${this.attempt_number} due to ${this.error_type}.`; + } } +export const EVENT_CONTENT_RETRY_FAILURE = + 'gemini_cli.chat.content_retry_failure'; export class ContentRetryFailureEvent implements BaseTelemetryEvent { 'event.name': 'content_retry_failure'; 'event.timestamp': string; @@ -537,8 +970,25 @@ export class ContentRetryFailureEvent implements BaseTelemetryEvent { this.total_duration_ms = total_duration_ms; this.model = model; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_CONTENT_RETRY_FAILURE, + 'event.timestamp': this['event.timestamp'], + total_attempts: this.total_attempts, + final_error_type: this.final_error_type, + total_duration_ms: this.total_duration_ms, + model: this.model, + }; + } + + toLogBody(): string { + return `All content retries failed after ${this.total_attempts} attempts.`; + } } +export const EVENT_MODEL_ROUTING = 'gemini_cli.model_routing'; export class ModelRoutingEvent implements BaseTelemetryEvent { 'event.name': 'model_routing'; 'event.timestamp': string; @@ -566,8 +1016,27 @@ export class ModelRoutingEvent implements BaseTelemetryEvent { this.failed = failed; this.error_message = error_message; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_MODEL_ROUTING, + 'event.timestamp': this['event.timestamp'], + decision_model: this.decision_model, + decision_source: this.decision_source, + routing_latency_ms: this.routing_latency_ms, + reasoning: this.reasoning, + failed: this.failed, + error_message: this.error_message, + }; + } + + toLogBody(): string { + return `Model routing decision. Model: ${this.decision_model}, Source: ${this.decision_source}`; + } } +export const EVENT_EXTENSION_INSTALL = 'gemini_cli.extension_install'; export class ExtensionInstallEvent implements BaseTelemetryEvent { 'event.name': 'extension_install'; 'event.timestamp': string; @@ -589,8 +1058,25 @@ export class ExtensionInstallEvent implements BaseTelemetryEvent { this.extension_source = extension_source; this.status = status; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_EXTENSION_INSTALL, + 'event.timestamp': this['event.timestamp'], + extension_name: this.extension_name, + extension_version: this.extension_version, + extension_source: this.extension_source, + status: this.status, + }; + } + + toLogBody(): string { + return `Installed extension ${this.extension_name}`; + } } +export const EVENT_TOOL_OUTPUT_TRUNCATED = 'gemini_cli.tool_output_truncated'; export class ToolOutputTruncatedEvent implements BaseTelemetryEvent { readonly eventName = 'tool_output_truncated'; readonly 'event.timestamp' = new Date().toISOString(); @@ -620,8 +1106,28 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent { this.threshold = details.threshold; this.lines = details.lines; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_TOOL_OUTPUT_TRUNCATED, + eventName: this.eventName, + 'event.timestamp': this['event.timestamp'], + tool_name: this.tool_name, + original_content_length: this.original_content_length, + truncated_content_length: this.truncated_content_length, + threshold: this.threshold, + lines: this.lines, + prompt_id: this.prompt_id, + }; + } + + toLogBody(): string { + return `Tool output truncated for ${this.tool_name}.`; + } } +export const EVENT_EXTENSION_UNINSTALL = 'gemini_cli.extension_uninstall'; export class ExtensionUninstallEvent implements BaseTelemetryEvent { 'event.name': 'extension_uninstall'; 'event.timestamp': string; @@ -634,8 +1140,23 @@ export class ExtensionUninstallEvent implements BaseTelemetryEvent { this.extension_name = extension_name; this.status = status; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_EXTENSION_UNINSTALL, + 'event.timestamp': this['event.timestamp'], + extension_name: this.extension_name, + status: this.status, + }; + } + + toLogBody(): string { + return `Uninstalled extension ${this.extension_name}`; + } } +export const EVENT_EXTENSION_ENABLE = 'gemini_cli.extension_enable'; export class ExtensionEnableEvent implements BaseTelemetryEvent { 'event.name': 'extension_enable'; 'event.timestamp': string; @@ -648,8 +1169,23 @@ export class ExtensionEnableEvent implements BaseTelemetryEvent { this.extension_name = extension_name; this.setting_scope = settingScope; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_EXTENSION_ENABLE, + 'event.timestamp': this['event.timestamp'], + extension_name: this.extension_name, + setting_scope: this.setting_scope, + }; + } + + toLogBody(): string { + return `Enabled extension ${this.extension_name}`; + } } +export const EVENT_MODEL_SLASH_COMMAND = 'gemini_cli.slash_command.model'; export class ModelSlashCommandEvent implements BaseTelemetryEvent { 'event.name': 'model_slash_command'; 'event.timestamp': string; @@ -660,6 +1196,19 @@ export class ModelSlashCommandEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.model_name = model_name; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_MODEL_SLASH_COMMAND, + 'event.timestamp': this['event.timestamp'], + model_name: this.model_name, + }; + } + + toLogBody(): string { + return `Model slash command. Model: ${this.model_name}`; + } } export type TelemetryEvent = @@ -693,6 +1242,7 @@ export type TelemetryEvent = | AgentFinishEvent | WebFetchFallbackAttemptEvent; +export const EVENT_EXTENSION_DISABLE = 'gemini_cli.extension_disable'; export class ExtensionDisableEvent implements BaseTelemetryEvent { 'event.name': 'extension_disable'; 'event.timestamp': string; @@ -705,8 +1255,23 @@ export class ExtensionDisableEvent implements BaseTelemetryEvent { this.extension_name = extension_name; this.setting_scope = settingScope; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_EXTENSION_DISABLE, + 'event.timestamp': this['event.timestamp'], + extension_name: this.extension_name, + setting_scope: this.setting_scope, + }; + } + + toLogBody(): string { + return `Disabled extension ${this.extension_name}`; + } } +export const EVENT_SMART_EDIT_STRATEGY = 'gemini_cli.smart_edit_strategy'; export class SmartEditStrategyEvent implements BaseTelemetryEvent { 'event.name': 'smart_edit_strategy'; 'event.timestamp': string; @@ -717,8 +1282,22 @@ export class SmartEditStrategyEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.strategy = strategy; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_SMART_EDIT_STRATEGY, + 'event.timestamp': this['event.timestamp'], + strategy: this.strategy, + }; + } + + toLogBody(): string { + return `Smart Edit Tool Strategy: ${this.strategy}`; + } } +export const EVENT_SMART_EDIT_CORRECTION = 'gemini_cli.smart_edit_correction'; export class SmartEditCorrectionEvent implements BaseTelemetryEvent { 'event.name': 'smart_edit_correction'; 'event.timestamp': string; @@ -729,8 +1308,22 @@ export class SmartEditCorrectionEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.correction = correction; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_SMART_EDIT_CORRECTION, + 'event.timestamp': this['event.timestamp'], + correction: this.correction, + }; + } + + toLogBody(): string { + return `Smart Edit Tool Correction: ${this.correction}`; + } } +export const EVENT_AGENT_START = 'gemini_cli.agent.start'; export class AgentStartEvent implements BaseTelemetryEvent { 'event.name': 'agent_start'; 'event.timestamp': string; @@ -743,8 +1336,23 @@ export class AgentStartEvent implements BaseTelemetryEvent { this.agent_id = agent_id; this.agent_name = agent_name; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_AGENT_START, + 'event.timestamp': this['event.timestamp'], + agent_id: this.agent_id, + agent_name: this.agent_name, + }; + } + + toLogBody(): string { + return `Agent ${this.agent_name} started. ID: ${this.agent_id}`; + } } +export const EVENT_AGENT_FINISH = 'gemini_cli.agent.finish'; export class AgentFinishEvent implements BaseTelemetryEvent { 'event.name': 'agent_finish'; 'event.timestamp': string; @@ -769,8 +1377,27 @@ export class AgentFinishEvent implements BaseTelemetryEvent { this.turn_count = turn_count; this.terminate_reason = terminate_reason; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_AGENT_FINISH, + 'event.timestamp': this['event.timestamp'], + agent_id: this.agent_id, + agent_name: this.agent_name, + duration_ms: this.duration_ms, + turn_count: this.turn_count, + terminate_reason: this.terminate_reason, + }; + } + + toLogBody(): string { + return `Agent ${this.agent_name} finished. Reason: ${this.terminate_reason}. Duration: ${this.duration_ms}ms. Turns: ${this.turn_count}.`; + } } +export const EVENT_WEB_FETCH_FALLBACK_ATTEMPT = + 'gemini_cli.web_fetch_fallback_attempt'; export class WebFetchFallbackAttemptEvent implements BaseTelemetryEvent { 'event.name': 'web_fetch_fallback_attempt'; 'event.timestamp': string; @@ -781,4 +1408,17 @@ export class WebFetchFallbackAttemptEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.reason = reason; } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_WEB_FETCH_FALLBACK_ATTEMPT, + 'event.timestamp': this['event.timestamp'], + reason: this.reason, + }; + } + + toLogBody(): string { + return `Web fetch fallback attempt. Reason: ${this.reason}`; + } } diff --git a/packages/core/src/telemetry/uiTelemetry.test.ts b/packages/core/src/telemetry/uiTelemetry.test.ts index d5ef8099da..9f4aed54e7 100644 --- a/packages/core/src/telemetry/uiTelemetry.test.ts +++ b/packages/core/src/telemetry/uiTelemetry.test.ts @@ -13,7 +13,7 @@ import { EVENT_API_ERROR, EVENT_API_RESPONSE, EVENT_TOOL_CALL, -} from './constants.js'; +} from './types.js'; import type { CompletedToolCall, ErroredToolCall, diff --git a/packages/core/src/telemetry/uiTelemetry.ts b/packages/core/src/telemetry/uiTelemetry.ts index 5917d48501..2b6a813485 100644 --- a/packages/core/src/telemetry/uiTelemetry.ts +++ b/packages/core/src/telemetry/uiTelemetry.ts @@ -9,7 +9,7 @@ import { EVENT_API_ERROR, EVENT_API_RESPONSE, EVENT_TOOL_CALL, -} from './constants.js'; +} from './types.js'; import { ToolCallDecision } from './tool-call-decision.js'; import type {