feat(telemetry): Update API response in telemetry

Adds the text content of the API response to the  telemetry event. This provides more context for debugging and analysis without logging the entire, potentially large, response object.

- Adds an optional  field to the  type.
- Updates  to include the  field in the logged attributes.
- Modifies the  to extract the response text using  and pass it to the logger.
- Adds a new test file for the telemetry loggers, including tests for the  function to verify the new functionality.
This commit is contained in:
jerop 2025-06-11 17:47:21 +00:00 committed by Jerop Kipruto
parent 9237e95f11
commit 03bc1f3141
5 changed files with 130 additions and 1 deletions

View File

@ -291,7 +291,7 @@ These are timestamped records of specific events.
- **Attributes**:
- `model`
- `duration_ms`
- `prompt_token_count`
- `input_token_count`
- `gemini_cli.api_error`: Fired if the API request fails.
@ -310,6 +310,11 @@ These are timestamped records of specific events.
- `duration_ms`
- `error` (optional)
- `attempt`
- `output_token_count`
- `cached_content_token_count`
- `thoughts_token_count`
- `tool_token_count`
- `response_text` (optional)
### Metrics

View File

@ -250,6 +250,7 @@ export class GeminiClient {
response.usageMetadata?.cachedContentTokenCount ?? 0,
thoughts_token_count: response.usageMetadata?.thoughtsTokenCount ?? 0,
tool_token_count: response.usageMetadata?.toolUsePromptTokenCount ?? 0,
response_text: getResponseText(response),
});
}

View File

@ -0,0 +1,119 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { logs } from '@opentelemetry/api-logs';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { Config } from '../config/config.js';
import { EVENT_API_RESPONSE } from './constants.js';
import { logApiResponse } from './loggers.js';
import * as metrics from './metrics.js';
import * as sdk from './sdk.js';
import { vi, describe, beforeEach, it, expect } from 'vitest';
describe('logApiResponse', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
} as Config;
const mockLogger = {
emit: vi.fn(),
};
const mockMetrics = {
recordApiResponseMetrics: vi.fn(),
recordTokenUsageMetrics: vi.fn(),
};
beforeEach(() => {
vi.spyOn(sdk, 'isTelemetrySdkInitialized').mockReturnValue(true);
vi.spyOn(logs, 'getLogger').mockReturnValue(mockLogger);
vi.spyOn(metrics, 'recordApiResponseMetrics').mockImplementation(
mockMetrics.recordApiResponseMetrics,
);
vi.spyOn(metrics, 'recordTokenUsageMetrics').mockImplementation(
mockMetrics.recordTokenUsageMetrics,
);
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-01-01T00:00:00.000Z'));
});
it('should log an API response with all fields', () => {
const event = {
model: 'test-model',
status_code: 200,
duration_ms: 100,
attempt: 1,
output_token_count: 50,
cached_content_token_count: 10,
thoughts_token_count: 5,
tool_token_count: 2,
response_text: 'test-response',
};
logApiResponse(mockConfig, event);
expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'API response from test-model. Status: 200. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
'event.name': EVENT_API_RESPONSE,
'event.timestamp': '2025-01-01T00:00:00.000Z',
[SemanticAttributes.HTTP_STATUS_CODE]: 200,
model: 'test-model',
status_code: 200,
duration_ms: 100,
attempt: 1,
output_token_count: 50,
cached_content_token_count: 10,
thoughts_token_count: 5,
tool_token_count: 2,
response_text: 'test-response',
},
});
expect(mockMetrics.recordApiResponseMetrics).toHaveBeenCalledWith(
mockConfig,
'test-model',
100,
200,
undefined,
);
expect(mockMetrics.recordTokenUsageMetrics).toHaveBeenCalledWith(
mockConfig,
'test-model',
50,
'output',
);
});
it('should log an API response with an error', () => {
const event = {
model: 'test-model',
duration_ms: 100,
attempt: 1,
error: 'test-error',
output_token_count: 50,
cached_content_token_count: 10,
thoughts_token_count: 5,
tool_token_count: 2,
response_text: 'test-response',
};
logApiResponse(mockConfig, event);
expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'API response from test-model. Status: N/A. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
...event,
'event.name': EVENT_API_RESPONSE,
'event.timestamp': '2025-01-01T00:00:00.000Z',
'error.message': 'test-error',
},
});
});
});

View File

@ -195,6 +195,9 @@ export function logApiResponse(
'event.name': EVENT_API_RESPONSE,
'event.timestamp': new Date().toISOString(),
};
if (event.response_text) {
attributes.response_text = event.response_text;
}
if (event.error) {
attributes['error.message'] = event.error;
} else if (event.status_code) {

View File

@ -53,6 +53,7 @@ export interface ApiResponseEvent {
cached_content_token_count: number;
thoughts_token_count: number;
tool_token_count: number;
response_text?: string;
}
export interface CliConfigEvent {