OpenTelemetry Integration & Telemetry Control Flag (#762)

This commit is contained in:
Jerop Kipruto 2025-06-05 16:04:25 -04:00 committed by GitHub
parent d3e43437a0
commit 2ebf2fbc82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1992 additions and 31 deletions

285
docs/core/telemetry.md Normal file
View File

@ -0,0 +1,285 @@
# Gemini CLI Observability Guide
Telemetry provides crucial data about the Gemini CLI's performance, health, and usage. By enabling it, you can monitor operations, debug issues, and optimize tool usage through traces, metrics, and structured logs.
This entire system is built on the **[OpenTelemetry] (OTEL)** standard, allowing you to send data to any compatible backend, from your local terminal to a cloud service.
[OpenTelemetry]: https://opentelemetry.io/
## Quick Start: 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.
**Order of Precedence:**
1. **CLI Flag (`--telemetry`):** These override all other settings for the current session.
2. **Workspace Settings File (`.gemini/settings.json`):** If no CLI flag is used, the `telemetry` value from this project-specific file is used.
3. **User Settings File (`~/.gemini/settings.json`):** If not set by a flag or workspace settings, the value from this global user file is used.
4. **Default:** If telemetry is not configured by a flag or in any settings file, it is disabled.
Add this line to enable telemetry by in workspace (`.gemini/settings.json`) or user (`~/.gemini/settings.json`) settings:
```json
{
"telemetry": true
}
```
#### Mode 1: Console Output (Default)
If you only set `"telemetry": true` and do nothing else, the CLI will output all telemetry data directly to your console. This is the simplest way to inspect events, metrics, and traces without any external tools.
#### Mode 2: Sending to a Collector
To send data to a local or remote OpenTelemetry collector, set the following environment variable:
```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
```
The CLI sends data using the OTLP/gRPC protocol.
Learn more about OTEL exporter standard configuration in [documentation][otel-config-docs].
[otel-config-docs]: https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
## Running an OTEL Collector
An OTEL Collector is a service that receives, processes, and exports telemetry data. Below are common setups.
### Local
This setup prints all telemetry from the Gemini CLI to your terminal using a local Docker container.
**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]
exporters: [debug]
metrics:
receivers: [otlp]
exporters: [debug]
logs:
receivers: [otlp]
exporters: [debug]
EOF
```
**2. Run the Collector**
In your terminal, run this Docker command:
```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
```
**3. Stop the Collector**
```bash
docker stop otel-collector-local
```
### Google Cloud
This setup sends all telemetry to Google Cloud for robust, long-term analysis.
**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).
**2. 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}"
trace:
metric:
prefix: "custom.googleapis.com/gemini_code"
log:
default_log_name: "gemini_code"
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
exporters: [googlecloud]
metrics:
receivers: [otlp]
exporters: [googlecloud]
logs:
receivers: [otlp]
exporters: [googlecloud]
EOF
```
**3. Run the Collector**
This command mounts your Google Cloud credentials into the container.
If using application default credentials:
```bash
docker run --rm --name otel-collector-gcp \
-p 4317:4317 \
-v "/home/user/.config/gcloud/application_default_credentials.json":/etc/gcp/credentials.json \
-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 sevice account key:
```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
```
Your telemetry data will now appear in Cloud Trace, Monitoring, and Logging.
**3. Stop the Collector**
```bash
docker stop otel-collector-gcp
```
---
## Data Reference: Logs & Metrics
### Logs
These are timestamped records of specific events.
- `gemini_code.config`: Fired once at startup with the CLI's configuration.
- **Attributes**:
- `model` (string)
- `sandbox_enabled` (boolean)
- `core_tools_enabled` (string)
- `approval_mode` (string)
- `vertex_ai_enabled` (boolean)
- `log_user_prompts_enabled` (boolean)
- `file_filtering_respect_git_ignore` (boolean)
- `file_filtering_allow_build_artifacts` (boolean)
- `gemini_code.user_prompt`: Fired when a user submits a prompt.
- **Attributes**:
- `prompt_char_count`
- `prompt` (except if `log_user_prompts_enabled` is false)
- `gemini_code.tool_call`: Fired for every function call.
- **Attributes**:
- `function_name`
- `function_args`
- `duration_ms`
- `success` (boolean)
- `error` (optional)
- `error_type` (optional)
- `gemini_code.api_request`: Fired when making a request to the Gemini API.
- **Attributes**:
- `model`
- `duration_ms`
- `prompt_token_count`
- `gemini_code.api_error`: Fired if the API request fails.
- **Attributes**:
- `model`
- `error`
- `error_type`
- `status_code`
- `duration_ms`
- `attempt`
- `gemini_code.api_response`: Fired upon receiving a response from the Gemini API.
- **Attributes**:
- `model`
- `status_code`
- `duration_ms`
- `error` (optional)
- `attempt`
### Metrics
These are numerical measurements of behavior over time.
- `gemini_code.session.count` (Counter, Int): Incremented once per CLI startup.
- `gemini_code.tool.call.count` (Counter, Int): Counts tool calls.
- **Attributes**:
- `function_name`
- `success` (boolean)
- `gemini_code.tool.call.latency` (Histogram, ms): Measures tool call latency.
- **Attributes**:
- `function_name`
- `gemini_code.api.request.count` (Counter, Int): Counts all API requests.
- **Attributes**:
- `model`
- `status_code`
- `error_type` (optional)
- `gemini_code.api.request.latency` (Histogram, ms): Measures API request latency.
- **Attributes**:
- `model`
- `gemini_code.token.input.count` (Counter, Int): Counts the total number of input tokens sent to the API.
- **Attributes**:
- `model`

680
package-lock.json generated
View File

@ -918,6 +918,37 @@
}
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/@grpc/proto-loader": {
"version": "0.7.15",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.2.5",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1065,6 +1096,16 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@jsonjoy.com/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
@ -1179,6 +1220,472 @@
"node": ">= 8"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/api-logs": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz",
"integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@opentelemetry/context-async-hooks": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz",
"integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/core": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
"integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/exporter-logs-otlp-grpc": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.52.1.tgz",
"integrity": "sha512-sXgcp4fsL3zCo96A0LmFIGYOj2LSEDI6wD7nBYRhuDDxeRsk18NQgqRVlCf4VIyTBZzGu1M7yOtdFukQPgII1A==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.7.1",
"@opentelemetry/core": "1.25.1",
"@opentelemetry/otlp-grpc-exporter-base": "0.52.1",
"@opentelemetry/otlp-transformer": "0.52.1",
"@opentelemetry/sdk-logs": "0.52.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0"
}
},
"node_modules/@opentelemetry/exporter-metrics-otlp-grpc": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.52.1.tgz",
"integrity": "sha512-CE0f1IEE1GQj8JWl/BxKvKwx9wBTLR09OpPQHaIs5LGBw3ODu8ek5kcbrHPNsFYh/pWh+pcjbZQoxq3CqvQVnA==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.7.1",
"@opentelemetry/core": "1.25.1",
"@opentelemetry/exporter-metrics-otlp-http": "0.52.1",
"@opentelemetry/otlp-exporter-base": "0.52.1",
"@opentelemetry/otlp-grpc-exporter-base": "0.52.1",
"@opentelemetry/otlp-transformer": "0.52.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-metrics": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/exporter-metrics-otlp-http": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.52.1.tgz",
"integrity": "sha512-oAHPOy1sZi58bwqXaucd19F/v7+qE2EuVslQOEeLQT94CDuZJJ4tbWzx8DpYBTrOSzKqqrMtx9+PMxkrcbxOyQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/otlp-exporter-base": "0.52.1",
"@opentelemetry/otlp-transformer": "0.52.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-metrics": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/exporter-trace-otlp-grpc": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz",
"integrity": "sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.7.1",
"@opentelemetry/core": "1.25.1",
"@opentelemetry/otlp-grpc-exporter-base": "0.52.1",
"@opentelemetry/otlp-transformer": "0.52.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0"
}
},
"node_modules/@opentelemetry/exporter-trace-otlp-http": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.52.1.tgz",
"integrity": "sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/otlp-exporter-base": "0.52.1",
"@opentelemetry/otlp-transformer": "0.52.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0"
}
},
"node_modules/@opentelemetry/exporter-trace-otlp-proto": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.52.1.tgz",
"integrity": "sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/otlp-exporter-base": "0.52.1",
"@opentelemetry/otlp-transformer": "0.52.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0"
}
},
"node_modules/@opentelemetry/exporter-zipkin": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.1.tgz",
"integrity": "sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0"
}
},
"node_modules/@opentelemetry/instrumentation": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz",
"integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.52.1",
"@types/shimmer": "^1.0.2",
"import-in-the-middle": "^1.8.1",
"require-in-the-middle": "^7.1.1",
"semver": "^7.5.2",
"shimmer": "^1.2.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/instrumentation-http": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.1.tgz",
"integrity": "sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/instrumentation": "0.52.1",
"@opentelemetry/semantic-conventions": "1.25.1",
"semver": "^7.5.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/instrumentation-http/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@opentelemetry/instrumentation/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@opentelemetry/otlp-exporter-base": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.52.1.tgz",
"integrity": "sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/otlp-transformer": "0.52.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0"
}
},
"node_modules/@opentelemetry/otlp-grpc-exporter-base": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz",
"integrity": "sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.7.1",
"@opentelemetry/core": "1.25.1",
"@opentelemetry/otlp-exporter-base": "0.52.1",
"@opentelemetry/otlp-transformer": "0.52.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0"
}
},
"node_modules/@opentelemetry/otlp-transformer": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.52.1.tgz",
"integrity": "sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.52.1",
"@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-logs": "0.52.1",
"@opentelemetry/sdk-metrics": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1",
"protobufjs": "^7.3.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/propagator-b3": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.25.1.tgz",
"integrity": "sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/propagator-jaeger": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.25.1.tgz",
"integrity": "sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/resources": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz",
"integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.52.1.tgz",
"integrity": "sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.52.1",
"@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.4.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-metrics": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
"integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1",
"lodash.merge": "^4.6.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-node": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz",
"integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.52.1",
"@opentelemetry/core": "1.25.1",
"@opentelemetry/exporter-trace-otlp-grpc": "0.52.1",
"@opentelemetry/exporter-trace-otlp-http": "0.52.1",
"@opentelemetry/exporter-trace-otlp-proto": "0.52.1",
"@opentelemetry/exporter-zipkin": "1.25.1",
"@opentelemetry/instrumentation": "0.52.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-logs": "0.52.1",
"@opentelemetry/sdk-metrics": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1",
"@opentelemetry/sdk-trace-node": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-base": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
"integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-node": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.25.1.tgz",
"integrity": "sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/context-async-hooks": "1.25.1",
"@opentelemetry/core": "1.25.1",
"@opentelemetry/propagator-b3": "1.25.1",
"@opentelemetry/propagator-jaeger": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1",
"semver": "^7.5.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-node/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
"integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -1190,6 +1697,70 @@
"node": ">=14"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
@ -1651,7 +2222,6 @@
"version": "20.17.57",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
"integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
@ -1698,6 +2268,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/shimmer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
"integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
"license": "MIT"
},
"node_modules/@types/tinycolor2": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz",
@ -2183,7 +2759,6 @@
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@ -2192,6 +2767,15 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-import-attributes": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
"integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
"license": "MIT",
"peerDependencies": {
"acorn": "^8"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@ -2753,6 +3337,12 @@
"node": ">= 16"
}
},
"node_modules/cjs-module-lexer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
"license": "MIT"
},
"node_modules/cli-boxes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
@ -4958,6 +5548,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/import-in-the-middle": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.0.tgz",
"integrity": "sha512-g5zLT0HaztRJWysayWYiUq/7E5H825QIiecMD2pI5QO7Wzr847l6GDvPvmZaDIdrDtS2w7qRczywxiK6SL5vRw==",
"license": "Apache-2.0",
"dependencies": {
"acorn": "^8.14.0",
"acorn-import-attributes": "^1.9.5",
"cjs-module-lexer": "^1.2.2",
"module-details-from-path": "^1.0.3"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@ -5405,7 +6007,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@ -6175,13 +6776,24 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -6424,6 +7036,12 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/module-details-from-path": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
"integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -6891,7 +7509,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
@ -7075,6 +7692,30 @@
"react-is": "^16.13.1"
}
},
"node_modules/protobufjs": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
"integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -7305,6 +7946,20 @@
"node": ">=0.10.0"
}
},
"node_modules/require-in-the-middle": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
"integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
"module-details-from-path": "^1.0.3",
"resolve": "^1.22.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/requireindex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
@ -7319,7 +7974,6 @@
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
@ -7735,6 +8389,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/shimmer": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
"integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
"license": "BSD-2-Clause"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@ -8211,7 +8871,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -8713,7 +9372,6 @@
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/unicorn-magic": {
@ -9575,6 +10233,12 @@
"dependencies": {
"@google/genai": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.11.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.52.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.52.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.52.0",
"@opentelemetry/instrumentation-http": "^0.52.0",
"@opentelemetry/sdk-node": "^0.52.0",
"diff": "^7.0.0",
"dotenv": "^16.4.7",
"fast-glob": "^3.3.3",

View File

@ -53,6 +53,7 @@ vi.mock('@gemini-code/core', async () => {
getGeminiMdFileCount: () => params.geminiMdFileCount,
getVertexAI: () => params.vertexai,
getShowMemoryUsage: () => params.showMemoryUsage, // Added for the test
getTelemetry: () => params.telemetry,
// Add any other methods that are called on the config object
setUserMemory: vi.fn(),
setGeminiMdFileCount: vi.fn(),
@ -108,6 +109,72 @@ describe('loadCliConfig', () => {
});
});
describe('loadCliConfig telemetry', () => {
const originalArgv = process.argv;
const originalEnv = { ...process.env };
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue(MOCK_HOME_DIR);
process.env.GEMINI_API_KEY = 'test-api-key';
});
afterEach(() => {
process.argv = originalArgv;
process.env = originalEnv;
vi.restoreAllMocks();
});
it('should set telemetry to false by default when no flag or setting is present', async () => {
process.argv = ['node', 'script.js'];
const settings: Settings = {};
const result = await loadCliConfig(settings);
expect(result.config.getTelemetry()).toBe(false);
});
it('should set telemetry to true when --telemetry flag is present', async () => {
process.argv = ['node', 'script.js', '--telemetry'];
const settings: Settings = {};
const result = await loadCliConfig(settings);
expect(result.config.getTelemetry()).toBe(true);
});
it('should set telemetry to false when --no-telemetry flag is present', async () => {
process.argv = ['node', 'script.js', '--no-telemetry'];
const settings: Settings = {};
const result = await loadCliConfig(settings);
expect(result.config.getTelemetry()).toBe(false);
});
it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => {
process.argv = ['node', 'script.js'];
const settings: Settings = { telemetry: true };
const result = await loadCliConfig(settings);
expect(result.config.getTelemetry()).toBe(true);
});
it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => {
process.argv = ['node', 'script.js'];
const settings: Settings = { telemetry: false };
const result = await loadCliConfig(settings);
expect(result.config.getTelemetry()).toBe(false);
});
it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => {
process.argv = ['node', 'script.js', '--telemetry'];
const settings: Settings = { telemetry: false };
const result = await loadCliConfig(settings);
expect(result.config.getTelemetry()).toBe(true);
});
it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => {
process.argv = ['node', 'script.js', '--no-telemetry'];
const settings: Settings = { telemetry: true };
const result = await loadCliConfig(settings);
expect(result.config.getTelemetry()).toBe(false);
});
});
describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
beforeEach(() => {
vi.resetAllMocks();

View File

@ -45,6 +45,7 @@ interface CliArgs {
all_files: boolean | undefined;
show_memory_usage: boolean | undefined;
yolo: boolean | undefined;
telemetry: boolean | undefined;
}
async function parseArguments(): Promise<CliArgs> {
@ -89,6 +90,10 @@ async function parseArguments(): Promise<CliArgs> {
'Automatically accept all actions (aka YOLO mode, see https://www.youtube.com/watch?v=xvFZjo5PgG0 for more details)?',
default: false,
})
.option('telemetry', {
type: 'boolean',
description: 'Enable telemetry?',
})
.version() // This will enable the --version flag based on package.json
.help()
.alias('h', 'help')
@ -214,6 +219,10 @@ export async function loadCliConfig(
argv.show_memory_usage || settings.showMemoryUsage || false,
geminiIgnorePatterns,
accessibility: settings.accessibility,
telemetry:
argv.telemetry !== undefined
? argv.telemetry
: (settings.telemetry ?? false),
// Git-aware file filtering settings
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore,
fileFilteringAllowBuildArtifacts:

View File

@ -243,6 +243,62 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged.contextFileName).toBeUndefined();
});
it('should load telemetry setting from user settings', () => {
(mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
);
const userSettingsContent = { telemetry: true };
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
return JSON.stringify(userSettingsContent);
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.telemetry).toBe(true);
});
it('should load telemetry setting from workspace settings', () => {
(mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
);
const workspaceSettingsContent = { telemetry: false };
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
return JSON.stringify(workspaceSettingsContent);
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.telemetry).toBe(false);
});
it('should prioritize workspace telemetry setting over user setting', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const userSettingsContent = { telemetry: true };
const workspaceSettingsContent = { telemetry: false };
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
return JSON.stringify(userSettingsContent);
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
return JSON.stringify(workspaceSettingsContent);
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.telemetry).toBe(false);
});
it('should have telemetry as undefined if not in any settings file', () => {
(mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist
(fs.readFileSync as Mock).mockReturnValue('{}');
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.telemetry).toBeUndefined();
});
it('should handle JSON parsing errors gracefully', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
(fs.readFileSync as Mock).mockImplementation(

View File

@ -36,6 +36,7 @@ export interface Settings {
showMemoryUsage?: boolean;
contextFileName?: string;
accessibility?: AccessibilitySettings;
telemetry?: boolean;
// Git-aware file filtering settings
fileFiltering?: {

View File

@ -17,6 +17,7 @@ import {
Config,
MessageSenderType,
ToolCallRequestInfo,
logUserPrompt,
} from '@gemini-code/core';
import { type PartListUnion } from '@google/genai';
import {
@ -178,6 +179,10 @@ export const useGeminiStream = (
if (typeof query === 'string') {
const trimmedQuery = query.trim();
logUserPrompt(config, {
prompt: trimmedQuery,
prompt_char_count: trimmedQuery.length,
});
onDebugMessage(`User query: '${trimmedQuery}'`);
await logger?.logMessage(MessageSenderType.USER, trimmedQuery);

View File

@ -20,6 +20,7 @@ import {
Tool,
ToolCall,
Status as CoreStatus,
logToolCall,
} from '@gemini-code/core';
import { useCallback, useEffect, useState, useRef } from 'react';
import {
@ -108,6 +109,30 @@ export function useReactToolScheduler(
const allToolCallsCompleteHandler: AllToolCallsCompleteHandler = (
completedToolCalls,
) => {
completedToolCalls.forEach((call) => {
let success = false;
let errorMessage: string | undefined;
let duration = 0;
if (call.status === 'success') {
success = true;
}
if (
call.status === 'error' &&
typeof call.response.resultDisplay === 'string'
) {
errorMessage = call.response.resultDisplay;
}
duration = call.durationMs || 0;
logToolCall({
function_name: call.request.name,
function_args: call.request.args,
duration_ms: duration,
success,
error: errorMessage,
});
});
onComplete(completedToolCalls);
};

View File

@ -28,7 +28,13 @@
"fast-glob": "^3.3.3",
"minimatch": "^10.0.0",
"shell-quote": "^1.8.2",
"strip-ansi": "^7.1.0"
"strip-ansi": "^7.1.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-node": "^0.52.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.52.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.52.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.52.0",
"@opentelemetry/instrumentation-http": "^0.52.0"
},
"devDependencies": {
"@types/diff": "^7.0.2",

View File

@ -48,6 +48,7 @@ describe('Server Config (config.ts)', () => {
const FULL_CONTEXT = false;
const USER_AGENT = 'ServerTestAgent/1.0';
const USER_MEMORY = 'Test User Memory';
const TELEMETRY = false;
const baseParams: ConfigParameters = {
apiKey: API_KEY,
model: MODEL,
@ -58,6 +59,7 @@ describe('Server Config (config.ts)', () => {
fullContext: FULL_CONTEXT,
userAgent: USER_AGENT,
userMemory: USER_MEMORY,
telemetry: TELEMETRY,
};
beforeEach(() => {
@ -161,6 +163,56 @@ describe('Server Config (config.ts)', () => {
expect(config.getFileFilteringAllowBuildArtifacts()).toBe(true);
});
it('Config constructor should set telemetry to true when provided as true', () => {
const paramsWithTelemetry: ConfigParameters = {
...baseParams,
telemetry: true,
};
const config = new Config(paramsWithTelemetry);
expect(config.getTelemetryEnabled()).toBe(true);
});
it('Config constructor should set telemetry to false when provided as false', () => {
const paramsWithTelemetry: ConfigParameters = {
...baseParams,
telemetry: false,
};
const config = new Config(paramsWithTelemetry);
expect(config.getTelemetryEnabled()).toBe(false);
});
it('Config constructor should default telemetry to default value if not provided', () => {
const paramsWithoutTelemetry: ConfigParameters = { ...baseParams };
delete paramsWithoutTelemetry.telemetry;
const config = new Config(paramsWithoutTelemetry);
expect(config.getTelemetryEnabled()).toBe(TELEMETRY);
});
it('createServerConfig should pass telemetry to Config constructor when true', () => {
const paramsWithTelemetry: ConfigParameters = {
...baseParams,
telemetry: true,
};
const config = createServerConfig(paramsWithTelemetry);
expect(config.getTelemetryEnabled()).toBe(true);
});
it('createServerConfig should pass telemetry to Config constructor when false', () => {
const paramsWithTelemetry: ConfigParameters = {
...baseParams,
telemetry: false,
};
const config = createServerConfig(paramsWithTelemetry);
expect(config.getTelemetryEnabled()).toBe(false);
});
it('createServerConfig should default telemetry (to false via Config constructor) if omitted', () => {
const paramsWithoutTelemetry: ConfigParameters = { ...baseParams };
delete paramsWithoutTelemetry.telemetry;
const config = createServerConfig(paramsWithoutTelemetry);
expect(config.getTelemetryEnabled()).toBe(TELEMETRY);
});
it('should have a getFileService method that returns FileDiscoveryService', async () => {
const config = new Config(baseParams);
const fileService = await config.getFileService();

View File

@ -24,6 +24,7 @@ import { WebSearchTool } from '../tools/web-search.js';
import { GeminiClient } from '../core/client.js';
import { GEMINI_CONFIG_DIR as GEMINI_DIR } from '../tools/memoryTool.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { initializeTelemetry } from '../telemetry/index.js';
export enum ApprovalMode {
DEFAULT = 'default',
@ -72,6 +73,8 @@ export interface ConfigParameters {
contextFileName?: string;
geminiIgnorePatterns?: string[];
accessibility?: AccessibilitySettings;
telemetry?: boolean;
telemetryLogUserPromptsEnabled?: boolean;
fileFilteringRespectGitIgnore?: boolean;
fileFilteringAllowBuildArtifacts?: boolean;
}
@ -97,6 +100,9 @@ export class Config {
private readonly vertexai: boolean | undefined;
private readonly showMemoryUsage: boolean;
private readonly accessibility: AccessibilitySettings;
private readonly telemetry: boolean;
private readonly telemetryLogUserPromptsEnabled: boolean;
private readonly telemetryOtlpEndpoint: string;
private readonly geminiClient: GeminiClient;
private readonly geminiIgnorePatterns: string[] = [];
private readonly fileFilteringRespectGitIgnore: boolean;
@ -123,6 +129,11 @@ export class Config {
this.vertexai = params.vertexai;
this.showMemoryUsage = params.showMemoryUsage ?? false;
this.accessibility = params.accessibility ?? {};
this.telemetry = params.telemetry ?? false;
this.telemetryLogUserPromptsEnabled =
params.telemetryLogUserPromptsEnabled ?? true;
this.telemetryOtlpEndpoint =
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4317';
this.fileFilteringRespectGitIgnore =
params.fileFilteringRespectGitIgnore ?? true;
this.fileFilteringAllowBuildArtifacts =
@ -137,6 +148,10 @@ export class Config {
this.toolRegistry = createToolRegistry(this);
this.geminiClient = new GeminiClient(this);
if (this.telemetry) {
initializeTelemetry(this);
}
}
getApiKey(): string {
@ -230,6 +245,18 @@ export class Config {
return this.accessibility;
}
getTelemetryEnabled(): boolean {
return this.telemetry;
}
getTelemetryLogUserPromptsEnabled(): boolean {
return this.telemetryLogUserPromptsEnabled;
}
getTelemetryOtlpEndpoint(): string {
return this.telemetryOtlpEndpoint;
}
getGeminiClient(): GeminiClient {
return this.geminiClient;
}

View File

@ -27,6 +27,11 @@ import { GeminiChat } from './geminiChat.js';
import { retryWithBackoff } from '../utils/retry.js';
import { getErrorMessage } from '../utils/errors.js';
import { tokenLimit } from './tokenLimits.js';
import {
logApiRequest,
logApiResponse,
logApiError,
} from '../telemetry/index.js';
export class GeminiClient {
private chat: Promise<GeminiChat>;
@ -192,6 +197,74 @@ export class GeminiClient {
}
}
private _logApiRequest(model: string, inputTokenCount: number): void {
logApiRequest({
model,
prompt_token_count: inputTokenCount,
duration_ms: 0, // Duration is not known at request time
});
}
private _logApiResponse(
model: string,
durationMs: number,
attempt: number,
response: GenerateContentResponse,
): void {
const promptFeedback = response.promptFeedback;
const finishReason = response.candidates?.[0]?.finishReason;
let responseError;
if (promptFeedback?.blockReason) {
responseError = `Blocked: ${promptFeedback.blockReason}${promptFeedback.blockReasonMessage ? ' - ' + promptFeedback.blockReasonMessage : ''}`;
} else if (
finishReason &&
!['STOP', 'MAX_TOKENS', 'UNSPECIFIED'].includes(finishReason)
) {
responseError = `Finished with reason: ${finishReason}`;
}
logApiResponse({
model,
duration_ms: durationMs,
attempt,
status_code: undefined,
error: responseError,
});
}
private _logApiError(
model: string,
error: unknown,
durationMs: number,
attempt: number,
isAbort: boolean = false,
): void {
let statusCode: number | string | undefined;
let errorMessage = getErrorMessage(error);
if (isAbort) {
errorMessage = 'Request aborted by user';
statusCode = 'ABORTED'; // Custom S
} else if (typeof error === 'object' && error !== null) {
if ('status' in error) {
statusCode = (error as { status: number | string }).status;
} else if ('code' in error) {
statusCode = (error as { code: number | string }).code;
} else if ('httpStatusCode' in error) {
statusCode = (error as { httpStatusCode: number | string })
.httpStatusCode;
}
}
logApiError({
model,
error: errorMessage,
status_code: statusCode,
duration_ms: durationMs,
attempt,
});
}
async generateJson(
contents: Content[],
schema: SchemaUnion,
@ -199,6 +272,8 @@ export class GeminiClient {
model: string = 'gemini-2.0-flash',
config: GenerateContentConfig = {},
): Promise<Record<string, unknown>> {
const attempt = 1;
const startTime = Date.now();
try {
const userMemory = this.config.getUserMemory();
const systemInstruction = getCoreSystemPrompt(userMemory);
@ -208,6 +283,22 @@ export class GeminiClient {
...config,
};
let inputTokenCount = 0;
try {
const { totalTokens } = await this.client.models.countTokens({
model,
contents,
});
inputTokenCount = totalTokens || 0;
} catch (_e) {
console.warn(
`Failed to count tokens for model ${model}. Proceeding with inputTokenCount = 0. Error: ${getErrorMessage(_e)}`,
);
inputTokenCount = 0;
}
this._logApiRequest(model, inputTokenCount);
const apiCall = () =>
this.client.models.generateContent({
model,
@ -221,6 +312,7 @@ export class GeminiClient {
});
const result = await retryWithBackoff(apiCall);
const durationMs = Date.now() - startTime;
const text = getResponseText(result);
if (!text) {
@ -233,10 +325,13 @@ export class GeminiClient {
contents,
'generateJson-empty-response',
);
this._logApiError(model, error, durationMs, attempt);
throw error;
}
try {
return JSON.parse(text);
const parsedJson = JSON.parse(text);
this._logApiResponse(model, durationMs, attempt, result);
return parsedJson;
} catch (parseError) {
await reportError(
parseError,
@ -247,13 +342,15 @@ export class GeminiClient {
},
'generateJson-parse',
);
this._logApiError(model, parseError, durationMs, attempt);
throw new Error(
`Failed to parse API response as JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
`Failed to parse API response as JSON: ${getErrorMessage(parseError)}`,
);
}
} catch (error) {
const durationMs = Date.now() - startTime;
if (abortSignal.aborted) {
// Regular cancellation error, fail normally
this._logApiError(model, error, durationMs, attempt, true);
throw error;
}
@ -264,15 +361,17 @@ export class GeminiClient {
) {
throw error;
}
this._logApiError(model, error, durationMs, attempt);
await reportError(
error,
'Error generating JSON content via API.',
contents,
'generateJson-api',
);
const message =
error instanceof Error ? error.message : 'Unknown API error.';
throw new Error(`Failed to generate JSON content: ${message}`);
throw new Error(
`Failed to generate JSON content: ${getErrorMessage(error)}`,
);
}
}
@ -286,6 +385,8 @@ export class GeminiClient {
...this.generateContentConfig,
...generationConfig,
};
const attempt = 1;
const startTime = Date.now();
try {
const userMemory = this.config.getUserMemory();
@ -297,6 +398,22 @@ export class GeminiClient {
systemInstruction,
};
let inputTokenCount = 0;
try {
const { totalTokens } = await this.client.models.countTokens({
model: modelToUse,
contents,
});
inputTokenCount = totalTokens || 0;
} catch (_e) {
console.warn(
`Failed to count tokens for model ${modelToUse}. Proceeding with inputTokenCount = 0. Error: ${getErrorMessage(_e)}`,
);
inputTokenCount = 0;
}
this._logApiRequest(modelToUse, inputTokenCount);
const apiCall = () =>
this.client.models.generateContent({
model: modelToUse,
@ -305,12 +422,18 @@ export class GeminiClient {
});
const result = await retryWithBackoff(apiCall);
const durationMs = Date.now() - startTime;
this._logApiResponse(modelToUse, durationMs, attempt, result);
return result;
} catch (error) {
} catch (error: unknown) {
const durationMs = Date.now() - startTime;
if (abortSignal.aborted) {
this._logApiError(modelToUse, error, durationMs, attempt, true);
throw error;
}
this._logApiError(modelToUse, error, durationMs, attempt);
await reportError(
error,
`Error generating content via API with model ${modelToUse}.`,
@ -320,10 +443,8 @@ export class GeminiClient {
},
'generateContent-api',
);
const message =
error instanceof Error ? error.message : 'Unknown API error.';
throw new Error(
`Failed to generate content with model ${modelToUse}: ${message}`,
`Failed to generate content with model ${modelToUse}: ${getErrorMessage(error)}`,
);
}
}

View File

@ -21,18 +21,21 @@ export type ValidatingToolCall = {
status: 'validating';
request: ToolCallRequestInfo;
tool: Tool;
startTime?: number;
};
export type ScheduledToolCall = {
status: 'scheduled';
request: ToolCallRequestInfo;
tool: Tool;
startTime?: number;
};
export type ErroredToolCall = {
status: 'error';
request: ToolCallRequestInfo;
response: ToolCallResponseInfo;
durationMs?: number;
};
export type SuccessfulToolCall = {
@ -40,6 +43,7 @@ export type SuccessfulToolCall = {
request: ToolCallRequestInfo;
tool: Tool;
response: ToolCallResponseInfo;
durationMs?: number;
};
export type ExecutingToolCall = {
@ -47,6 +51,7 @@ export type ExecutingToolCall = {
request: ToolCallRequestInfo;
tool: Tool;
liveOutput?: string;
startTime?: number;
};
export type CancelledToolCall = {
@ -54,6 +59,7 @@ export type CancelledToolCall = {
request: ToolCallRequestInfo;
response: ToolCallResponseInfo;
tool: Tool;
durationMs?: number;
};
export type WaitingToolCall = {
@ -61,6 +67,7 @@ export type WaitingToolCall = {
request: ToolCallRequestInfo;
tool: Tool;
confirmationDetails: ToolCallConfirmationDetails;
startTime?: number;
};
export type Status = ToolCall['status'];
@ -246,40 +253,69 @@ export class CoreToolScheduler {
this.toolCalls = this.toolCalls.map((currentCall) => {
if (
currentCall.request.callId !== targetCallId ||
currentCall.status === 'error'
currentCall.status === 'success' ||
currentCall.status === 'error' ||
currentCall.status === 'cancelled'
) {
return currentCall;
}
const callWithToolContext = currentCall as ToolCall & { tool: Tool };
// currentCall is a non-terminal state here and should have startTime and tool.
const existingStartTime = currentCall.startTime;
const toolInstance = (
currentCall as
| ValidatingToolCall
| ScheduledToolCall
| ExecutingToolCall
| WaitingToolCall
).tool;
switch (newStatus) {
case 'success':
case 'success': {
const durationMs = existingStartTime
? Date.now() - existingStartTime
: undefined;
return {
...callWithToolContext,
request: currentCall.request,
tool: toolInstance,
status: 'success',
response: auxiliaryData as ToolCallResponseInfo,
durationMs,
} as SuccessfulToolCall;
case 'error':
}
case 'error': {
const durationMs = existingStartTime
? Date.now() - existingStartTime
: undefined;
return {
request: currentCall.request,
status: 'error',
response: auxiliaryData as ToolCallResponseInfo,
durationMs,
} as ErroredToolCall;
}
case 'awaiting_approval':
return {
...callWithToolContext,
request: currentCall.request,
tool: toolInstance,
status: 'awaiting_approval',
confirmationDetails: auxiliaryData as ToolCallConfirmationDetails,
startTime: existingStartTime,
} as WaitingToolCall;
case 'scheduled':
return {
...callWithToolContext,
request: currentCall.request,
tool: toolInstance,
status: 'scheduled',
startTime: existingStartTime,
} as ScheduledToolCall;
case 'cancelled':
case 'cancelled': {
const durationMs = existingStartTime
? Date.now() - existingStartTime
: undefined;
return {
...callWithToolContext,
request: currentCall.request,
tool: toolInstance,
status: 'cancelled',
response: {
callId: currentCall.request.callId,
@ -295,16 +331,22 @@ export class CoreToolScheduler {
resultDisplay: undefined,
error: undefined,
},
durationMs,
} as CancelledToolCall;
}
case 'validating':
return {
...(currentCall as ValidatingToolCall),
request: currentCall.request,
tool: toolInstance,
status: 'validating',
startTime: existingStartTime,
} as ValidatingToolCall;
case 'executing':
return {
...callWithToolContext,
request: currentCall.request,
tool: toolInstance,
status: 'executing',
startTime: existingStartTime,
} as ExecutingToolCall;
default: {
const exhaustiveCheck: never = newStatus;
@ -345,9 +387,15 @@ export class CoreToolScheduler {
reqInfo,
new Error(`Tool "${reqInfo.name}" not found in registry.`),
),
durationMs: 0,
};
}
return { status: 'validating', request: reqInfo, tool: toolInstance };
return {
status: 'validating',
request: reqInfo,
tool: toolInstance,
startTime: Date.now(),
};
},
);

View File

@ -44,3 +44,6 @@ export * from './tools/memoryTool.js';
export * from './tools/shell.js';
export * from './tools/web-search.js';
export * from './tools/read-many-files.js';
// Export telemetry functions
export * from './telemetry/index.js';

View File

@ -0,0 +1,24 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { randomUUID } from 'crypto';
export const SERVICE_NAME = 'gemini-code';
export const sessionId = randomUUID();
export const EVENT_USER_PROMPT = 'gemini_code.user_prompt';
export const EVENT_TOOL_CALL = 'gemini_code.tool_call';
export const EVENT_API_REQUEST = 'gemini_code.api_request';
export const EVENT_API_ERROR = 'gemini_code.api_error';
export const EVENT_API_RESPONSE = 'gemini_code.api_response';
export const EVENT_CLI_CONFIG = 'gemini_code.config';
export const METRIC_TOOL_CALL_COUNT = 'gemini_code.tool.call.count';
export const METRIC_TOOL_CALL_LATENCY = 'gemini_code.tool.call.latency';
export const METRIC_API_REQUEST_COUNT = 'gemini_code.api.request.count';
export const METRIC_API_REQUEST_LATENCY = 'gemini_code.api.request.latency';
export const METRIC_TOKEN_INPUT_COUNT = 'gemini_code.token.input.count';
export const METRIC_SESSION_COUNT = 'gemini_code.session.count';

View File

@ -0,0 +1,31 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export {
initializeTelemetry,
shutdownTelemetry,
isTelemetrySdkInitialized,
} from './sdk.js';
export {
logCliConfiguration,
logUserPrompt,
logToolCall,
logApiRequest,
logApiError,
logApiResponse,
} from './loggers.js';
export {
UserPromptEvent,
ToolCallEvent,
ApiRequestEvent,
ApiErrorEvent,
ApiResponseEvent,
CliConfigEvent,
TelemetryEvent,
} from './types.js';
export { SpanStatusCode, ValueType } from '@opentelemetry/api';
export { SemanticAttributes } from '@opentelemetry/semantic-conventions';
export { sessionId } from './constants.js';

View File

@ -0,0 +1,191 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { logs, LogRecord, LogAttributes } from '@opentelemetry/api-logs';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { Config } from '../config/config.js';
import {
EVENT_API_ERROR,
EVENT_API_REQUEST,
EVENT_API_RESPONSE,
EVENT_CLI_CONFIG,
EVENT_TOOL_CALL,
EVENT_USER_PROMPT,
SERVICE_NAME,
} from './constants.js';
import {
ApiErrorEvent,
ApiRequestEvent,
ApiResponseEvent,
ToolCallEvent,
UserPromptEvent,
} from './types.js';
import {
recordApiErrorMetrics,
recordApiRequestMetrics,
recordApiResponseMetrics,
recordToolCallMetrics,
} from './metrics.js';
import { isTelemetrySdkInitialized } from './sdk.js';
const shouldLogUserPrompts = (config: Config): boolean =>
config.getTelemetryLogUserPromptsEnabled() ?? false;
export function logCliConfiguration(config: Config): void {
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
'event.name': EVENT_CLI_CONFIG,
'event.timestamp': new Date().toISOString(),
model: config.getModel(),
sandbox_enabled:
typeof config.getSandbox() === 'string' ? true : config.getSandbox(),
core_tools_enabled: (config.getCoreTools() ?? []).join(','),
approval_mode: config.getApprovalMode(),
vertex_ai_enabled: config.getVertexAI() ?? false,
log_user_prompts_enabled: config.getTelemetryLogUserPromptsEnabled(),
file_filtering_respect_git_ignore:
config.getFileFilteringRespectGitIgnore(),
file_filtering_allow_build_artifacts:
config.getFileFilteringAllowBuildArtifacts(),
};
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: 'CLI configuration loaded.',
attributes,
};
logger.emit(logRecord);
}
export function logUserPrompt(
config: Config,
event: Omit<UserPromptEvent, 'event.name' | 'event.timestamp' | 'prompt'> & {
prompt: string;
},
): void {
if (!isTelemetrySdkInitialized()) return;
const { prompt, ...restOfEventArgs } = event;
const attributes: LogAttributes = {
...restOfEventArgs,
'event.name': EVENT_USER_PROMPT,
'event.timestamp': new Date().toISOString(),
};
if (shouldLogUserPrompts(config)) {
attributes.prompt = prompt;
}
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `User prompt. Length: ${event.prompt_char_count}`,
attributes,
};
logger.emit(logRecord);
}
export function logToolCall(
event: Omit<ToolCallEvent, 'event.name' | 'event.timestamp'>,
): void {
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...event,
'event.name': EVENT_TOOL_CALL,
'event.timestamp': new Date().toISOString(),
function_args: JSON.stringify(event.function_args),
};
if (event.error) {
attributes['error.message'] = event.error;
if (event.error_type) {
attributes['error.type'] = event.error_type;
}
}
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `Tool call: ${event.function_name}. Success: ${event.success}. Duration: ${event.duration_ms}ms.`,
attributes,
};
logger.emit(logRecord);
recordToolCallMetrics(event.function_name, event.duration_ms, event.success);
}
export function logApiRequest(
event: Omit<ApiRequestEvent, 'event.name' | 'event.timestamp'>,
): void {
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...event,
'event.name': EVENT_API_REQUEST,
'event.timestamp': new Date().toISOString(),
};
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `API request to ${event.model}. Tokens: ${event.prompt_token_count}.`,
attributes,
};
logger.emit(logRecord);
recordApiRequestMetrics(event.model, event.prompt_token_count);
}
export function logApiError(
event: Omit<ApiErrorEvent, 'event.name' | 'event.timestamp'>,
): void {
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...event,
'event.name': EVENT_API_ERROR,
'event.timestamp': new Date().toISOString(),
['error.message']: event.error,
};
if (event.error_type) {
attributes['error.type'] = event.error_type;
}
if (typeof event.status_code === 'number') {
attributes[SemanticAttributes.HTTP_STATUS_CODE] = event.status_code;
}
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `API error for ${event.model}. Error: ${event.error}. Duration: ${event.duration_ms}ms.`,
attributes,
};
logger.emit(logRecord);
recordApiErrorMetrics(
event.model,
event.duration_ms,
event.status_code,
event.error_type,
);
}
export function logApiResponse(
event: Omit<ApiResponseEvent, 'event.name' | 'event.timestamp'>,
): void {
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...event,
'event.name': EVENT_API_RESPONSE,
'event.timestamp': new Date().toISOString(),
};
if (event.error) {
attributes['error.message'] = event.error;
} else if (event.status_code) {
if (typeof event.status_code === 'number') {
attributes[SemanticAttributes.HTTP_STATUS_CODE] = event.status_code;
}
}
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `API response from ${event.model}. Status: ${event.status_code || 'N/A'}. Duration: ${event.duration_ms}ms.`,
attributes,
};
logger.emit(logRecord);
recordApiResponseMetrics(
event.model,
event.duration_ms,
event.status_code,
event.error,
);
}

