Add telemetry command and refactor telemetry settings (#1060)

#750 

### Telemetry Settings
Refactors telemetry configuration to use a nested `telemetry` object in `settings.json`, for example:

```json
{
  "telemetry": {
    "enabled": true,
    "target": "gcp"
    "log-prompts": "true"
  },
  "sandbox": false
}
```

The above includes
- Centralized telemetry settings under a `telemetry` object in `settings.json`.
- CLI flags for the `gemini` command to override all telemetry sub-settings:
    - `--telemetry` / `--no-telemetry`
    - `--telemetry-target <local|gcp>`
    - `--telemetry-otlp-endpoint <URL>`
    - `--telemetry-log-prompts` / `--no-telemetry-log-prompts`
- Updates `packages/cli/src/config/config.ts` and `packages/core/src/config/config.ts` to read from the new settings structure and respect the new CLI flags.
- Modifies `scripts/handle-telemetry.js`, `scripts/local_telemetry.js`, and `scripts/telemetry_utils.js` to align with the new settings structure.
- Updates `docs/core/telemetry.md` to reflect the new settings structure, CLI flags, and order of precedence.
- Renames `logUserPromptsEnabled` to `logPrompts` for brevity.

### `npm run telemetry`

Add a new `npm run telemetry` command that uses `scripts/telemetry.js`, automates the entire process of setting up a local and GCP telemetry pipelines, including configuring the necessary settings in the `.gemini/settings.json` workspace file and installing required binaries (e.g. `otelcol-contrib`).

---
```shell
$ npm run telemetry -- --target=gcp

> gemini-cli@0.1.0 telemetry
> node scripts/telemetry.js --target=gcp

⚙️  Using command-line target: gcp
🚀 Running telemetry script for target: gcp.
 Starting Local Telemetry Exporter for Google Cloud 
⚙️  Enabled telemetry in workspace settings.
🔧 Set telemetry OTLP endpoint to http://localhost:4317.
🎯 Set telemetry target to gcp.
 Workspace settings updated.
 Using Google Cloud Project ID: foo-bar

🔑 Please ensure you are authenticated with Google Cloud:
  - Run `gcloud auth application-default login` OR ensure `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key.
  - The account needs "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer" roles.
 otelcol-contrib already exists at /Users/jerop/github/gemini-cli/.gemini/otel/bin/otelcol-contrib
🧹 Cleaning up old processes and logs...
 Deleted old GCP collector log.
📄 Wrote OTEL collector config to /Users/jerop/github/gemini-cli/.gemini/otel/collector-gcp.yaml
🚀 Starting OTEL collector for GCP... Logs: /Users/jerop/github/gemini-cli/.gemini/otel/collector-gcp.log
 Waiting for OTEL collector to start (PID: 17013)...
 OTEL collector started successfully on port 4317.

 Local OTEL collector for GCP is running.

🚀 To send telemetry, run the Gemini CLI in a separate terminal window.

📄 Collector logs are being written to: /Users/jerop/github/gemini-cli/.gemini/otel/collector-gcp.log

📊 View your telemetry data in Google Cloud Console:
   - Logs: https://console.cloud.google.com/logs/query;query=logName%3D%22projects%2Ffoo-bar%2Flogs%2Fgemini_cli%22?project=foo-bar
   - Metrics: https://console.cloud.google.com/monitoring/metrics-explorer?project=foo-bar
   - Traces: https://console.cloud.google.com/traces/list?project=foo-bar

Press Ctrl+C to exit.
^C
👋 Shutting down...
⚙️  Disabled telemetry in workspace settings.
🔧 Cleared telemetry OTLP endpoint.
🎯 Cleared telemetry target.
 Workspace settings updated.
🛑 Stopping otelcol-contrib (PID: 17013)...
 otelcol-contrib stopped.
```
This commit is contained in:
Jerop Kipruto 2025-06-15 00:47:32 -04:00 committed by GitHub
parent 32dd298351
commit 53753f0455
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 448 additions and 87 deletions

View File

@ -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.

View File

@ -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"

View File

@ -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', () => {

View File

@ -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,
});

View File

@ -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;

View File

@ -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);
});
});
});

View File

@ -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 {

View File

@ -26,7 +26,7 @@ const mockModelsModule = {
const mockConfig = {
getSessionId: () => 'test-session-id',
getTelemetryLogUserPromptsEnabled: () => true,
getTelemetryLogPromptsEnabled: () => true,
} as unknown as Config;
describe('GeminiChat', () => {

View File

@ -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, {

View File

@ -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,

View File

@ -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',

View File

@ -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(),

View File

@ -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.');

85
scripts/telemetry.js Executable file
View File

@ -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);
}

View File

@ -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.`);
}

View File

@ -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