From 1452bb4ca4ffe3b5c13aab81baaf510d4c45f06f Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Fri, 13 Jun 2025 20:28:18 -0700 Subject: [PATCH] Add GCP telemetry script (#1033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. ``` --- docs/core/telemetry.md | 243 ++++++------------------- scripts/telemetry_gcp.js | 178 ++++++++++++++++++ scripts/telemetry_utils.js | 362 +++++++++++++++++++++++++++++++++++++ 3 files changed, 595 insertions(+), 188 deletions(-) create mode 100755 scripts/telemetry_gcp.js create mode 100644 scripts/telemetry_utils.js 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 < .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. +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. -**1. Prerequisites** +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). + - 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". -**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 < .gemini/otel/collector-gcp.yaml -receivers: - otlp: - protocols: - grpc: - endpoint: "0.0.0.0:4317" - -processors: - batch: - timeout: 1s - -exporters: - googlecloud: - project: "${GOOGLE_CLOUD_PROJECT}" - metric: - prefix: "custom.googleapis.com/gemini_cli" - log: - default_log_name: "gemini_cli" - debug: - verbosity: detailed - -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. - -3. **Stop the Collector**: - ```bash - docker stop otel-collector-gcp - ``` - -**_Option 2: Use `otelcol-contrib`_** - -Use this method if you prefer not to use Docker. - -1. **Run the Collector**: +2. **Run the Script**: + Execute the script from the root of the repository: ```bash - ./otelcol-contrib --config="file:$(pwd)/.gemini/otel/collector-gcp.yaml" + ./scripts/telemetry_gcp.js ``` -2. **Check Status**: - Your telemetry data will now appear in Google Cloud Trace, Monitoring, and Logging. + The script will: -3. **Stop the Collector**: - Press `Ctrl+C` in the terminal where the collector is running. + - 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. ---- +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 + tail -f .gemini/otel/collector-gcp.log + ``` + +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); + }); +}