summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/core/telemetry.md7
-rw-r--r--packages/core/src/core/client.ts1
-rw-r--r--packages/core/src/telemetry/loggers.test.ts119
-rw-r--r--packages/core/src/telemetry/loggers.ts3
-rw-r--r--packages/core/src/telemetry/types.ts1
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 {