View File

@ -0,0 +1,145 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
metrics,
Attributes,
ValueType,
Meter,
Counter,
Histogram,
} from '@opentelemetry/api';
import {
SERVICE_NAME,
METRIC_TOOL_CALL_COUNT,
METRIC_TOOL_CALL_LATENCY,
METRIC_API_REQUEST_COUNT,
METRIC_API_REQUEST_LATENCY,
METRIC_TOKEN_INPUT_COUNT,
METRIC_SESSION_COUNT,
} from './constants.js';
let cliMeter: Meter | undefined;
let toolCallCounter: Counter | undefined;
let toolCallLatencyHistogram: Histogram | undefined;
let apiRequestCounter: Counter | undefined;
let apiRequestLatencyHistogram: Histogram | undefined;
let tokenInputCounter: Counter | undefined;
let isMetricsInitialized = false;
export function getMeter(): Meter | undefined {
if (!cliMeter) {
cliMeter = metrics.getMeter(SERVICE_NAME);
}
return cliMeter;
}
export function initializeMetrics(): void {
if (isMetricsInitialized) return;
const meter = getMeter();
if (!meter) return;
toolCallCounter = meter.createCounter(METRIC_TOOL_CALL_COUNT, {
description: 'Counts tool calls, tagged by function name and success.',
valueType: ValueType.INT,
});
toolCallLatencyHistogram = meter.createHistogram(METRIC_TOOL_CALL_LATENCY, {
description: 'Latency of tool calls in milliseconds.',
unit: 'ms',
valueType: ValueType.INT,
});
apiRequestCounter = meter.createCounter(METRIC_API_REQUEST_COUNT, {
description: 'Counts API requests, tagged by model and status.',
valueType: ValueType.INT,
});
apiRequestLatencyHistogram = meter.createHistogram(
METRIC_API_REQUEST_LATENCY,
{
description: 'Latency of API requests in milliseconds.',
unit: 'ms',
valueType: ValueType.INT,
},
);
tokenInputCounter = meter.createCounter(METRIC_TOKEN_INPUT_COUNT, {
description: 'Counts the total number of input tokens sent to the API.',
valueType: ValueType.INT,
});
const sessionCounter = meter.createCounter(METRIC_SESSION_COUNT, {
description: 'Count of CLI sessions started.',
valueType: ValueType.INT,
});
sessionCounter.add(1);
isMetricsInitialized = true;
}
export function recordToolCallMetrics(
functionName: string,
durationMs: number,
success: boolean,
): void {
if (!toolCallCounter || !toolCallLatencyHistogram || !isMetricsInitialized)
return;
const metricAttributes: Attributes = {
function_name: functionName,
success,
};
toolCallCounter.add(1, metricAttributes);
toolCallLatencyHistogram.record(durationMs, {
function_name: functionName,
});
}
export function recordApiRequestMetrics(
model: string,
inputTokenCount: number,
): void {
if (!tokenInputCounter || !isMetricsInitialized) return;
tokenInputCounter.add(inputTokenCount, { model });
}
export function recordApiResponseMetrics(
model: string,
durationMs: number,
statusCode?: number | string,
error?: string,
): void {
if (
!apiRequestCounter ||
!apiRequestLatencyHistogram ||
!isMetricsInitialized
)
return;
const metricAttributes: Attributes = {
model,
status_code: statusCode ?? (error ? 'error' : 'ok'),
};
apiRequestCounter.add(1, metricAttributes);
apiRequestLatencyHistogram.record(durationMs, { model });
}
export function recordApiErrorMetrics(
model: string,
durationMs: number,
statusCode?: number | string,
errorType?: string,
): void {
if (
!apiRequestCounter ||
!apiRequestLatencyHistogram ||
!isMetricsInitialized
)
return;
const metricAttributes: Attributes = {
model,
status_code: statusCode ?? 'error',
error_type: errorType ?? 'unknown',
};
apiRequestCounter.add(1, metricAttributes);
apiRequestLatencyHistogram.record(durationMs, { model });
}

