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:
parent
9237e95f11
commit
03bc1f3141
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue