mirror of
https://github.com/google-gemini/gemini-cli
synced 2026-05-24 09:38:34 +00:00
Merge 7492314011 into 3cc7e5b096
This commit is contained in:
commit
f00a3fe409
4 changed files with 105 additions and 42 deletions
|
|
@ -99,6 +99,7 @@ import {
|
|||
EmptyWalletMenuShownEvent,
|
||||
CreditPurchaseClickEvent,
|
||||
} from './billingEvents.js';
|
||||
import { safeTruncate } from './utils.js';
|
||||
|
||||
export function logCliConfiguration(
|
||||
config: Config,
|
||||
|
|
@ -124,10 +125,14 @@ export function logCliConfiguration(
|
|||
}
|
||||
|
||||
export function logUserPrompt(config: Config, event: UserPromptEvent): void {
|
||||
// SHIELD: Prevent massive prompt strings from sitting in the buffer heap
|
||||
if (event.prompt) {
|
||||
event.prompt = safeTruncate(event.prompt, 4096);
|
||||
}
|
||||
|
||||
ClearcutLogger.getInstance(config)?.logNewPromptEvent(event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
|
|
@ -233,12 +238,27 @@ export function logFileOperation(
|
|||
}
|
||||
|
||||
export function logApiRequest(config: Config, event: ApiRequestEvent): void {
|
||||
ClearcutLogger.getInstance(config)?.logApiRequestEvent(event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
logger.emit(event.toLogRecord(config));
|
||||
logger.emit(event.toSemanticLogRecord(config));
|
||||
});
|
||||
if (event.request_text) {
|
||||
event.request_text = safeTruncate(event.request_text, 4096);
|
||||
}
|
||||
|
||||
// FIX: Clear structured data that contains the same large text to prevent OOM
|
||||
if (event.prompt && (event.prompt as any).contents) {
|
||||
(event.prompt as any).contents = undefined;
|
||||
}
|
||||
|
||||
// FIX: Explicit mapping to avoid unsafe class spreading
|
||||
const uiEvent: UiEvent = {
|
||||
'event.name': EVENT_API_REQUEST,
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
status_code: event.status_code,
|
||||
duration_ms: event.duration_ms,
|
||||
request_text: event.request_text,
|
||||
auth_type: event.auth_type,
|
||||
model: event.model,
|
||||
role: event.role,
|
||||
};
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
}
|
||||
|
||||
export function logFlashFallback(
|
||||
|
|
@ -305,44 +325,30 @@ export function logApiError(config: Config, event: ApiErrorEvent): void {
|
|||
}
|
||||
|
||||
export function logApiResponse(config: Config, event: ApiResponseEvent): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const uiEvent = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-spread
|
||||
...event,
|
||||
if (event.response_text) {
|
||||
event.response_text = safeTruncate(event.response_text, 4096);
|
||||
}
|
||||
|
||||
// FIX: Clear structured data found by bot
|
||||
if (event.response && (event.response as any).candidates) {
|
||||
(event.response as any).candidates = undefined;
|
||||
}
|
||||
if (event.prompt && (event.prompt as any).contents) {
|
||||
(event.prompt as any).contents = undefined;
|
||||
}
|
||||
|
||||
// FIX: Explicit mapping to avoid @typescript-eslint/no-misused-spread
|
||||
const uiEvent: UiEvent = {
|
||||
'event.name': EVENT_API_RESPONSE,
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
} as UiEvent;
|
||||
status_code: event.status_code,
|
||||
duration_ms: event.duration_ms,
|
||||
response_text: event.response_text,
|
||||
auth_type: event.auth_type,
|
||||
model: event.model,
|
||||
role: event.role,
|
||||
};
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
ClearcutLogger.getInstance(config)?.logApiResponseEvent(event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
logger.emit(event.toLogRecord(config));
|
||||
logger.emit(event.toSemanticLogRecord(config));
|
||||
|
||||
const conventionAttributes = getConventionAttributes(event);
|
||||
|
||||
recordApiResponseMetrics(config, event.duration_ms, {
|
||||
model: event.model,
|
||||
status_code: event.status_code,
|
||||
genAiAttributes: conventionAttributes,
|
||||
});
|
||||
|
||||
const tokenUsageData = [
|
||||
{ count: event.usage.input_token_count, type: 'input' as const },
|
||||
{ count: event.usage.output_token_count, type: 'output' as const },
|
||||
{ count: event.usage.cached_content_token_count, type: 'cache' as const },
|
||||
{ count: event.usage.thoughts_token_count, type: 'thought' as const },
|
||||
{ count: event.usage.tool_token_count, type: 'tool' as const },
|
||||
];
|
||||
|
||||
for (const { count, type } of tokenUsageData) {
|
||||
recordTokenUsageMetrics(config, count, {
|
||||
model: event.model,
|
||||
type,
|
||||
genAiAttributes: conventionAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function logLoopDetected(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import * as path from 'node:path';
|
|||
|
||||
import { authEvents } from '../code_assist/oauth2.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { safeTruncate } from './utils.js';
|
||||
|
||||
vi.mock('@opentelemetry/exporter-trace-otlp-grpc');
|
||||
vi.mock('@opentelemetry/exporter-logs-otlp-grpc');
|
||||
|
|
@ -361,6 +362,31 @@ describe('Telemetry SDK', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should enforce MAX_TELEMETRY_BUFFER_SIZE and evict oldest events', () => {
|
||||
// Fill the buffer past the limit (100)
|
||||
const callbacks = [];
|
||||
for (let i = 0; i < 110; i++) {
|
||||
const cb = vi.fn();
|
||||
callbacks.push(cb);
|
||||
bufferTelemetryEvent(cb);
|
||||
}
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Telemetry buffer full')
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly truncate large strings via safeTruncate', () => {
|
||||
const limit = 2048;
|
||||
const largeString = 'a'.repeat(3000);
|
||||
const result = safeTruncate(largeString, limit);
|
||||
|
||||
// 1. Check for the specific production-ready suffix
|
||||
expect(result).toContain('truncated for performance');
|
||||
|
||||
// 2. Check that the math is RUTHLESS (Total length must be exactly <= limit)
|
||||
expect(result.length).toBeLessThanOrEqual(limit);
|
||||
});
|
||||
|
||||
it('should disable telemetry and log error if useCollector and useCliAuth are both true', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(true);
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCliAuth').mockReturnValue(true);
|
||||
|
|
|
|||
|
|
@ -114,11 +114,18 @@ export function isTelemetrySdkInitialized(): boolean {
|
|||
return telemetryInitialized;
|
||||
}
|
||||
|
||||
const MAX_TELEMETRY_BUFFER_SIZE = 100;
|
||||
|
||||
export function bufferTelemetryEvent(fn: () => void | Promise<void>): void {
|
||||
if (telemetryInitialized) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
fn();
|
||||
} else {
|
||||
// 2. If the buffer is full, remove the oldest event to save memory
|
||||
if (telemetryBuffer.length >= MAX_TELEMETRY_BUFFER_SIZE) {
|
||||
telemetryBuffer.shift();
|
||||
debugLogger.warn('Telemetry buffer full. Dropping oldest event to prevent OOM.');
|
||||
}
|
||||
telemetryBuffer.push(fn);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
packages/core/src/telemetry/utils.ts
Normal file
24
packages/core/src/telemetry/utils.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
const TRUNCATION_SUFFIX = '... (truncated for performance)';
|
||||
|
||||
export function safeTruncate(val: string, limit: number): string;
|
||||
export function safeTruncate(val: unknown, limit: number): unknown;
|
||||
export function safeTruncate(val: unknown, limit: number): unknown {
|
||||
if (typeof val !== 'string' || val.length <= limit) {
|
||||
return val;
|
||||
}
|
||||
|
||||
// Use Array.from to count actual characters (graphemes) instead of UTF-16 units
|
||||
const characters = Array.from(val);
|
||||
if (characters.length <= limit) {
|
||||
return val;
|
||||
}
|
||||
|
||||
const truncateAt = Math.max(0, limit - TRUNCATION_SUFFIX.length);
|
||||
return characters.slice(0, truncateAt).join('') + TRUNCATION_SUFFIX;
|
||||
}
|
||||
Loading…
Reference in a new issue