View File

@ -0,0 +1,128 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { Resource } from '@opentelemetry/resources';
import {
BatchSpanProcessor,
ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-node';
import {
BatchLogRecordProcessor,
ConsoleLogRecordExporter,
} from '@opentelemetry/sdk-logs';
import {
ConsoleMetricExporter,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { Config } from '../config/config.js';
import { SERVICE_NAME, sessionId } from './constants.js';
import { initializeMetrics } from './metrics.js';
import { logCliConfiguration } from './loggers.js';
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
let sdk: NodeSDK | undefined;
let telemetryInitialized = false;
export function isTelemetrySdkInitialized(): boolean {
return telemetryInitialized;
}
function parseGrpcEndpoint(
otlpEndpointSetting: string | undefined,
): string | undefined {
if (!otlpEndpointSetting) {
return undefined;
}
// Trim leading/trailing quotes that might come from env variables
const trimmedEndpoint = otlpEndpointSetting.replace(/^["']|["']$/g, '');
try {
const url = new URL(trimmedEndpoint);
// OTLP gRPC exporters expect an endpoint in the format scheme://host:port
// The `origin` property provides this, stripping any path, query, or hash.
return url.origin;
} catch (error) {
diag.error('Invalid OTLP endpoint URL provided:', trimmedEndpoint, error);
return undefined;
}
}
export function initializeTelemetry(config: Config): void {
if (telemetryInitialized || !config.getTelemetryEnabled()) {
return;
}
const geminiCliVersion = config.getUserAgent() || 'unknown';
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
[SemanticResourceAttributes.SERVICE_VERSION]: geminiCliVersion,
'session.id': sessionId,
});
const otlpEndpoint = config.getTelemetryOtlpEndpoint();
const grpcParsedEndpoint = parseGrpcEndpoint(otlpEndpoint);
const useOtlp = !!grpcParsedEndpoint;
const spanExporter = useOtlp
? new OTLPTraceExporter({ url: grpcParsedEndpoint })
: new ConsoleSpanExporter();
const logExporter = useOtlp
? new OTLPLogExporter({ url: grpcParsedEndpoint })
: new ConsoleLogRecordExporter();
const metricReader = useOtlp
? new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: grpcParsedEndpoint }),
exportIntervalMillis: 10000,
})
: new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
exportIntervalMillis: 10000,
});
sdk = new NodeSDK({
resource,
spanProcessors: [new BatchSpanProcessor(spanExporter)],
logRecordProcessor: new BatchLogRecordProcessor(logExporter),
metricReader,
instrumentations: [new HttpInstrumentation()],
});
try {
sdk.start();
console.log('OpenTelemetry SDK started successfully.');
telemetryInitialized = true;
initializeMetrics();
logCliConfiguration(config);
} catch (error) {
console.error('Error starting OpenTelemetry SDK:', error);
}
process.on('SIGTERM', shutdownTelemetry);
process.on('SIGINT', shutdownTelemetry);
}
export async function shutdownTelemetry(): Promise<void> {
if (!telemetryInitialized || !sdk) {
return;
}
try {
await sdk.shutdown();
console.log('OpenTelemetry SDK shut down successfully.');
} catch (error) {
console.error('Error shutting down SDK:', error);
} finally {
telemetryInitialized = false;
}
}

