add(telemetry): Add OTel logging for FileOperationEvent (#7082)

Co-authored-by: Shnatu <snatu@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Shardul Natu
2025-09-04 10:20:46 -07:00
committed by GitHub
parent cda4280d74
commit 2aa25ba87b
4 changed files with 106 additions and 1 deletions

View File

@@ -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`

View File

@@ -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';

View File

@@ -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',
);
});
});
});

View File

@@ -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,