mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
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:
@@ -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`
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user