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. ```
This commit is contained in:
parent
defb0fac2c
commit
1452bb4ca4
|
@ -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.
|
||||
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 <<EOF > .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`)**:
|
||||
2. **Run the Script**:
|
||||
Execute the script from the root of the repository:
|
||||
|
||||
```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
|
||||
./scripts/telemetry_gcp.js
|
||||
```
|
||||
|
||||
- **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
|
||||
```
|
||||
The script will:
|
||||
|
||||
2. **Check Status**:
|
||||
Your telemetry data will now appear in Google Cloud Trace, Monitoring, and Logging.
|
||||
- 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. **Stop the Collector**:
|
||||
```bash
|
||||
docker stop otel-collector-gcp
|
||||
```
|
||||
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.
|
||||
|
||||
**_Option 2: Use `otelcol-contrib`_**
|
||||
|
||||
Use this method if you prefer not to use Docker.
|
||||
|
||||
1. **Run the Collector**:
|
||||
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
|
||||
|
||||
|
|
|
@ -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();
|
|
@ -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);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue