From 2aa25ba87ba2b86d51215e3c84f9bfaca7870ead Mon Sep 17 00:00:00 2001 From: Shardul Natu <43422294+kiranani@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:20:46 -0700 Subject: [PATCH] add(telemetry): Add OTel logging for `FileOperationEvent` (#7082) Co-authored-by: Shnatu Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- docs/telemetry.md | 14 +++++ packages/core/src/telemetry/constants.ts | 2 +- packages/core/src/telemetry/loggers.test.ts | 62 +++++++++++++++++++++ packages/core/src/telemetry/loggers.ts | 29 ++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/docs/telemetry.md b/docs/telemetry.md index 5a60ddc0fe..71038f5e57 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -195,6 +195,20 @@ Logs are timestamped records of specific events. The following events are logged - `error_type` (if applicable) - `metadata` (if applicable, dictionary of string -> any) +- `gemini_cli.file_operation`: This event occurs for each file operation. + - **Attributes**: + - `tool_name` (string) + - `operation` (string: "create", "read", "update") + - `lines` (int, if applicable) + - `mimetype` (string, if applicable) + - `extension` (string, if applicable) + - `programming_language` (string, if applicable) + - `diff_stat` (json string, if applicable): A JSON string with the following members: + - `ai_added_lines` (int) + - `ai_removed_lines` (int) + - `user_added_lines` (int) + - `user_removed_lines` (int) + - `gemini_cli.api_request`: This event occurs when making a request to Gemini API. - **Attributes**: - `model` diff --git a/packages/core/src/telemetry/constants.ts b/packages/core/src/telemetry/constants.ts index f78fc54480..6b62b6deed 100644 --- a/packages/core/src/telemetry/constants.ts +++ b/packages/core/src/telemetry/constants.ts @@ -24,7 +24,7 @@ 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 METRIC_TOOL_CALL_COUNT = 'gemini_cli.tool.call.count'; export const METRIC_TOOL_CALL_LATENCY = 'gemini_cli.tool.call.latency'; export const METRIC_API_REQUEST_COUNT = 'gemini_cli.api.request.count'; diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 3852fe5653..0407154961 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -29,6 +29,7 @@ import { EVENT_USER_PROMPT, EVENT_FLASH_FALLBACK, EVENT_MALFORMED_JSON_RESPONSE, + EVENT_FILE_OPERATION, } from './constants.js'; import { logApiRequest, @@ -39,6 +40,7 @@ import { logFlashFallback, logChatCompression, logMalformedJsonResponse, + logFileOperation, } from './loggers.js'; import { ToolCallDecision } from './tool-call-decision.js'; import { @@ -50,8 +52,10 @@ import { FlashFallbackEvent, MalformedJsonResponseEvent, makeChatCompressionEvent, + FileOperationEvent, } from './types.js'; import * as metrics from './metrics.js'; +import { FileOperation } from './metrics.js'; import * as sdk from './sdk.js'; import { vi, describe, beforeEach, it, expect } from 'vitest'; import type { GenerateContentResponseUsageMetadata } from '@google/genai'; @@ -890,4 +894,62 @@ describe('loggers', () => { }); }); }); + + describe('logFileOperation', () => { + const mockConfig = { + getSessionId: () => 'test-session-id', + getTargetDir: () => 'target-dir', + getUsageStatisticsEnabled: () => true, + getTelemetryEnabled: () => true, + getTelemetryLogPromptsEnabled: () => true, + } as Config; + + const mockMetrics = { + recordFileOperationMetric: vi.fn(), + }; + + beforeEach(() => { + vi.spyOn(metrics, 'recordFileOperationMetric').mockImplementation( + mockMetrics.recordFileOperationMetric, + ); + }); + + it('should log a file operation event', () => { + const event = new FileOperationEvent( + 'test-tool', + FileOperation.READ, + 10, + 'text/plain', + '.txt', + 'typescript', + ); + + logFileOperation(mockConfig, event); + + expect(mockLogger.emit).toHaveBeenCalledWith({ + body: 'File operation: read. Lines: 10.', + attributes: { + 'session.id': 'test-session-id', + 'user.email': 'test-user@example.com', + 'event.name': EVENT_FILE_OPERATION, + 'event.timestamp': '2025-01-01T00:00:00.000Z', + tool_name: 'test-tool', + operation: 'read', + lines: 10, + mimetype: 'text/plain', + extension: '.txt', + programming_language: 'typescript', + }, + }); + + expect(mockMetrics.recordFileOperationMetric).toHaveBeenCalledWith( + mockConfig, + 'read', + 10, + 'text/plain', + '.txt', + 'typescript', + ); + }); + }); }); diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 1dc6b2a344..06476d3a81 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -26,6 +26,7 @@ import { EVENT_INVALID_CHUNK, EVENT_CONTENT_RETRY, EVENT_CONTENT_RETRY_FAILURE, + EVENT_FILE_OPERATION, } from './constants.js'; import type { ApiErrorEvent, @@ -188,6 +189,34 @@ 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, + }; + logger.emit(logRecord); + recordFileOperationMetric( config, event.operation,