View File

@ -0,0 +1,73 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export interface UserPromptEvent {
'event.name': 'user_prompt';
'event.timestamp': string; // ISO 8601
prompt_char_count: number;
prompt?: string;
}
export interface ToolCallEvent {
'event.name': 'tool_call';
'event.timestamp': string; // ISO 8601
function_name: string;
function_args: Record<string, unknown>;
duration_ms: number;
success: boolean;
error?: string;
error_type?: string;
}
export interface ApiRequestEvent {
'event.name': 'api_request';
'event.timestamp': string; // ISO 8601
model: string;
duration_ms: number;
prompt_token_count: number;
}
export interface ApiErrorEvent {
'event.name': 'api_error';
'event.timestamp': string; // ISO 8601
model: string;
error: string;
error_type?: string;
status_code?: number | string;
duration_ms: number;
attempt: number;
}
export interface ApiResponseEvent {
'event.name': 'api_response';
'event.timestamp': string; // ISO 8601
model: string;
status_code?: number | string;
duration_ms: number;
error?: string;
attempt: number;
}
export interface CliConfigEvent {
'event.name': 'cli_config';
'event.timestamp': string; // ISO 8601
model: string;
sandbox_enabled: boolean;
core_tools_enabled: string;
approval_mode: string;
vertex_ai_enabled: boolean;
log_user_prompts_enabled: boolean;
file_filtering_respect_git_ignore: boolean;
file_filtering_allow_build_artifacts: boolean;
}
export type TelemetryEvent =
| UserPromptEvent
| ToolCallEvent
| ApiRequestEvent
| ApiErrorEvent
| ApiResponseEvent
| CliConfigEvent;