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 `: Overrides `telemetry.target`. + - `--telemetry-otlp-endpoint `: 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=` 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 { @@ -93,7 +97,24 @@ async function parseArguments(): Promise { }) .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(); + 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 { 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}.`); } - } else { - if (workspaceSettings.telemetry === true) { - delete workspaceSettings.telemetry; + if (workspaceSettings.telemetry.target !== target) { + workspaceSettings.telemetry.target = target; settingsModified = true; - console.log('āš™ļø Disabled telemetry in workspace settings.'); + console.log(`šŸŽÆ Set telemetry target to ${target}.`); } - if (workspaceSettings.telemetryOtlpEndpoint) { - delete workspaceSettings.telemetryOtlpEndpoint; + } else { + if (workspaceSettings.telemetry.enabled === true) { + delete workspaceSettings.telemetry.enabled; + settingsModified = true; + console.log('āš™ļø Disabled telemetry in workspace settings.'); + } + 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