summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/core/telemetry.md65
-rw-r--r--package.json3
-rw-r--r--packages/cli/src/config/config.test.ts97
-rw-r--r--packages/cli/src/config/config.ts39
-rw-r--r--packages/cli/src/config/settings.ts4
-rw-r--r--packages/core/src/config/config.test.ts91
-rw-r--r--packages/core/src/config/config.ts48
-rw-r--r--packages/core/src/core/geminiChat.test.ts2
-rw-r--r--packages/core/src/core/geminiChat.ts2
-rw-r--r--packages/core/src/telemetry/index.ts9
-rw-r--r--packages/core/src/telemetry/loggers.test.ts6
-rw-r--r--packages/core/src/telemetry/loggers.ts4
-rwxr-xr-xscripts/local_telemetry.js27
-rwxr-xr-xscripts/telemetry.js85
-rwxr-xr-xscripts/telemetry_gcp.js12
-rw-r--r--scripts/telemetry_utils.js39
16 files changed, 447 insertions, 86 deletions
diff --git a/docs/core/telemetry.md b/docs/core/telemetry.md
index c2c69eb9..c42c2ed6 100644
--- a/docs/core/telemetry.md
+++ b/docs/core/telemetry.md
@@ -15,19 +15,19 @@ This entire system is built on the **[OpenTelemetry] (OTEL)** standard, allowing
- You have exported the `GOOGLE_CLOUD_PROJECT` environment variable.
- You have authenticated with Google Cloud and have the necessary IAM roles.
For full details, see the [Google Cloud](#google-cloud) prerequisites.
-2. **Run the Script:** Execute the following command from the project root:
+1. **Run the Command:** Execute the following command from the project root:
```bash
- ./scripts/telemetry_gcp.js
+ npm run telemetry -- --target=gcp
```
-3. **Run Gemini CLI:** In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
-4. **View Data:** The script will provide links to view your telemetry data (traces, metrics, logs) in the Google Cloud Console.
-5. **Details:** Refer to documentation for telemetry in [Google Cloud](#google-cloud).
+1. **Run Gemini CLI:** In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
+1. **View Data:** The script will provide links to view your telemetry data (traces, metrics, logs) in the Google Cloud Console.
+1. **Details:** Refer to documentation for telemetry in [Google Cloud](#google-cloud).
### Local Telemetry with Jaeger UI (for Traces)
-1. **Run the Script:** Execute the following command from the project root:
+1. **Run the Command:** Execute the following command from the project root:
```bash
- ./scripts/local_telemetry.js
+ npm run telemetry -- --target=local
```
2. **Run Gemini CLI:** In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
3. **View Logs/Metrics:** Check the `.gemini/otel/collector.log` file for raw logs and metrics.
@@ -42,16 +42,35 @@ You can enable telemetry in multiple ways. [Configuration](configuration.md) is
**Order of Precedence:**
-1. **CLI Flag (`--telemetry`):** These override all other settings for the current session.
-2. **Workspace Settings File (`.gemini/settings.json`):** If no CLI flag is used, the `telemetry` value from this project-specific file is used.
-3. **User Settings File (`~/.gemini/settings.json`):** If not set by a flag or workspace settings, the value from this global user file is used.
-4. **Default:** If telemetry is not configured by a flag or in any settings file, it is disabled.
+Telemetry settings are resolved in the following order (highest precedence first):
-Add these lines to enable telemetry by in workspace (`.gemini/settings.json`) or user (`~/.gemini/settings.json`) settings:
+1. **CLI Flags (for `gemini` command):**
+ - `--telemetry` / `--no-telemetry`: Overrides `telemetry.enabled`. If this flag is not provided, telemetry is disabled unless enabled in settings files.
+ - `--telemetry-target <local|gcp>`: Overrides `telemetry.target`.
+ - `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`.
+ - `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`.
+2. **Environment Variables:**
+ - `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint` if no `--telemetry-otlp-endpoint` flag is present.
+3. **Workspace Settings File (`.gemini/settings.json`):** Values from the `telemetry` object in this project-specific file.
+4. **User Settings File (`~/.gemini/settings.json`):** Values from the `telemetry` object in this global user file.
+5. **Defaults:** applied if not set by any of the above.
+ - `telemetry.enabled`: `false`
+ - `telemetry.target`: `local`
+ - `telemetry.otlpEndpoint`: `http://localhost:4317`
+ - `telemetry.logPrompts`: `true`
+
+**For the `npm run telemetry -- --target=<gcp|local>` script:**
+The `--target` argument to this script _only_ overrides the `telemetry.target` for the duration and purpose of that script (i.e., choosing which collector to start). It does not permanently change your `settings.json`. The script will first look at `settings.json` for a `telemetry.target` to use as its default.
+
+**Example settings:**
+Add these lines to configure telemetry in your workspace (`.gemini/settings.json`) or user (`~/.gemini/settings.json`) settings for GCP:
```json
{
- "telemetry": true,
+ "telemetry": {
+ "enabled": true,
+ "target": "gcp"
+ },
"sandbox": false
}
```
@@ -80,13 +99,13 @@ mkdir .gemini/otel
### Local
-Use the `scripts/local_telemetry.js` script that automates the entire process of setting up a local telemetry pipeline, including configuring the necessary settings in your `.gemini/settings.json` file. The script installs `otelcol-contrib` (The OpenTelemetry Collector) and `jaeger` (The Jaeger UI for viewing traces). To use it:
+Use the `npm run telemetry -- --target=local` command which automates the entire process of setting up a local telemetry pipeline, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib` (The OpenTelemetry Collector) and `jaeger` (The Jaeger UI for viewing traces). To use it:
-1. **Run the Script**:
- Execute the script from the root of the repository:
+1. **Run the Command**:
+ Execute the command from the root of the repository:
```bash
- ./scripts/local_telemetry.js
+ npm run telemetry -- --target=local
```
The script will:
@@ -110,7 +129,7 @@ Use the `scripts/local_telemetry.js` script that automates the entire process of
### Google Cloud
-For a streamlined setup targeting Google Cloud, use the `scripts/telemetry_gcp.js` script which automates setting up a local OpenTelemetry collector that forwards data to your Google Cloud project.
+Use the `npm run telemetry -- --target=gcp` command which automates setting up a local OpenTelemetry collector that forwards data to your Google Cloud project, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib`. To use it:
1. **Prerequisites**:
@@ -122,11 +141,11 @@ For a streamlined setup targeting Google Cloud, use the `scripts/telemetry_gcp.j
- Authenticate with Google Cloud (e.g., run `gcloud auth application-default login` or ensure `GOOGLE_APPLICATION_CREDENTIALS` is set).
- Ensure your account/service account has the necessary roles: "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer".
-2. **Run the Script**:
- Execute the script from the root of the repository:
+2. **Run the Command**:
+ Execute the command from the root of the repository:
```bash
- ./scripts/telemetry_gcp.js
+ npm run telemetry -- --target=gcp
```
The script will:
@@ -172,7 +191,7 @@ These are timestamped records of specific events.
- `api_key_enabled` (boolean)
- `vertex_ai_enabled` (boolean)
- `code_assist_enabled` (boolean)
- - `log_user_prompts_enabled` (boolean)
+ - `log_prompts_enabled` (boolean)
- `file_filtering_respect_git_ignore` (boolean)
- `debug_mode` (boolean)
- `mcp_servers` (string)
@@ -181,7 +200,7 @@ These are timestamped records of specific events.
- **Attributes**:
- `prompt_length`
- - `prompt` (except if `log_user_prompts_enabled` is false)
+ - `prompt` (except if `log_prompts_enabled` is false)
- `gemini_cli.tool_call`: Fired for every function call.
diff --git a/package.json b/package.json
index 4eae069c..563e09ac 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"prepare:cli-packagejson": "node scripts/prepare-cli-packagejson.js",
"publish:sandbox": "node scripts/publish-sandbox.js",
"publish:npm": "npm publish --workspaces ${NPM_PUBLISH_TAG:+--tag=$NPM_PUBLISH_TAG} ${NPM_DRY_RUN:+--dry-run}",
- "publish:release": "npm run build:packages && npm run prepare:cli-packagejson && npm run build:docker && npm run tag:docker && npm run publish:sandbox && npm run publish:npm"
+ "publish:release": "npm run build:packages && npm run prepare:cli-packagejson && npm run build:docker && npm run tag:docker && npm run publish:sandbox && npm run publish:npm",
+ "telemetry": "node scripts/telemetry.js"
},
"bin": {
"gemini": "bundle/gemini.js"
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index 1d8c486a..6afe7f6e 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -127,31 +127,120 @@ describe('loadCliConfig telemetry', () => {
it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => {
process.argv = ['node', 'script.js'];
- const settings: Settings = { telemetry: true };
+ const settings: Settings = { telemetry: { enabled: true } };
const config = await loadCliConfig(settings, [], [], 'test-session');
expect(config.getTelemetryEnabled()).toBe(true);
});
it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => {
process.argv = ['node', 'script.js'];
- const settings: Settings = { telemetry: false };
+ const settings: Settings = { telemetry: { enabled: false } };
const config = await loadCliConfig(settings, [], [], 'test-session');
expect(config.getTelemetryEnabled()).toBe(false);
});
it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => {
process.argv = ['node', 'script.js', '--telemetry'];
- const settings: Settings = { telemetry: false };
+ const settings: Settings = { telemetry: { enabled: false } };
const config = await loadCliConfig(settings, [], [], 'test-session');
expect(config.getTelemetryEnabled()).toBe(true);
});
it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => {
process.argv = ['node', 'script.js', '--no-telemetry'];
- const settings: Settings = { telemetry: true };
+ const settings: Settings = { telemetry: { enabled: true } };
const config = await loadCliConfig(settings, [], [], 'test-session');
expect(config.getTelemetryEnabled()).toBe(false);
});
+
+ it('should use telemetry OTLP endpoint from settings if CLI flag is not present', async () => {
+ process.argv = ['node', 'script.js'];
+ const settings: Settings = {
+ telemetry: { otlpEndpoint: 'http://settings.example.com' },
+ };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryOtlpEndpoint()).toBe(
+ 'http://settings.example.com',
+ );
+ });
+
+ it('should prioritize --telemetry-otlp-endpoint CLI flag over settings', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '--telemetry-otlp-endpoint',
+ 'http://cli.example.com',
+ ];
+ const settings: Settings = {
+ telemetry: { otlpEndpoint: 'http://settings.example.com' },
+ };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryOtlpEndpoint()).toBe('http://cli.example.com');
+ });
+
+ it('should use default endpoint if no OTLP endpoint is provided via CLI or settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const settings: Settings = { telemetry: { enabled: true } };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317');
+ });
+
+ it('should use telemetry target from settings if CLI flag is not present', async () => {
+ process.argv = ['node', 'script.js'];
+ const settings: Settings = {
+ telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
+ };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryTarget()).toBe(
+ ServerConfig.DEFAULT_TELEMETRY_TARGET,
+ );
+ });
+
+ it('should prioritize --telemetry-target CLI flag over settings', async () => {
+ process.argv = ['node', 'script.js', '--telemetry-target', 'gcp'];
+ const settings: Settings = {
+ telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
+ };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryTarget()).toBe('gcp');
+ });
+
+ it('should use default target if no target is provided via CLI or settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const settings: Settings = { telemetry: { enabled: true } };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryTarget()).toBe(
+ ServerConfig.DEFAULT_TELEMETRY_TARGET,
+ );
+ });
+
+ it('should use telemetry log prompts from settings if CLI flag is not present', async () => {
+ process.argv = ['node', 'script.js'];
+ const settings: Settings = { telemetry: { logPrompts: false } };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
+ });
+
+ it('should prioritize --telemetry-log-prompts CLI flag (true) over settings (false)', async () => {
+ process.argv = ['node', 'script.js', '--telemetry-log-prompts'];
+ const settings: Settings = { telemetry: { logPrompts: false } };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
+ });
+
+ it('should prioritize --no-telemetry-log-prompts CLI flag (false) over settings (true)', async () => {
+ process.argv = ['node', 'script.js', '--no-telemetry-log-prompts'];
+ const settings: Settings = { telemetry: { logPrompts: true } };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
+ });
+
+ it('should use default log prompts (true) if no value is provided via CLI or settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const settings: Settings = { telemetry: { enabled: true } };
+ const config = await loadCliConfig(settings, [], [], 'test-session');
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
+ });
});
describe('API Key Handling', () => {
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index ca7cfa48..b737daa4 100644
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -18,6 +18,7 @@ import {
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_EMBEDDING_MODEL,
FileDiscoveryService,
+ TelemetryTarget,
} from '@gemini-cli/core';
import { Settings } from './settings.js';
import { getEffectiveModel } from '../utils/modelCheck.js';
@@ -47,6 +48,9 @@ interface CliArgs {
yolo: boolean | undefined;
telemetry: boolean | undefined;
checkpoint: boolean | undefined;
+ telemetryTarget: string | undefined;
+ telemetryOtlpEndpoint: string | undefined;
+ telemetryLogPrompts: boolean | undefined;
}
async function parseArguments(): Promise<CliArgs> {
@@ -93,7 +97,24 @@ async function parseArguments(): Promise<CliArgs> {
})
.option('telemetry', {
type: 'boolean',
- description: 'Enable telemetry?',
+ description:
+ 'Enable telemetry? This flag specifically controls if telemetry is sent. Other --telemetry-* flags set specific values but do not enable telemetry on their own.',
+ })
+ .option('telemetry-target', {
+ type: 'string',
+ choices: ['local', 'gcp'],
+ description:
+ 'Set the telemetry target (local or gcp). Overrides settings files.',
+ })
+ .option('telemetry-otlp-endpoint', {
+ type: 'string',
+ description:
+ 'Set the OTLP endpoint for telemetry. Overrides environment variables and settings files.',
+ })
+ .option('telemetry-log-prompts', {
+ type: 'boolean',
+ description:
+ 'Enable or disable logging of user prompts for telemetry. Overrides settings files.',
})
.option('checkpoint', {
alias: 'c',
@@ -190,10 +211,16 @@ export async function loadCliConfig(
showMemoryUsage:
argv.show_memory_usage || settings.showMemoryUsage || false,
accessibility: settings.accessibility,
- telemetry:
- argv.telemetry !== undefined
- ? argv.telemetry
- : (settings.telemetry ?? false),
+ telemetry: {
+ enabled: argv.telemetry ?? settings.telemetry?.enabled,
+ target: (argv.telemetryTarget ??
+ settings.telemetry?.target) as TelemetryTarget,
+ otlpEndpoint:
+ argv.telemetryOtlpEndpoint ??
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
+ settings.telemetry?.otlpEndpoint,
+ logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
+ },
// Git-aware file filtering settings
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore,
checkpoint: argv.checkpoint,
@@ -203,8 +230,6 @@ export async function loadCliConfig(
process.env.HTTP_PROXY ||
process.env.http_proxy,
cwd: process.cwd(),
- telemetryOtlpEndpoint:
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? settings.telemetryOtlpEndpoint,
fileDiscoveryService: fileService,
bugCommand: settings.bugCommand,
});
diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts
index a0030a05..b17b4c9d 100644
--- a/packages/cli/src/config/settings.ts
+++ b/packages/cli/src/config/settings.ts
@@ -11,6 +11,7 @@ import {
MCPServerConfig,
getErrorMessage,
BugCommandSettings,
+ TelemetrySettings,
} from '@gemini-cli/core';
import stripJsonComments from 'strip-json-comments';
import { DefaultLight } from '../ui/themes/default-light.js';
@@ -41,8 +42,7 @@ export interface Settings {
showMemoryUsage?: boolean;
contextFileName?: string | string[];
accessibility?: AccessibilitySettings;
- telemetry?: boolean;
- telemetryOtlpEndpoint?: string;
+ telemetry?: TelemetrySettings;
preferredEditor?: string;
bugCommand?: BugCommandSettings;
diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts
index ea555bd4..e8f56f75 100644
--- a/packages/core/src/config/config.test.ts
+++ b/packages/core/src/config/config.test.ts
@@ -8,6 +8,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Config, ConfigParameters } from './config.js';
import * as path from 'path';
import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js';
+import {
+ DEFAULT_TELEMETRY_TARGET,
+ DEFAULT_OTLP_ENDPOINT,
+} from '../telemetry/index.js';
// Mock dependencies that might be called during Config construction or createServerConfig
vi.mock('../tools/tool-registry', () => {
@@ -38,6 +42,14 @@ vi.mock('../tools/memoryTool', () => ({
GEMINI_CONFIG_DIR: '.gemini',
}));
+vi.mock('../telemetry/index.js', async (importOriginal) => {
+ const actual = await importOriginal<typeof import('../telemetry/index.js')>();
+ return {
+ ...actual,
+ initializeTelemetry: vi.fn(),
+ };
+});
+
describe('Server Config (config.ts)', () => {
const API_KEY = 'server-api-key';
const MODEL = 'gemini-pro';
@@ -47,7 +59,7 @@ describe('Server Config (config.ts)', () => {
const QUESTION = 'test question';
const FULL_CONTEXT = false;
const USER_MEMORY = 'Test User Memory';
- const TELEMETRY = false;
+ const TELEMETRY_SETTINGS = { enabled: false };
const EMBEDDING_MODEL = 'gemini-embedding';
const SESSION_ID = 'test-session-id';
const baseParams: ConfigParameters = {
@@ -63,7 +75,7 @@ describe('Server Config (config.ts)', () => {
question: QUESTION,
fullContext: FULL_CONTEXT,
userMemory: USER_MEMORY,
- telemetry: TELEMETRY,
+ telemetry: TELEMETRY_SETTINGS,
sessionId: SESSION_ID,
};
@@ -120,7 +132,7 @@ describe('Server Config (config.ts)', () => {
it('Config constructor should set telemetry to true when provided as true', () => {
const paramsWithTelemetry: ConfigParameters = {
...baseParams,
- telemetry: true,
+ telemetry: { enabled: true },
};
const config = new Config(paramsWithTelemetry);
expect(config.getTelemetryEnabled()).toBe(true);
@@ -129,7 +141,7 @@ describe('Server Config (config.ts)', () => {
it('Config constructor should set telemetry to false when provided as false', () => {
const paramsWithTelemetry: ConfigParameters = {
...baseParams,
- telemetry: false,
+ telemetry: { enabled: false },
};
const config = new Config(paramsWithTelemetry);
expect(config.getTelemetryEnabled()).toBe(false);
@@ -139,7 +151,7 @@ describe('Server Config (config.ts)', () => {
const paramsWithoutTelemetry: ConfigParameters = { ...baseParams };
delete paramsWithoutTelemetry.telemetry;
const config = new Config(paramsWithoutTelemetry);
- expect(config.getTelemetryEnabled()).toBe(TELEMETRY);
+ expect(config.getTelemetryEnabled()).toBe(TELEMETRY_SETTINGS.enabled);
});
it('should have a getFileService method that returns FileDiscoveryService', () => {
@@ -147,4 +159,73 @@ describe('Server Config (config.ts)', () => {
const fileService = config.getFileService();
expect(fileService).toBeDefined();
});
+
+ describe('Telemetry Settings', () => {
+ it('should return default telemetry target if not provided', () => {
+ const params: ConfigParameters = {
+ ...baseParams,
+ telemetry: { enabled: true },
+ };
+ const config = new Config(params);
+ expect(config.getTelemetryTarget()).toBe(DEFAULT_TELEMETRY_TARGET);
+ });
+
+ it('should return provided OTLP endpoint', () => {
+ const endpoint = 'http://custom.otel.collector:4317';
+ const params: ConfigParameters = {
+ ...baseParams,
+ telemetry: { enabled: true, otlpEndpoint: endpoint },
+ };
+ const config = new Config(params);
+ expect(config.getTelemetryOtlpEndpoint()).toBe(endpoint);
+ });
+
+ it('should return default OTLP endpoint if not provided', () => {
+ const params: ConfigParameters = {
+ ...baseParams,
+ telemetry: { enabled: true },
+ };
+ const config = new Config(params);
+ expect(config.getTelemetryOtlpEndpoint()).toBe(DEFAULT_OTLP_ENDPOINT);
+ });
+
+ it('should return provided logPrompts setting', () => {
+ const params: ConfigParameters = {
+ ...baseParams,
+ telemetry: { enabled: true, logPrompts: false },
+ };
+ const config = new Config(params);
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
+ });
+
+ it('should return default logPrompts setting (true) if not provided', () => {
+ const params: ConfigParameters = {
+ ...baseParams,
+ telemetry: { enabled: true },
+ };
+ const config = new Config(params);
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
+ });
+
+ it('should return default logPrompts setting (true) if telemetry object is not provided', () => {
+ const paramsWithoutTelemetry: ConfigParameters = { ...baseParams };
+ delete paramsWithoutTelemetry.telemetry;
+ const config = new Config(paramsWithoutTelemetry);
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
+ });
+
+ it('should return default telemetry target if telemetry object is not provided', () => {
+ const paramsWithoutTelemetry: ConfigParameters = { ...baseParams };
+ delete paramsWithoutTelemetry.telemetry;
+ const config = new Config(paramsWithoutTelemetry);
+ expect(config.getTelemetryTarget()).toBe(DEFAULT_TELEMETRY_TARGET);
+ });
+
+ it('should return default OTLP endpoint if telemetry object is not provided', () => {
+ const paramsWithoutTelemetry: ConfigParameters = { ...baseParams };
+ delete paramsWithoutTelemetry.telemetry;
+ const config = new Config(paramsWithoutTelemetry);
+ expect(config.getTelemetryOtlpEndpoint()).toBe(DEFAULT_OTLP_ENDPOINT);
+ });
+ });
});
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index d841f4b3..891b6302 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -23,7 +23,12 @@ import { GeminiClient } from '../core/client.js';
import { GEMINI_CONFIG_DIR as GEMINI_DIR } from '../tools/memoryTool.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { GitService } from '../services/gitService.js';
-import { initializeTelemetry } from '../telemetry/index.js';
+import {
+ initializeTelemetry,
+ DEFAULT_TELEMETRY_TARGET,
+ DEFAULT_OTLP_ENDPOINT,
+ TelemetryTarget,
+} from '../telemetry/index.js';
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from './models.js';
export enum ApprovalMode {
@@ -40,6 +45,13 @@ export interface BugCommandSettings {
urlTemplate: string;
}
+export interface TelemetrySettings {
+ enabled?: boolean;
+ target?: TelemetryTarget;
+ otlpEndpoint?: string;
+ logPrompts?: boolean;
+}
+
export class MCPServerConfig {
constructor(
// For stdio transport
@@ -82,9 +94,7 @@ export interface ConfigParameters {
showMemoryUsage?: boolean;
contextFileName?: string | string[];
accessibility?: AccessibilitySettings;
- telemetry?: boolean;
- telemetryLogUserPromptsEnabled?: boolean;
- telemetryOtlpEndpoint?: string;
+ telemetry?: TelemetrySettings;
fileFilteringRespectGitIgnore?: boolean;
checkpoint?: boolean;
proxy?: string;
@@ -114,9 +124,7 @@ export class Config {
private approvalMode: ApprovalMode;
private readonly showMemoryUsage: boolean;
private readonly accessibility: AccessibilitySettings;
- private readonly telemetry: boolean;
- private readonly telemetryLogUserPromptsEnabled: boolean;
- private readonly telemetryOtlpEndpoint: string;
+ private readonly telemetrySettings: TelemetrySettings;
private readonly geminiClient: GeminiClient;
private readonly fileFilteringRespectGitIgnore: boolean;
private fileDiscoveryService: FileDiscoveryService | null = null;
@@ -147,11 +155,13 @@ export class Config {
this.approvalMode = params.approvalMode ?? ApprovalMode.DEFAULT;
this.showMemoryUsage = params.showMemoryUsage ?? false;
this.accessibility = params.accessibility ?? {};
- this.telemetry = params.telemetry ?? false;
- this.telemetryLogUserPromptsEnabled =
- params.telemetryLogUserPromptsEnabled ?? true;
- this.telemetryOtlpEndpoint =
- params.telemetryOtlpEndpoint ?? 'http://localhost:4317';
+ this.telemetrySettings = {
+ enabled: params.telemetry?.enabled ?? false,
+ target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET,
+ otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT,
+ logPrompts: params.telemetry?.logPrompts ?? true,
+ };
+
this.fileFilteringRespectGitIgnore =
params.fileFilteringRespectGitIgnore ?? true;
this.checkpoint = params.checkpoint ?? false;
@@ -167,7 +177,7 @@ export class Config {
this.toolRegistry = createToolRegistry(this);
this.geminiClient = new GeminiClient(this);
- if (this.telemetry) {
+ if (this.telemetrySettings.enabled) {
initializeTelemetry(this);
}
}
@@ -272,15 +282,19 @@ export class Config {
}
getTelemetryEnabled(): boolean {
- return this.telemetry;
+ return this.telemetrySettings.enabled ?? false;
}
- getTelemetryLogUserPromptsEnabled(): boolean {
- return this.telemetryLogUserPromptsEnabled;
+ getTelemetryLogPromptsEnabled(): boolean {
+ return this.telemetrySettings.logPrompts ?? true;
}
getTelemetryOtlpEndpoint(): string {
- return this.telemetryOtlpEndpoint;
+ return this.telemetrySettings.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT;
+ }
+
+ getTelemetryTarget(): TelemetryTarget {
+ return this.telemetrySettings.target ?? DEFAULT_TELEMETRY_TARGET;
}
getGeminiClient(): GeminiClient {
diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts
index 24a7279d..9961103d 100644
--- a/packages/core/src/core/geminiChat.test.ts
+++ b/packages/core/src/core/geminiChat.test.ts
@@ -26,7 +26,7 @@ const mockModelsModule = {
const mockConfig = {
getSessionId: () => 'test-session-id',
- getTelemetryLogUserPromptsEnabled: () => true,
+ getTelemetryLogPromptsEnabled: () => true,
} as unknown as Config;
describe('GeminiChat', () => {
diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts
index eb740c4a..1268e8c2 100644
--- a/packages/core/src/core/geminiChat.ts
+++ b/packages/core/src/core/geminiChat.ts
@@ -153,7 +153,7 @@ export class GeminiChat {
model: string,
): Promise<void> {
const shouldLogUserPrompts = (config: Config): boolean =>
- config.getTelemetryLogUserPromptsEnabled() ?? false;
+ config.getTelemetryLogPromptsEnabled() ?? false;
const requestText = this._getRequestTextFromContents(contents);
logApiRequest(this.config, {
diff --git a/packages/core/src/telemetry/index.ts b/packages/core/src/telemetry/index.ts
index e8248bf9..32e98144 100644
--- a/packages/core/src/telemetry/index.ts
+++ b/packages/core/src/telemetry/index.ts
@@ -4,6 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
+export enum TelemetryTarget {
+ GCP = 'gcp',
+ LOCAL = 'local',
+}
+
+const DEFAULT_TELEMETRY_TARGET = TelemetryTarget.LOCAL;
+const DEFAULT_OTLP_ENDPOINT = 'http://localhost:4317';
+
+export { DEFAULT_TELEMETRY_TARGET, DEFAULT_OTLP_ENDPOINT };
export {
initializeTelemetry,
shutdownTelemetry,
diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts
index 2f909c22..6ec73853 100644
--- a/packages/core/src/telemetry/loggers.test.ts
+++ b/packages/core/src/telemetry/loggers.test.ts
@@ -60,7 +60,7 @@ describe('loggers', () => {
vertexai: true,
codeAssist: false,
}),
- getTelemetryLogUserPromptsEnabled: () => true,
+ getTelemetryLogPromptsEnabled: () => true,
getFileFilteringRespectGitIgnore: () => true,
getDebugMode: () => true,
getMcpServers: () => ({
@@ -99,7 +99,7 @@ describe('loggers', () => {
describe('logUserPrompt', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
- getTelemetryLogUserPromptsEnabled: () => true,
+ getTelemetryLogPromptsEnabled: () => true,
} as unknown as Config;
it('should log a user prompt', () => {
@@ -125,7 +125,7 @@ describe('loggers', () => {
it('should not log prompt if disabled', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
- getTelemetryLogUserPromptsEnabled: () => false,
+ getTelemetryLogPromptsEnabled: () => false,
} as unknown as Config;
const event = {
prompt: 'test-prompt',
diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts
index e788119c..01e83908 100644
--- a/packages/core/src/telemetry/loggers.ts
+++ b/packages/core/src/telemetry/loggers.ts
@@ -37,7 +37,7 @@ import {
} from '@google/genai';
const shouldLogUserPrompts = (config: Config): boolean =>
- config.getTelemetryLogUserPromptsEnabled() ?? false;
+ config.getTelemetryLogPromptsEnabled() ?? false;
function getCommonAttributes(config: Config): LogAttributes {
return {
@@ -86,7 +86,7 @@ export function logCliConfiguration(config: Config): void {
api_key_enabled: !!generatorConfig.apiKey,
vertex_ai_enabled: !!generatorConfig.vertexai,
code_assist_enabled: !!generatorConfig.codeAssist,
- log_user_prompts_enabled: config.getTelemetryLogUserPromptsEnabled(),
+ log_user_prompts_enabled: config.getTelemetryLogPromptsEnabled(),
file_filtering_respect_git_ignore:
config.getFileFilteringRespectGitIgnore(),
debug_mode: config.getDebugMode(),
diff --git a/scripts/local_telemetry.js b/scripts/local_telemetry.js
index 74e7f750..5d8c564f 100755
--- a/scripts/local_telemetry.js
+++ b/scripts/local_telemetry.js
@@ -348,8 +348,13 @@ async function main() {
// Restore original settings
const finalSettings = readJsonFile(WORKSPACE_SETTINGS_FILE);
- delete finalSettings.telemetry;
- delete finalSettings.telemetryOtlpEndpoint;
+ if (finalSettings.telemetry) {
+ delete finalSettings.telemetry.enabled;
+ delete finalSettings.telemetry.otlpEndpoint;
+ if (Object.keys(finalSettings.telemetry).length === 0) {
+ delete finalSettings.telemetry;
+ }
+ }
finalSettings.sandbox = originalSandboxSetting;
writeJsonFile(WORKSPACE_SETTINGS_FILE, finalSettings);
console.log('āœ… Restored original telemetry and sandbox settings.');
@@ -393,8 +398,12 @@ async function main() {
const originalSandboxSetting = workspaceSettings.sandbox;
let settingsModified = false;
- if (workspaceSettings.telemetry !== true) {
- workspaceSettings.telemetry = true;
+ if (typeof workspaceSettings.telemetry !== 'object') {
+ workspaceSettings.telemetry = {};
+ }
+
+ if (workspaceSettings.telemetry.enabled !== true) {
+ workspaceSettings.telemetry.enabled = true;
settingsModified = true;
console.log('āš™ļø Enabled telemetry in workspace settings.');
}
@@ -405,12 +414,18 @@ async function main() {
console.log('āœ… Disabled sandbox mode for local telemetry.');
}
- if (workspaceSettings.telemetryOtlpEndpoint !== 'http://localhost:4317') {
- workspaceSettings.telemetryOtlpEndpoint = 'http://localhost:4317';
+ if (workspaceSettings.telemetry.otlpEndpoint !== 'http://localhost:4317') {
+ workspaceSettings.telemetry.otlpEndpoint = 'http://localhost:4317';
settingsModified = true;
console.log('šŸ”§ Set telemetry endpoint to http://localhost:4317.');
}
+ if (workspaceSettings.telemetry.target !== 'local') {
+ workspaceSettings.telemetry.target = 'local';
+ settingsModified = true;
+ console.log('šŸŽÆ Set telemetry target to local.');
+ }
+
if (settingsModified) {
writeJsonFile(WORKSPACE_SETTINGS_FILE, workspaceSettings);
console.log('āœ… Workspace settings updated.');
diff --git a/scripts/telemetry.js b/scripts/telemetry.js
new file mode 100755
index 00000000..9d441072
--- /dev/null
+++ b/scripts/telemetry.js
@@ -0,0 +1,85 @@
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { execSync } from 'child_process';
+import { join } from 'path';
+import { existsSync, readFileSync } from 'fs';
+
+const projectRoot = join(import.meta.dirname, '..');
+
+const SETTINGS_DIRECTORY_NAME = '.gemini';
+const USER_SETTINGS_DIR = join(
+ process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || '',
+ SETTINGS_DIRECTORY_NAME,
+);
+const USER_SETTINGS_PATH = join(USER_SETTINGS_DIR, 'settings.json');
+const WORKSPACE_SETTINGS_PATH = join(
+ projectRoot,
+ SETTINGS_DIRECTORY_NAME,
+ 'settings.json',
+);
+
+let settingsTarget = undefined;
+
+function loadSettingsValue(filePath) {
+ try {
+ if (existsSync(filePath)) {
+ const content = readFileSync(filePath, 'utf-8');
+ const jsonContent = content.replace(/\/\/[^\n]*/g, '');
+ const settings = JSON.parse(jsonContent);
+ return settings.telemetry?.target;
+ }
+ } catch (e) {
+ console.warn(
+ `āš ļø Warning: Could not parse settings file at ${filePath}: ${e.message}`,
+ );
+ }
+ return undefined;
+}
+
+settingsTarget = loadSettingsValue(WORKSPACE_SETTINGS_PATH);
+
+if (!settingsTarget) {
+ settingsTarget = loadSettingsValue(USER_SETTINGS_PATH);
+}
+
+let target = settingsTarget || 'local';
+const allowedTargets = ['local', 'gcp'];
+
+const targetArg = process.argv.find((arg) => arg.startsWith('--target='));
+if (targetArg) {
+ const potentialTarget = targetArg.split('=')[1];
+ if (allowedTargets.includes(potentialTarget)) {
+ target = potentialTarget;
+ console.log(`āš™ļø Using command-line target: ${target}`);
+ } else {
+ console.error(
+ `šŸ›‘ Error: Invalid target '${potentialTarget}'. Allowed targets are: ${allowedTargets.join(', ')}.`,
+ );
+ process.exit(1);
+ }
+} else if (settingsTarget) {
+ console.log(
+ `āš™ļø Using telemetry target from settings.json: ${settingsTarget}`,
+ );
+}
+
+const scriptPath = join(
+ projectRoot,
+ 'scripts',
+ target === 'gcp' ? 'telemetry_gcp.js' : 'local_telemetry.js',
+);
+
+try {
+ console.log(`šŸš€ Running telemetry script for target: ${target}.`);
+ execSync(`node ${scriptPath}`, { stdio: 'inherit', cwd: projectRoot });
+} catch (error) {
+ console.error(`šŸ›‘ Failed to run telemetry script for target: ${target}`);
+ console.error(error);
+ process.exit(1);
+}
diff --git a/scripts/telemetry_gcp.js b/scripts/telemetry_gcp.js
index 6fc3ebe4..c88a9dbc 100755
--- a/scripts/telemetry_gcp.js
+++ b/scripts/telemetry_gcp.js
@@ -70,6 +70,7 @@ async function main() {
const originalSandboxSetting = manageTelemetrySettings(
true,
'http://localhost:4317',
+ 'gcp',
);
registerCleanup(
() => [collectorProcess].filter((p) => p), // Function to get processes
@@ -80,9 +81,12 @@ async function main() {
const projectId = process.env.GOOGLE_CLOUD_PROJECT;
if (!projectId) {
console.error(
- 'šŸ›‘ Error: GOOGLE_CLOUD_PROJECT environment variable is not set.',
+ 'šŸ›‘ Error: GOOGLE_CLOUD_PROJECT environment variable is not exported.',
);
- console.log('Please set it to your Google Cloud Project ID and try again.');
+ console.log(
+ ' Please set it to your Google Cloud Project ID and try again.',
+ );
+ console.log(' `export GOOGLE_CLOUD_PROJECT=your-project-id`');
process.exit(1);
}
console.log(`āœ… Using Google Cloud Project ID: ${projectId}`);
@@ -167,13 +171,13 @@ async function main() {
console.log(`\nšŸ“„ Collector logs are being written to: ${OTEL_LOG_FILE}`);
console.log(`\nšŸ“Š View your telemetry data in Google Cloud Console:`);
console.log(
- ` - Traces: https://console.cloud.google.com/traces/list?project=${projectId}`,
+ ` - Logs: https://console.cloud.google.com/logs/query;query=logName%3D%22projects%2F${projectId}%2Flogs%2Fgemini_cli%22?project=${projectId}`,
);
console.log(
` - Metrics: https://console.cloud.google.com/monitoring/metrics-explorer?project=${projectId}`,
);
console.log(
- ` - Logs: https://console.cloud.google.com/logs/query;query=logName%3D%22projects%2F${projectId}%2Flogs%2Fgemini_cli%22?project=${projectId}`,
+ ` - Traces: https://console.cloud.google.com/traces/list?project=${projectId}`,
);
console.log(`\nPress Ctrl+C to exit.`);
}
diff --git a/scripts/telemetry_utils.js b/scripts/telemetry_utils.js
index 62eb910b..05f607a2 100644
--- a/scripts/telemetry_utils.js
+++ b/scripts/telemetry_utils.js
@@ -10,7 +10,7 @@ import path from 'path';
import fs from 'fs';
import net from 'net';
import os from 'os';
-import { execSync } from 'child_process'; // Removed spawn, it's not used here
+import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
@@ -251,15 +251,20 @@ export async function ensureBinary(
export function manageTelemetrySettings(
enable,
oTelEndpoint = 'http://localhost:4317',
+ target = 'local',
originalSandboxSettingToRestore,
) {
const workspaceSettings = readJsonFile(WORKSPACE_SETTINGS_FILE);
const currentSandboxSetting = workspaceSettings.sandbox;
let settingsModified = false;
+ if (typeof workspaceSettings.telemetry !== 'object') {
+ workspaceSettings.telemetry = {};
+ }
+
if (enable) {
- if (workspaceSettings.telemetry !== true) {
- workspaceSettings.telemetry = true;
+ if (workspaceSettings.telemetry.enabled !== true) {
+ workspaceSettings.telemetry.enabled = true;
settingsModified = true;
console.log('āš™ļø Enabled telemetry in workspace settings.');
}
@@ -268,22 +273,36 @@ export function manageTelemetrySettings(
settingsModified = true;
console.log('āœ… Disabled sandbox mode for telemetry.');
}
- if (workspaceSettings.telemetryOtlpEndpoint !== oTelEndpoint) {
- workspaceSettings.telemetryOtlpEndpoint = oTelEndpoint;
+ if (workspaceSettings.telemetry.otlpEndpoint !== oTelEndpoint) {
+ workspaceSettings.telemetry.otlpEndpoint = oTelEndpoint;
settingsModified = true;
console.log(`šŸ”§ Set telemetry OTLP endpoint to ${oTelEndpoint}.`);
}
+ if (workspaceSettings.telemetry.target !== target) {
+ workspaceSettings.telemetry.target = target;
+ settingsModified = true;
+ console.log(`šŸŽÆ Set telemetry target to ${target}.`);
+ }
} else {
- if (workspaceSettings.telemetry === true) {
- delete workspaceSettings.telemetry;
+ if (workspaceSettings.telemetry.enabled === true) {
+ delete workspaceSettings.telemetry.enabled;
settingsModified = true;
- console.log('āš™ļø Disabled telemetry in workspace settings.');
+ console.log('āš™ļø Disabled telemetry in workspace settings.');
}
- if (workspaceSettings.telemetryOtlpEndpoint) {
- delete workspaceSettings.telemetryOtlpEndpoint;
+ if (workspaceSettings.telemetry.otlpEndpoint) {
+ delete workspaceSettings.telemetry.otlpEndpoint;
settingsModified = true;
console.log('šŸ”§ Cleared telemetry OTLP endpoint.');
}
+ if (workspaceSettings.telemetry.target) {
+ delete workspaceSettings.telemetry.target;
+ settingsModified = true;
+ console.log('šŸŽÆ Cleared telemetry target.');
+ }
+ if (Object.keys(workspaceSettings.telemetry).length === 0) {
+ delete workspaceSettings.telemetry;
+ }
+
if (
originalSandboxSettingToRestore !== undefined &&
workspaceSettings.sandbox !== originalSandboxSettingToRestore