diff options
| -rw-r--r-- | docs/core/telemetry.md | 7 | ||||
| -rw-r--r-- | packages/core/src/core/client.ts | 1 | ||||
| -rw-r--r-- | packages/core/src/telemetry/loggers.test.ts | 119 | ||||
| -rw-r--r-- | packages/core/src/telemetry/loggers.ts | 3 | ||||
| -rw-r--r-- | packages/core/src/telemetry/types.ts | 1 |
5 files changed, 130 insertions, 1 deletions
diff --git a/docs/core/telemetry.md b/docs/core/telemetry.md index aec63e80..e7b82b65 100644 --- a/docs/core/telemetry.md +++ b/docs/core/telemetry.md @@ -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 diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index a2deca4e..596ddcd7 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -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), }); } diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts new file mode 100644 index 00000000..3493dc49 --- /dev/null +++ b/packages/core/src/telemetry/loggers.test.ts @@ -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', + }, + }); + }); +}); diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index d2b01f65..48275829 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -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) { diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 4e2933a0..f62bd23e 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -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 { |
