add(OTel): Add OTel logging for MalformedJsonEvent (#6912)

Co-authored-by: Shnatu <snatu@google.com>
Co-authored-by: cornmander <shikhman@google.com>
This commit is contained in:
Shardul Natu
2025-08-24 19:11:41 -07:00
committed by GitHub
parent bedd1d2c20
commit 1918f4466b
5 changed files with 62 additions and 2 deletions

View File

@@ -220,6 +220,10 @@ Logs are timestamped records of specific events. The following events are logged
- `response_text` (if applicable) - `response_text` (if applicable)
- `auth_type` - `auth_type`
- `gemini_cli.malformed_json_response`: This event occurs when a `generateJson` response from Gemini API cannot be parsed as a json.
- **Attributes**:
- `model`
- `gemini_cli.flash_fallback`: This event occurs when Gemini CLI switches to flash as fallback. - `gemini_cli.flash_fallback`: This event occurs when Gemini CLI switches to flash as fallback.
- **Attributes**: - **Attributes**:
- `auth_type` - `auth_type`

View File

@@ -46,13 +46,13 @@ import { ideContext } from '../ide/ideContext.js';
import { import {
logChatCompression, logChatCompression,
logNextSpeakerCheck, logNextSpeakerCheck,
logMalformedJsonResponse,
} from '../telemetry/loggers.js'; } from '../telemetry/loggers.js';
import { import {
makeChatCompressionEvent, makeChatCompressionEvent,
MalformedJsonResponseEvent, MalformedJsonResponseEvent,
NextSpeakerCheckEvent, NextSpeakerCheckEvent,
} from '../telemetry/types.js'; } from '../telemetry/types.js';
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
import { IdeContext, File } from '../ide/ideContext.js'; import { IdeContext, File } from '../ide/ideContext.js';
function isThinkingSupported(model: string) { function isThinkingSupported(model: string) {
@@ -617,7 +617,8 @@ export class GeminiClient {
const prefix = '```json'; const prefix = '```json';
const suffix = '```'; const suffix = '```';
if (text.startsWith(prefix) && text.endsWith(suffix)) { if (text.startsWith(prefix) && text.endsWith(suffix)) {
ClearcutLogger.getInstance(this.config)?.logMalformedJsonResponseEvent( logMalformedJsonResponse(
this.config,
new MalformedJsonResponseEvent(modelToUse), new MalformedJsonResponseEvent(modelToUse),
); );
text = text text = text

View File

@@ -17,6 +17,8 @@ export const EVENT_NEXT_SPEAKER_CHECK = 'gemini_cli.next_speaker_check';
export const EVENT_SLASH_COMMAND = 'gemini_cli.slash_command'; export const EVENT_SLASH_COMMAND = 'gemini_cli.slash_command';
export const EVENT_IDE_CONNECTION = 'gemini_cli.ide_connection'; export const EVENT_IDE_CONNECTION = 'gemini_cli.ide_connection';
export const EVENT_CHAT_COMPRESSION = 'gemini_cli.chat_compression'; export const EVENT_CHAT_COMPRESSION = 'gemini_cli.chat_compression';
export const EVENT_MALFORMED_JSON_RESPONSE =
'gemini_cli.malformed_json_response';
export const METRIC_TOOL_CALL_COUNT = 'gemini_cli.tool.call.count'; 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_TOOL_CALL_LATENCY = 'gemini_cli.tool.call.latency';
export const METRIC_API_REQUEST_COUNT = 'gemini_cli.api.request.count'; export const METRIC_API_REQUEST_COUNT = 'gemini_cli.api.request.count';

View File

@@ -26,6 +26,7 @@ import {
EVENT_TOOL_CALL, EVENT_TOOL_CALL,
EVENT_USER_PROMPT, EVENT_USER_PROMPT,
EVENT_FLASH_FALLBACK, EVENT_FLASH_FALLBACK,
EVENT_MALFORMED_JSON_RESPONSE,
} from './constants.js'; } from './constants.js';
import { import {
logApiRequest, logApiRequest,
@@ -35,6 +36,7 @@ import {
logToolCall, logToolCall,
logFlashFallback, logFlashFallback,
logChatCompression, logChatCompression,
logMalformedJsonResponse,
} from './loggers.js'; } from './loggers.js';
import { ToolCallDecision } from './tool-call-decision.js'; import { ToolCallDecision } from './tool-call-decision.js';
import { import {
@@ -44,6 +46,7 @@ import {
ToolCallEvent, ToolCallEvent,
UserPromptEvent, UserPromptEvent,
FlashFallbackEvent, FlashFallbackEvent,
MalformedJsonResponseEvent,
makeChatCompressionEvent, makeChatCompressionEvent,
} from './types.js'; } from './types.js';
import * as metrics from './metrics.js'; import * as metrics from './metrics.js';
@@ -818,4 +821,31 @@ describe('loggers', () => {
}); });
}); });
}); });
describe('logMalformedJsonResponse', () => {
beforeEach(() => {
vi.spyOn(ClearcutLogger.prototype, 'logMalformedJsonResponseEvent');
});
it('logs the event to Clearcut and OTEL', () => {
const mockConfig = makeFakeConfig();
const event = new MalformedJsonResponseEvent('test-model');
logMalformedJsonResponse(mockConfig, event);
expect(
ClearcutLogger.prototype.logMalformedJsonResponseEvent,
).toHaveBeenCalledWith(event);
expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Malformed JSON response from test-model.',
attributes: {
'session.id': 'test-session-id',
'event.name': EVENT_MALFORMED_JSON_RESPONSE,
'event.timestamp': '2025-01-01T00:00:00.000Z',
model: 'test-model',
},
});
});
});
}); });

View File

@@ -20,6 +20,7 @@ import {
SERVICE_NAME, SERVICE_NAME,
EVENT_SLASH_COMMAND, EVENT_SLASH_COMMAND,
EVENT_CHAT_COMPRESSION, EVENT_CHAT_COMPRESSION,
EVENT_MALFORMED_JSON_RESPONSE,
} from './constants.js'; } from './constants.js';
import { import {
ApiErrorEvent, ApiErrorEvent,
@@ -36,6 +37,7 @@ import {
SlashCommandEvent, SlashCommandEvent,
KittySequenceOverflowEvent, KittySequenceOverflowEvent,
ChatCompressionEvent, ChatCompressionEvent,
MalformedJsonResponseEvent,
} from './types.js'; } from './types.js';
import { import {
recordApiErrorMetrics, recordApiErrorMetrics,
@@ -449,3 +451,24 @@ export function logKittySequenceOverflow(
}; };
logger.emit(logRecord); logger.emit(logRecord);
} }
export function logMalformedJsonResponse(
config: Config,
event: MalformedJsonResponseEvent,
): void {
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,
};
logger.emit(logRecord);
}