summaryrefslogtreecommitdiff
path: root/scripts/telemetry_utils.js
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/telemetry_utils.js
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/telemetry_utils.js')
-rw-r--r--scripts/telemetry_utils.js362
1 files changed, 362 insertions, 0 deletions
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);
+ });
+}