summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJerop Kipruto <[email protected]>2025-06-13 20:28:18 -0700
committerGitHub <[email protected]>2025-06-13 23:28:18 -0400
commit1452bb4ca4ffe3b5c13aab81baaf510d4c45f06f (patch)
treeb61768b4dcfd0ec783798be22877a721487c1e24
parentdefb0fac2cf0bfd86f9696336f12b05493059a27 (diff)
Add GCP telemetry script (#1033)
Adds a script - `scripts/telemetry_gcp.js` - to simplify setting up a local OpenTelemetry collector that forwards data to Google Cloud. This is a follow up to the script for local telemetry `scripts/local_telemetry.js` added in #1015. This script automates downloading necessary binaries, configuring the collector, and updating workspace settings. Also includes `scripts/telemetry_utils.js` with shared helper functions for telemetry scripts. Will refactor `scripts/local_t elemetry.js` in next steps to use this shared functionality. Updates `docs/core/telemetry.md` to include: - A new "Quick Start" section - Detailed instructions for the new GCP automated script - Reorganization of existing sections for clarity #750 --- ``` โœจ Starting Local Telemetry Exporter for Google Cloud โœจ โš™๏ธ Enabled telemetry in workspace settings. ๐Ÿ”ง Set telemetry OTLP endpoint to http://localhost:4317. โœ… 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: 65145)... โœ… OTEL collector started successfully on port 4317. โœจ Local OTEL collector for GCP is running. ๐Ÿ“„ Collector logs are being written to: /Users/jerop/github/gemini-cli/.gemini/otel/collector-gcp.log ๐Ÿ“Š View your telemetry data in Google Cloud Console: - Traces: https://console.cloud.google.com/traces/list?project=foo-bar - Metrics: https://console.cloud.google.com/monitoring/metrics-explorer?project=foo-bar - Logs: https://console.cloud.google.com/logs/query;query=logName%3D%22projects%2Ffoo-bar%2Flogs%2Fgemini_cli%22?project=foo-bar Press Ctrl+C to exit. ^C ๐Ÿ‘‹ Shutting down... โš™๏ธ Disabled telemetry in workspace settings. ๐Ÿ”ง Cleared telemetry OTLP endpoint. โœ… Workspace settings updated. ๐Ÿ›‘ Stopping otelcol-contrib (PID: 65145)... โœ… otelcol-contrib stopped. ```
-rw-r--r--docs/core/telemetry.md235
-rwxr-xr-xscripts/telemetry_gcp.js178
-rw-r--r--scripts/telemetry_utils.js362
3 files changed, 591 insertions, 184 deletions
diff --git a/docs/core/telemetry.md b/docs/core/telemetry.md
index 2252e809..b0a3d4ae 100644
--- a/docs/core/telemetry.md
+++ b/docs/core/telemetry.md
@@ -6,6 +6,32 @@ This entire system is built on the **[OpenTelemetry] (OTEL)** standard, allowing
[OpenTelemetry]: https://opentelemetry.io/
+## Quick Start
+
+### Telemetry with Google Cloud
+
+1. **Ensure Prerequisites:**
+ Ensure that:
+ - You have set 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:
+ ```bash
+ ./scripts/telemetry_gcp.js
+ ```
+3. **View Data:** The script will provide links to view your telemetry data (traces, metrics, logs) in the Google Cloud Console.
+4. **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:
+ ```bash
+ ./scripts/local_telemetry.js
+ ```
+2. **View Logs/Metrics:** Check the `.gemini/otel/collector.log` file for raw logs and metrics.
+3. **View Traces:** Open your browser and go to `http://localhost:16686` to see traces in the Jaeger UI.
+4. **Details:** Refer to documentation for telemetry in [Local](#local).
+
## Enabling Telemetry
You can enable telemetry in multiple ways. [Configuration](configuration.md) is primarily managed via the `.gemini/settings.json` file and environment variables, but CLI flags can override these settings for a specific session.
@@ -50,10 +76,9 @@ Learn more about OTEL exporter standard configuration in [documentation][otel-co
mkdir .gemini/otel
```
-### Local (Automated Script)
+### Local
-For the most straightforward local setup, use the `scripts/local_telemetry.js` script. This script 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 `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:
1. **Run the Script**:
Execute the script from the root of the repository:
@@ -81,202 +106,44 @@ The script installs `otelcol-contrib` (The OpenTelemetry Collector) and `jaeger`
4. **Stop the Services**:
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector and Jaeger services.
-### Local (Manual Setup)
-
-**1. Create a Configuration File**
-
-Create the file `.gemini/otel/collector-local.yaml` with the following:
-
-```bash
-cat <<EOF > .gemini/otel/collector-local.yaml
-receivers:
- otlp:
- protocols:
- grpc:
- endpoint: "0.0.0.0:4317"
-
-processors:
- batch:
- timeout: 1s
-
-exporters:
- debug:
- verbosity: detailed
-
-service:
- telemetry:
- logs:
- level: "debug"
- pipelines:
- traces:
- receivers: [otlp]
- processors: [batch]
- exporters: [debug]
- metrics:
- receivers: [otlp]
- processors: [batch]
- exporters: [debug]
- logs:
- receivers: [otlp]
- processors: [batch]
- exporters: [debug]
-EOF
-```
-
-**2. Run the Collector**
-
-You can run the collector using `docker` or using the `otelcol-contrib` binary directly.
-
-**_Option 1: Use Docker_**
-
-This is the simplest method if you have Docker installed.
-
-1. **Run the Collector**:
-
- ```bash
- docker run --rm --name otel-collector-local \
- -p 4317:4317 \
- -v "$(pwd)/.gemini/otel/collector-local.yaml":/etc/otelcol-contrib/config.yaml \
- otel/opentelemetry-collector-contrib:latest
- ```
-
-2. **Stop the Collector**:
- ```bash
- docker stop otel-collector-local
- ```
-
-**_Option 2: Use `otelcol-contrib`_**
-
-Use this method if you prefer not to use Docker.
-
-1. **Run the Collector**:
- Once installed, run the collector with the configuration file you created earlier:
-
- ```bash
- ./otelcol-contrib --config="$(pwd)/.gemini/otel/collector-local.yaml"
- ```
-
-2. **Stop the Collector**:
- Press `Ctrl+C` in the terminal where the collector is running.
-
### Google Cloud
-This setup sends all telemetry to Google Cloud for robust, long-term analysis.
-
-**1. Prerequisites**
-
-- A Google Cloud Project ID.
-- **APIs Enabled**: Cloud Trace, Cloud Monitoring, Cloud Logging.
-- **Authentication**: A Service Account with the roles `Cloud Trace Agent`, `Monitoring Metric Writer`, and `Logs Writer`. Ensure your environment is authenticated (e.g., via `gcloud auth application-default login` or a service account key file).
-
-**2. Set environment variables**
-
-Set the `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_GENAI_USE_VERTEXAI` environment variables:
-
-```bash
-GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
-GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION" # e.g., us-central1
-GOOGLE_GENAI_USE_VERTEXAI=true
-```
-
-**3. Create a Configuration File**
-
-Create `.gemini/otel/collector-gcp.yaml`:
-
-```bash
-cat <<EOF > .gemini/otel/collector-gcp.yaml
-receivers:
- otlp:
- protocols:
- grpc:
- endpoint: "0.0.0.0:4317"
+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.
-processors:
- batch:
- timeout: 1s
+1. **Prerequisites**:
-exporters:
- googlecloud:
- project: "${GOOGLE_CLOUD_PROJECT}"
- metric:
- prefix: "custom.googleapis.com/gemini_cli"
- log:
- default_log_name: "gemini_cli"
- debug:
- verbosity: detailed
+ - Ensure you have a Google Cloud Project ID.
+ - Set the `GOOGLE_CLOUD_PROJECT` environment variable to your project ID.
+ - 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".
-service:
- pipelines:
- traces:
- receivers: [otlp]
- exporters: [googlecloud]
- metrics:
- receivers: [otlp]
- exporters: [googlecloud]
- logs:
- receivers: [otlp]
- exporters: [googlecloud]
-EOF
-```
-
-**4. Run the Collector**
-
-You can run the collector for Google Cloud using either Docker or a locally installed `otelcol` binary.
-
-**_Option 1: Use Docker _**
-
-This method encapsulates the collector and its dependencies within a container.
-
-1. **Run the Collector**:
- Choose the command that matches your authentication method.
-
- - **If using Application Default Credentials (`gcloud auth application-default login`)**:
-
- ```bash
- docker run --rm --name otel-collector-gcp \
- -p 4317:4317 \
- --user "$(id -u):$(id -g)" \
- -v "$HOME/.config/gcloud/application_default_credentials.json":/etc/gcp/credentials.json:ro \
- -e "GOOGLE_APPLICATION_CREDENTIALS=/etc/gcp/credentials.json" \
- -v "$(pwd)/.gemini/otel/collector-gcp.yaml":/etc/otelcol-contrib/config.yaml \
- otel/opentelemetry-collector-contrib:latest --config /etc/otelcol-contrib/config.yaml
- ```
-
- - **If using a Service Account Key File**:
- ```bash
- docker run --rm --name otel-collector-gcp \
- -p 4317:4317 \
- -v "/path/to/your/sa-key.json":/etc/gcp/sa-key.json:ro \
- -e "GOOGLE_APPLICATION_CREDENTIALS=/etc/gcp/sa-key.json" \
- -v "$(pwd)/.gemini/otel/collector-gcp.yaml":/etc/otelcol-contrib/config.yaml \
- otel/opentelemetry-collector-contrib:latest --config /etc/otelcol-contrib/config.yaml
- ```
-
-2. **Check Status**:
- Your telemetry data will now appear in Google Cloud Trace, Monitoring, and Logging.
+2. **Run the Script**:
+ Execute the script from the root of the repository:
-3. **Stop the Collector**:
```bash
- docker stop otel-collector-gcp
+ ./scripts/telemetry_gcp.js
```
-**_Option 2: Use `otelcol-contrib`_**
+ The script will:
-Use this method if you prefer not to use Docker.
+ - Download the `otelcol-contrib` binary if needed.
+ - Start an OTEL collector configured to receive data from the Gemini CLI and export it to your specified Google Cloud project.
+ - Automatically enable telemetry and disable sandbox mode in your workspace settings (`.gemini/settings.json`).
+ - Provide direct links to view traces, metrics, and logs in your Google Cloud Console.
+ - On exit (Ctrl+C), it will attempt to restore your original telemetry and sandbox settings.
-1. **Run the Collector**:
+3. **View Telemetry in Google Cloud**:
+ Use the links provided by the script to navigate to the Google Cloud Console and view your traces, metrics, and logs.
+
+4. **Inspect Local Collector Logs**:
+ The script redirects the local OTEL collector's output to `.gemini/otel/collector-gcp.log`. You can monitor this file for detailed information or troubleshooting:
```bash
- ./otelcol-contrib --config="file:$(pwd)/.gemini/otel/collector-gcp.yaml"
+ tail -f .gemini/otel/collector-gcp.log
```
-2. **Check Status**:
- Your telemetry data will now appear in Google Cloud Trace, Monitoring, and Logging.
-
-3. **Stop the Collector**:
- Press `Ctrl+C` in the terminal where the collector is running.
-
----
+5. **Stop the Service**:
+ Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector.
## Data Reference: Logs & Metrics
diff --git a/scripts/telemetry_gcp.js b/scripts/telemetry_gcp.js
new file mode 100755
index 00000000..a842625e
--- /dev/null
+++ b/scripts/telemetry_gcp.js
@@ -0,0 +1,178 @@
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+import fs from 'fs';
+import { spawn, execSync } from 'child_process';
+import {
+ OTEL_DIR,
+ BIN_DIR,
+ fileExists,
+ waitForPort,
+ ensureBinary,
+ manageTelemetrySettings,
+ registerCleanup,
+} from './telemetry_utils.js';
+
+const OTEL_CONFIG_FILE = path.join(OTEL_DIR, 'collector-gcp.yaml');
+const OTEL_LOG_FILE = path.join(OTEL_DIR, 'collector-gcp.log');
+
+const getOtelConfigContent = (projectId) => `
+receivers:
+ otlp:
+ protocols:
+ grpc:
+ endpoint: "localhost:4317"
+processors:
+ batch:
+ timeout: 1s
+exporters:
+ googlecloud:
+ project: "${projectId}"
+ metric:
+ prefix: "custom.googleapis.com/gemini_cli"
+ log:
+ default_log_name: "gemini_cli"
+ debug:
+ verbosity: detailed
+service:
+ telemetry:
+ logs:
+ level: "debug"
+ metrics:
+ level: "none"
+ pipelines:
+ traces:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [googlecloud]
+ metrics:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [googlecloud, debug]
+ logs:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [googlecloud, debug]
+`;
+
+async function main() {
+ console.log('โœจ Starting Local Telemetry Exporter for Google Cloud โœจ');
+
+ let collectorProcess;
+ let collectorLogFd;
+
+ const originalSandboxSetting = manageTelemetrySettings(
+ true,
+ 'http://localhost:4317',
+ );
+ registerCleanup(
+ () => [collectorProcess].filter((p) => p), // Function to get processes
+ () => [collectorLogFd].filter((fd) => fd), // Function to get FDs
+ originalSandboxSetting,
+ );
+
+ const projectId = process.env.GOOGLE_CLOUD_PROJECT;
+ if (!projectId) {
+ console.error(
+ '๐Ÿ›‘ Error: GOOGLE_CLOUD_PROJECT environment variable is not set.',
+ );
+ console.log('Please set it to your Google Cloud Project ID and try again.');
+ process.exit(1);
+ }
+ console.log(`โœ… Using Google Cloud Project ID: ${projectId}`);
+
+ console.log('\n๐Ÿ”‘ Please ensure you are authenticated with Google Cloud:');
+ console.log(
+ ' - Run `gcloud auth application-default login` OR ensure `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key.',
+ );
+ console.log(
+ ' - The account needs "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer" roles.',
+ );
+
+ if (!fileExists(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
+
+ const otelcolPath = await ensureBinary(
+ 'otelcol-contrib',
+ 'open-telemetry/opentelemetry-collector-releases',
+ (version, platform, arch, ext) =>
+ `otelcol-contrib_${version}_${platform}_${arch}.${ext}`,
+ 'otelcol-contrib',
+ false, // isJaeger = false
+ ).catch((e) => {
+ console.error(`๐Ÿ›‘ Error getting otelcol-contrib: ${e.message}`);
+ return null;
+ });
+ if (!otelcolPath) process.exit(1);
+
+ console.log('๐Ÿงน Cleaning up old processes and logs...');
+ try {
+ execSync('pkill -f "otelcol-contrib"');
+ console.log('โœ… Stopped existing otelcol-contrib process.');
+ } catch (_e) {
+ /* no-op */
+ }
+ try {
+ fs.unlinkSync(OTEL_LOG_FILE);
+ console.log('โœ… Deleted old GCP collector log.');
+ } catch (e) {
+ if (e.code !== 'ENOENT') console.error(e);
+ }
+
+ if (!fileExists(OTEL_DIR)) fs.mkdirSync(OTEL_DIR, { recursive: true });
+ fs.writeFileSync(OTEL_CONFIG_FILE, getOtelConfigContent(projectId));
+ console.log(`๐Ÿ“„ Wrote OTEL collector config to ${OTEL_CONFIG_FILE}`);
+
+ console.log(`๐Ÿš€ Starting OTEL collector for GCP... Logs: ${OTEL_LOG_FILE}`);
+ collectorLogFd = fs.openSync(OTEL_LOG_FILE, 'a');
+ collectorProcess = spawn(otelcolPath, ['--config', OTEL_CONFIG_FILE], {
+ stdio: ['ignore', collectorLogFd, collectorLogFd],
+ env: { ...process.env },
+ });
+
+ console.log(
+ `โณ Waiting for OTEL collector to start (PID: ${collectorProcess.pid})...`,
+ );
+
+ try {
+ await waitForPort(4317);
+ console.log(`โœ… OTEL collector started successfully on port 4317.`);
+ } catch (err) {
+ console.error(`๐Ÿ›‘ Error: OTEL collector failed to start on port 4317.`);
+ console.error(err.message);
+ if (collectorProcess && collectorProcess.pid) {
+ process.kill(collectorProcess.pid, 'SIGKILL');
+ }
+ if (fileExists(OTEL_LOG_FILE)) {
+ console.error('๐Ÿ“„ OTEL Collector Log Output:');
+ console.error(fs.readFileSync(OTEL_LOG_FILE, 'utf-8'));
+ }
+ process.exit(1);
+ }
+
+ collectorProcess.on('error', (err) => {
+ console.error(`${collectorProcess.spawnargs[0]} process error:`, err);
+ process.exit(1);
+ });
+
+ console.log(`\nโœจ Local OTEL collector for GCP is running.`);
+ 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}`,
+ );
+ 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}`,
+ );
+ console.log(`\nPress Ctrl+C to exit.`);
+}
+
+main();
diff --git a/scripts/telemetry_utils.js b/scripts/telemetry_utils.js
new file mode 100644
index 00000000..62eb910b
--- /dev/null
+++ b/scripts/telemetry_utils.js
@@ -0,0 +1,362 @@
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+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 { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export const ROOT_DIR = path.resolve(__dirname, '..');
+export const GEMINI_DIR = path.join(ROOT_DIR, '.gemini');
+export const OTEL_DIR = path.join(GEMINI_DIR, 'otel');
+export const BIN_DIR = path.join(OTEL_DIR, 'bin');
+export const WORKSPACE_SETTINGS_FILE = path.join(GEMINI_DIR, 'settings.json');
+
+export function getJson(url) {
+ const tmpFile = path.join(
+ os.tmpdir(),
+ `gemini-cli-releases-${Date.now()}.json`,
+ );
+ try {
+ execSync(
+ `curl -sL -H "User-Agent: gemini-cli-dev-script" -o "${tmpFile}" "${url}"`,
+ { stdio: 'pipe' },
+ );
+ const content = fs.readFileSync(tmpFile, 'utf-8');
+ return JSON.parse(content);
+ } catch (e) {
+ console.error(`Failed to fetch or parse JSON from ${url}`);
+ throw e;
+ } finally {
+ if (fs.existsSync(tmpFile)) {
+ fs.unlinkSync(tmpFile);
+ }
+ }
+}
+
+export function downloadFile(url, dest) {
+ try {
+ execSync(`curl -fL -sS -o "${dest}" "${url}"`, {
+ stdio: 'pipe',
+ });
+ return dest;
+ } catch (e) {
+ console.error(`Failed to download file from ${url}`);
+ throw e;
+ }
+}
+
+export function findFile(startPath, filter) {
+ if (!fs.existsSync(startPath)) {
+ return null;
+ }
+ const files = fs.readdirSync(startPath);
+ for (const file of files) {
+ const filename = path.join(startPath, file);
+ const stat = fs.lstatSync(filename);
+ if (stat.isDirectory()) {
+ const result = findFile(filename, filter);
+ if (result) return result;
+ } else if (filter(file)) {
+ return filename;
+ }
+ }
+ return null;
+}
+
+export function fileExists(filePath) {
+ return fs.existsSync(filePath);
+}
+
+export function readJsonFile(filePath) {
+ if (!fileExists(filePath)) {
+ return {};
+ }
+ const content = fs.readFileSync(filePath, 'utf-8');
+ try {
+ return JSON.parse(content);
+ } catch (e) {
+ console.error(`Error parsing JSON from ${filePath}: ${e.message}`);
+ return {};
+ }
+}
+
+export function writeJsonFile(filePath, data) {
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
+}
+
+export function waitForPort(port, timeout = 10000) {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now();
+ const tryConnect = () => {
+ const socket = new net.Socket();
+ socket.once('connect', () => {
+ socket.end();
+ resolve();
+ });
+ socket.once('error', (_) => {
+ if (Date.now() - startTime > timeout) {
+ reject(new Error(`Timeout waiting for port ${port} to open.`));
+ } else {
+ setTimeout(tryConnect, 500);
+ }
+ });
+ socket.connect(port, 'localhost');
+ };
+ tryConnect();
+ });
+}
+
+export async function ensureBinary(
+ executableName,
+ repo,
+ assetNameCallback,
+ binaryNameInArchive,
+ isJaeger = false,
+) {
+ const executablePath = path.join(BIN_DIR, executableName);
+ if (fileExists(executablePath)) {
+ console.log(`โœ… ${executableName} already exists at ${executablePath}`);
+ return executablePath;
+ }
+
+ console.log(`๐Ÿ” ${executableName} not found. Downloading from ${repo}...`);
+
+ const platform = process.platform === 'win32' ? 'windows' : process.platform;
+ const arch = process.arch === 'x64' ? 'amd64' : process.arch;
+ const ext = platform === 'windows' ? 'zip' : 'tar.gz';
+
+ if (isJaeger && platform === 'windows' && arch === 'arm64') {
+ console.warn(
+ `โš ๏ธ Jaeger does not have a release for Windows on ARM64. Skipping.`,
+ );
+ return null;
+ }
+
+ let release;
+ let asset;
+
+ if (isJaeger) {
+ console.log(`๐Ÿ” Finding latest Jaeger v2+ asset...`);
+ const releases = getJson(`https://api.github.com/repos/${repo}/releases`);
+ const sortedReleases = releases
+ .filter((r) => !r.prerelease && r.tag_name.startsWith('v'))
+ .sort((a, b) => {
+ const aVersion = a.tag_name.substring(1).split('.').map(Number);
+ const bVersion = b.tag_name.substring(1).split('.').map(Number);
+ for (let i = 0; i < Math.max(aVersion.length, bVersion.length); i++) {
+ if ((aVersion[i] || 0) > (bVersion[i] || 0)) return -1;
+ if ((aVersion[i] || 0) < (bVersion[i] || 0)) return 1;
+ }
+ return 0;
+ });
+
+ for (const r of sortedReleases) {
+ const expectedSuffix =
+ platform === 'windows'
+ ? `-${platform}-${arch}.zip`
+ : `-${platform}-${arch}.tar.gz`;
+ const foundAsset = r.assets.find(
+ (a) =>
+ a.name.startsWith('jaeger-2.') && a.name.endsWith(expectedSuffix),
+ );
+
+ if (foundAsset) {
+ release = r;
+ asset = foundAsset;
+ console.log(
+ `โฌ‡๏ธ Found ${asset.name} in release ${r.tag_name}, downloading...`,
+ );
+ break;
+ }
+ }
+ if (!asset) {
+ throw new Error(
+ `Could not find a suitable Jaeger v2 asset for platform ${platform}/${arch}.`,
+ );
+ }
+ } else {
+ release = getJson(`https://api.github.com/repos/${repo}/releases/latest`);
+ const version = release.tag_name.startsWith('v')
+ ? release.tag_name.substring(1)
+ : release.tag_name;
+ const assetName = assetNameCallback(version, platform, arch, ext);
+ asset = release.assets.find((a) => a.name === assetName);
+ if (!asset) {
+ throw new Error(
+ `Could not find a suitable asset for ${repo} (version ${version}) on platform ${platform}/${arch}. Searched for: ${assetName}`,
+ );
+ }
+ }
+
+ const downloadUrl = asset.browser_download_url;
+ const tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'gemini-cli-telemetry-'),
+ );
+ const archivePath = path.join(tmpDir, asset.name);
+
+ try {
+ console.log(`โฌ‡๏ธ Downloading ${asset.name}...`);
+ downloadFile(downloadUrl, archivePath);
+ console.log(`๐Ÿ“ฆ Extracting ${asset.name}...`);
+
+ const actualExt = asset.name.endsWith('.zip') ? 'zip' : 'tar.gz';
+
+ if (actualExt === 'zip') {
+ execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`, { stdio: 'pipe' });
+ } else {
+ execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { stdio: 'pipe' });
+ }
+
+ const nameToFind = binaryNameInArchive || executableName;
+ const foundBinaryPath = findFile(tmpDir, (file) => {
+ if (platform === 'windows') {
+ return file === `${nameToFind}.exe`;
+ }
+ return file === nameToFind;
+ });
+
+ if (!foundBinaryPath) {
+ throw new Error(
+ `Could not find binary "${nameToFind}" in extracted archive at ${tmpDir}. Contents: ${fs.readdirSync(tmpDir).join(', ')}`,
+ );
+ }
+
+ fs.renameSync(foundBinaryPath, executablePath);
+
+ if (platform !== 'windows') {
+ fs.chmodSync(executablePath, '755');
+ }
+
+ console.log(`โœ… ${executableName} installed at ${executablePath}`);
+ return executablePath;
+ } finally {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ if (fs.existsSync(archivePath)) {
+ fs.unlinkSync(archivePath);
+ }
+ }
+}
+
+export function manageTelemetrySettings(
+ enable,
+ oTelEndpoint = 'http://localhost:4317',
+ originalSandboxSettingToRestore,
+) {
+ const workspaceSettings = readJsonFile(WORKSPACE_SETTINGS_FILE);
+ const currentSandboxSetting = workspaceSettings.sandbox;
+ let settingsModified = false;
+
+ if (enable) {
+ if (workspaceSettings.telemetry !== true) {
+ workspaceSettings.telemetry = true;
+ settingsModified = true;
+ console.log('โš™๏ธ Enabled telemetry in workspace settings.');
+ }
+ if (workspaceSettings.sandbox !== false) {
+ workspaceSettings.sandbox = false;
+ settingsModified = true;
+ console.log('โœ… Disabled sandbox mode for telemetry.');
+ }
+ if (workspaceSettings.telemetryOtlpEndpoint !== oTelEndpoint) {
+ workspaceSettings.telemetryOtlpEndpoint = oTelEndpoint;
+ settingsModified = true;
+ console.log(`๐Ÿ”ง Set telemetry OTLP endpoint to ${oTelEndpoint}.`);
+ }
+ } else {
+ if (workspaceSettings.telemetry === true) {
+ delete workspaceSettings.telemetry;
+ settingsModified = true;
+ console.log('โš™๏ธ Disabled telemetry in workspace settings.');
+ }
+ if (workspaceSettings.telemetryOtlpEndpoint) {
+ delete workspaceSettings.telemetryOtlpEndpoint;
+ settingsModified = true;
+ console.log('๐Ÿ”ง Cleared telemetry OTLP endpoint.');
+ }
+ if (
+ originalSandboxSettingToRestore !== undefined &&
+ workspaceSettings.sandbox !== originalSandboxSettingToRestore
+ ) {
+ workspaceSettings.sandbox = originalSandboxSettingToRestore;
+ settingsModified = true;
+ console.log('โœ… Restored original sandbox setting.');
+ }
+ }
+
+ if (settingsModified) {
+ writeJsonFile(WORKSPACE_SETTINGS_FILE, workspaceSettings);
+ console.log('โœ… Workspace settings updated.');
+ } else {
+ console.log(
+ enable
+ ? 'โœ… Workspace settings are already configured for telemetry.'
+ : 'โœ… Workspace settings already reflect telemetry disabled.',
+ );
+ }
+ return currentSandboxSetting;
+}
+
+export function registerCleanup(
+ getProcesses,
+ getLogFileDescriptors,
+ originalSandboxSetting,
+) {
+ let cleanedUp = false;
+ const cleanup = () => {
+ if (cleanedUp) return;
+ cleanedUp = true;
+
+ console.log('\n๐Ÿ‘‹ Shutting down...');
+
+ manageTelemetrySettings(false, null, originalSandboxSetting);
+
+ const processes = getProcesses ? getProcesses() : [];
+ processes.forEach((proc) => {
+ if (proc && proc.pid) {
+ const name = path.basename(proc.spawnfile);
+ try {
+ console.log(`๐Ÿ›‘ Stopping ${name} (PID: ${proc.pid})...`);
+ process.kill(proc.pid, 'SIGTERM');
+ console.log(`โœ… ${name} stopped.`);
+ } catch (e) {
+ if (e.code !== 'ESRCH') {
+ console.error(`Error stopping ${name}: ${e.message}`);
+ }
+ }
+ }
+ });
+
+ const logFileDescriptors = getLogFileDescriptors
+ ? getLogFileDescriptors()
+ : [];
+ logFileDescriptors.forEach((fd) => {
+ if (fd) {
+ try {
+ fs.closeSync(fd);
+ } catch (_) {
+ /* no-op */
+ }
+ }
+ });
+ };
+
+ process.on('exit', cleanup);
+ process.on('SIGINT', () => process.exit(0));
+ process.on('SIGTERM', () => process.exit(0));
+ process.on('uncaughtException', (err) => {
+ console.error('Uncaught Exception:', err);
+ cleanup();
+ process.exit(1);
+ });
+}