summaryrefslogtreecommitdiff
path: root/scripts
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 /scripts
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. ```
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/telemetry_gcp.js178
-rw-r--r--scripts/telemetry_utils.js362
2 files changed, 540 insertions, 0 deletions
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);
+ });
+}