From 4421ef126fc6a2de89132aa35c261bf78cd481d2 Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Sun, 15 Jun 2025 15:32:12 -0400 Subject: [PATCH] Refactor: Use telemetry_utils.js in local_telemetry.js (#1066) ## TLDR Refactors `scripts/local_telemetry.js` to use shared helper functions and constants from `scripts/telemetry_utils.js`. ## Dive Deeper This change centralizes common telemetry-related logic, including: - Binary downloading and management (`ensureBinary`) - Reading and writing JSON files - Waiting for network ports - Managing workspace telemetry settings (`manageTelemetrySettings`) - Process cleanup and signal handling (`registerCleanup`) By using the shared utilities, `local_telemetry.js` becomes more concise and focused on its specific task of setting up the local OpenTelemetry and Jaeger environment. ## Docs https://github.com/google-gemini/gemini-cli/blob/main/docs/core/telemetry.md#local-telemetry-with-jaeger-ui-for-traces ## Issue #750 --- scripts/local_telemetry.js | 359 ++++--------------------------------- 1 file changed, 36 insertions(+), 323 deletions(-) diff --git a/scripts/local_telemetry.js b/scripts/local_telemetry.js index 5d8c564f..561ce5f8 100755 --- a/scripts/local_telemetry.js +++ b/scripts/local_telemetry.js @@ -8,23 +8,25 @@ import path from 'path'; import fs from 'fs'; -import net from 'net'; -import os from 'os'; import { spawn, execSync } from 'child_process'; import { fileURLToPath } from 'url'; +import { + BIN_DIR, + OTEL_DIR, + ensureBinary, + fileExists, + manageTelemetrySettings, + registerCleanup, + waitForPort, +} from './telemetry_utils.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const ROOT_DIR = path.resolve(__dirname, '..'); -const GEMINI_DIR = path.join(ROOT_DIR, '.gemini'); -const OTEL_DIR = path.join(GEMINI_DIR, 'otel'); -const BIN_DIR = path.join(OTEL_DIR, 'bin'); const OTEL_CONFIG_FILE = path.join(OTEL_DIR, 'collector-local.yaml'); const OTEL_LOG_FILE = path.join(OTEL_DIR, 'collector.log'); const JAEGER_LOG_FILE = path.join(OTEL_DIR, 'jaeger.log'); const JAEGER_PORT = 16686; -const WORKSPACE_SETTINGS_FILE = path.join(GEMINI_DIR, 'settings.json'); // This configuration is for the primary otelcol-contrib instance. // It receives from the CLI on 4317, exports traces to Jaeger on 14317, @@ -66,227 +68,6 @@ service: exporters: [debug] `; -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); - } - } -} - -function downloadFile(url, dest) { - try { - // Use -sS to hide progress but show errors. - execSync(`curl -fL -sS -o "${dest}" "${url}"`, { - stdio: 'pipe', // Suppress stdout/stderr from the command - }); - return dest; - } catch (e) { - console.error(`Failed to download file from ${url}`); - throw e; - } -} - -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)) { - // Test the simple file name, not the full path. - return filename; - } - } - return null; -} - -async function ensureBinary( - executableName, - repo, - assetNameCallback, - binaryNameInArchive, -) { - 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 (platform === 'windows' && arch === 'arm64') { - if (repo === 'jaegertracing/jaeger') { - console.warn( - `āš ļø Jaeger does not have a release for Windows on ARM64. Skipping.`, - ); - return null; - } - } - - let release; - let asset; - - if (repo === 'jaegertracing/jaeger') { - 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) { - // Jaeger v2 assets are named like 'jaeger-2.7.0-...' but can be in a v1.x release tag. - // We must search for the asset using simple string matching. - const expectedSuffix = `-${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} on platform ${platform}/${arch}.`, - ); - } - - 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 { - downloadFile(downloadUrl, archivePath); - - if (ext === '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.`, - ); - } - - 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); - } - } -} - -function fileExists(filePath) { - return fs.existsSync(filePath); -} - -function readJsonFile(filePath) { - if (!fileExists(filePath)) { - return {}; - } - const content = fs.readFileSync(filePath, 'utf-8'); - return JSON.parse(content); -} - -function writeJsonFile(filePath, data) { - fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); -} - -function waitForPort(port, timeout = 5000) { - 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(); - }); -} - async function main() { // 1. Ensure binaries are available, downloading if necessary. // Binaries are stored in the project's .gemini/otel/bin directory @@ -299,8 +80,9 @@ async function main() { (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}`); + console.error(`��� Error getting otelcol-contrib: ${e.message}`); return null; }); if (!otelcolPath) process.exit(1); @@ -311,6 +93,7 @@ async function main() { (version, platform, arch, ext) => `jaeger-${version}-${platform}-${arch}.${ext}`, 'jaeger', + true, // isJaeger = true ).catch((e) => { console.error(`šŸ›‘ Error getting jaeger: ${e.message}`); return null; @@ -328,13 +111,13 @@ async function main() { console.log('āœ… Stopped existing jaeger process.'); } catch (_e) {} // eslint-disable-line no-empty try { - fs.unlinkSync(OTEL_LOG_FILE); + if (fileExists(OTEL_LOG_FILE)) fs.unlinkSync(OTEL_LOG_FILE); console.log('āœ… Deleted old collector log.'); } catch (e) { if (e.code !== 'ENOENT') console.error(e); } try { - fs.unlinkSync(JAEGER_LOG_FILE); + if (fileExists(JAEGER_LOG_FILE)) fs.unlinkSync(JAEGER_LOG_FILE); console.log('āœ… Deleted old jaeger log.'); } catch (e) { if (e.code !== 'ENOENT') console.error(e); @@ -343,100 +126,25 @@ async function main() { let jaegerProcess, collectorProcess; let jaegerLogFd, collectorLogFd; - const cleanup = () => { - console.log('\nšŸ‘‹ Shutting down...'); + const originalSandboxSetting = manageTelemetrySettings( + true, + 'http://localhost:4317', + 'local', + ); - // Restore original settings - const finalSettings = readJsonFile(WORKSPACE_SETTINGS_FILE); - 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.'); - - [jaegerProcess, collectorProcess].forEach((proc) => { - if (proc && proc.pid) { - const name = path.basename(proc.spawnfile); - try { - console.log(`šŸ›‘ Stopping ${name} (PID: ${proc.pid})...`); - // Use SIGTERM for a graceful shutdown - process.kill(proc.pid, 'SIGTERM'); - console.log(`āœ… ${name} stopped.`); - } catch (e) { - // It's okay if the process is already gone. - if (e.code !== 'ESRCH') - console.error(`Error stopping ${name}: ${e.message}`); - } - } - }); - [jaegerLogFd, collectorLogFd].forEach((fd) => { - if (fd) - try { - fs.closeSync(fd); - } catch (_) {} // eslint-disable-line no-empty - }); - }; - - 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); - process.exit(1); - }); + registerCleanup( + () => [jaegerProcess, collectorProcess], + () => [jaegerLogFd, collectorLogFd], + originalSandboxSetting, + ); if (!fileExists(OTEL_DIR)) fs.mkdirSync(OTEL_DIR, { recursive: true }); fs.writeFileSync(OTEL_CONFIG_FILE, OTEL_CONFIG_CONTENT); console.log('šŸ“„ Wrote OTEL collector config.'); - const workspaceSettings = readJsonFile(WORKSPACE_SETTINGS_FILE); - const originalSandboxSetting = workspaceSettings.sandbox; - let settingsModified = false; - - 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.'); - } - - if (workspaceSettings.sandbox !== false) { - workspaceSettings.sandbox = false; - settingsModified = true; - console.log('āœ… Disabled sandbox mode for local telemetry.'); - } - - 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.'); - } else { - console.log('āœ… Telemetry is already configured correctly.'); - } - // Start Jaeger console.log(`šŸš€ Starting Jaeger service... Logs: ${JAEGER_LOG_FILE}`); jaegerLogFd = fs.openSync(JAEGER_LOG_FILE, 'a'); - // The collector is on 4317, so we move jaeger to 14317. jaegerProcess = spawn( jaegerPath, ['--set=receivers.otlp.protocols.grpc.endpoint=localhost:14317'], @@ -485,17 +193,22 @@ async function main() { } [jaegerProcess, collectorProcess].forEach((proc) => { - proc.on('error', (err) => { - console.error(`${proc.spawnargs[0]} process error:`, err); - process.exit(1); - }); + if (proc) { + proc.on('error', (err) => { + console.error(`${proc.spawnargs[0]} process error:`, err); + process.exit(1); + }); + } }); - console.log(`\n✨ Local telemetry environment is running.`); + console.log(` +✨ Local telemetry environment is running.`); console.log( - `\nšŸ”Ž View traces in the Jaeger UI: http://localhost:${JAEGER_PORT}`, + ` +šŸ”Ž View traces in the Jaeger UI: http://localhost:${JAEGER_PORT}`, ); - console.log(`\nPress Ctrl+C to exit.`); + console.log(` +Press Ctrl+C to exit.`); } main();