mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-28 06:50:35 -07:00
feat: add direct Google Cloud telemetry exporters (#8541)
This commit is contained in:
@@ -1,168 +1,212 @@
|
||||
# Gemini CLI Observability Guide
|
||||
# Observability with OpenTelemetry
|
||||
|
||||
Telemetry provides data about 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.
|
||||
Learn how to enable and setup OpenTelemetry for Gemini CLI.
|
||||
|
||||
Gemini CLI's telemetry system is built on the **[OpenTelemetry] (OTEL)** standard, allowing you to send data to any compatible backend.
|
||||
## Key Benefits
|
||||
|
||||
- **🔍 Usage Analytics**: Understand interaction patterns and feature adoption
|
||||
across your team
|
||||
- **⚡ Performance Monitoring**: Track response times, token consumption, and
|
||||
resource utilization
|
||||
- **🐛 Real-time Debugging**: Identify bottlenecks, failures, and error patterns
|
||||
as they occur
|
||||
- **📊 Workflow Optimization**: Make informed decisions to improve
|
||||
configurations and processes
|
||||
- **🏢 Enterprise Governance**: Monitor usage across teams, track costs, ensure
|
||||
compliance, and integrate with existing monitoring infrastructure
|
||||
|
||||
## OpenTelemetry Integration
|
||||
|
||||
Built on **[OpenTelemetry]** — the vendor-neutral, industry-standard
|
||||
observability framework — Gemini CLI's observability system provides:
|
||||
|
||||
- **Universal Compatibility**: Export to any OpenTelemetry backend (Google
|
||||
Cloud, Jaeger, Prometheus, Datadog, etc.)
|
||||
- **Standardized Data**: Use consistent formats and collection methods across
|
||||
your toolchain
|
||||
- **Future-Proof Integration**: Connect with existing and future observability
|
||||
infrastructure
|
||||
- **No Vendor Lock-in**: Switch between backends without changing your
|
||||
instrumentation
|
||||
|
||||
[OpenTelemetry]: https://opentelemetry.io/
|
||||
|
||||
## Enabling telemetry
|
||||
- [Observability with OpenTelemetry](#observability-with-opentelemetry)
|
||||
- [Key Benefits](#key-benefits)
|
||||
- [OpenTelemetry Integration](#opentelemetry-integration)
|
||||
- [Configuration](#configuration)
|
||||
- [Google Cloud Telemetry](#google-cloud-telemetry)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Direct Export (Recommended)](#direct-export-recommended)
|
||||
- [Collector-Based Export (Advanced)](#collector-based-export-advanced)
|
||||
- [Local Telemetry](#local-telemetry)
|
||||
- [File-based Output (Recommended)](#file-based-output-recommended)
|
||||
- [Collector-Based Export (Advanced)](#collector-based-export-advanced-1)
|
||||
- [Logs and Metrics](#logs-and-metrics)
|
||||
- [Logs](#logs)
|
||||
- [Metrics](#metrics)
|
||||
|
||||
You can enable telemetry in multiple ways. Configuration is primarily managed via the [`.gemini/settings.json` file](./cli/configuration.md) and environment variables, but CLI flags can override these settings for a specific session.
|
||||
## Configuration
|
||||
|
||||
### Order of precedence
|
||||
All telemetry behavior is controlled through your `.gemini/settings.json` file
|
||||
and can be overridden with CLI flags:
|
||||
|
||||
The following lists the precedence for applying telemetry settings, with items listed higher having greater precedence:
|
||||
| Setting | Values | Default | CLI Override | Description |
|
||||
| -------------- | ----------------- | ----------------------- | -------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `enabled` | `true`/`false` | `false` | `--telemetry` / `--no-telemetry` | Enable or disable telemetry |
|
||||
| `target` | `"gcp"`/`"local"` | `"local"` | `--telemetry-target <local\|gcp>` | Where to send telemetry data |
|
||||
| `otlpEndpoint` | URL string | `http://localhost:4317` | `--telemetry-otlp-endpoint <URL>` | OTLP collector endpoint |
|
||||
| `otlpProtocol` | `"grpc"`/`"http"` | `"grpc"` | `--telemetry-otlp-protocol <grpc\|http>` | OTLP transport protocol |
|
||||
| `outfile` | file path | - | `--telemetry-outfile <path>` | Save telemetry to file (requires `otlpEndpoint: ""`) |
|
||||
| `logPrompts` | `true`/`false` | `true` | `--telemetry-log-prompts` / `--no-telemetry-log-prompts` | Include prompts in telemetry logs |
|
||||
| `useCollector` | `true`/`false` | `false` | - | Use external OTLP collector (advanced) |
|
||||
|
||||
1. **CLI flags (for `gemini` command):**
|
||||
- `--telemetry` / `--no-telemetry`: Overrides `telemetry.enabled`.
|
||||
- `--telemetry-target <local|gcp>`: Overrides `telemetry.target`.
|
||||
- `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`.
|
||||
- `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`.
|
||||
- `--telemetry-outfile <path>`: Redirects telemetry output to a file. See [Exporting to a file](#exporting-to-a-file).
|
||||
For detailed information about all configuration options, see the
|
||||
[Configuration Guide](./cli/configuration.md).
|
||||
|
||||
1. **Environment variables:**
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint`.
|
||||
## Google Cloud Telemetry
|
||||
|
||||
1. **Workspace settings file (`.gemini/settings.json`):** Values from the `telemetry` object in this project-specific file.
|
||||
### Prerequisites
|
||||
|
||||
1. **User settings file (`~/.gemini/settings.json`):** Values from the `telemetry` object in this global user file.
|
||||
Before using either method below, complete these steps:
|
||||
|
||||
1. **Defaults:** applied if not set by any of the above.
|
||||
- `telemetry.enabled`: `false`
|
||||
- `telemetry.target`: `local`
|
||||
- `telemetry.otlpEndpoint`: `http://localhost:4317`
|
||||
- `telemetry.logPrompts`: `true`
|
||||
1. Set your Google Cloud project ID:
|
||||
- For telemetry in a separate project from inference:
|
||||
```bash
|
||||
export OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id"
|
||||
```
|
||||
- For telemetry in the same project as inference:
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
```
|
||||
|
||||
**For the `npm run telemetry -- --target=<gcp|local>` script:**
|
||||
The `--target` argument to this script _only_ overrides the `telemetry.target` for the duration and purpose of that script (i.e., choosing which collector to start). It does not permanently change your `settings.json`. The script will first look at `settings.json` for a `telemetry.target` to use as its default.
|
||||
2. Authenticate with Google Cloud:
|
||||
- If using a user account:
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
- If using a service account:
|
||||
```bash
|
||||
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account.json"
|
||||
```
|
||||
3. Make sure your account or service account has these IAM roles:
|
||||
- Cloud Trace Agent
|
||||
- Monitoring Metric Writer
|
||||
- Logs Writer
|
||||
|
||||
### Example settings
|
||||
4. Enable the required Google Cloud APIs (if not already enabled):
|
||||
```bash
|
||||
gcloud services enable \
|
||||
cloudtrace.googleapis.com \
|
||||
monitoring.googleapis.com \
|
||||
logging.googleapis.com \
|
||||
--project="$OTLP_GOOGLE_CLOUD_PROJECT"
|
||||
```
|
||||
|
||||
The following code can be added to your workspace (`.gemini/settings.json`) or user (`~/.gemini/settings.json`) settings to enable telemetry and send the output to Google Cloud:
|
||||
### Direct Export (Recommended)
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
},
|
||||
"tools": {
|
||||
"sandbox": false
|
||||
}
|
||||
}
|
||||
```
|
||||
Sends telemetry directly to Google Cloud services. No collector needed.
|
||||
|
||||
### Exporting to a file
|
||||
1. Enable telemetry in your `.gemini/settings.json`:
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Run Gemini CLI and send prompts.
|
||||
3. View logs and metrics:
|
||||
- Open the Google Cloud Console in your browser after sending prompts:
|
||||
- Logs: https://console.cloud.google.com/logs/
|
||||
- Metrics: https://console.cloud.google.com/monitoring/metrics-explorer
|
||||
- Traces: https://console.cloud.google.com/traces/list
|
||||
|
||||
You can export all telemetry data to a file for local inspection.
|
||||
### Collector-Based Export (Advanced)
|
||||
|
||||
To enable file export, use the `--telemetry-outfile` flag with a path to your desired output file. This must be run using `--telemetry-target=local`.
|
||||
For custom processing, filtering, or routing, use an OpenTelemetry collector to
|
||||
forward data to Google Cloud.
|
||||
|
||||
```bash
|
||||
# Set your desired output file path
|
||||
TELEMETRY_FILE=".gemini/telemetry.log"
|
||||
1. Configure your `.gemini/settings.json`:
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp",
|
||||
"useCollector": true
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Run the automation script:
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
This will:
|
||||
- Start a local OTEL collector that forwards to Google Cloud
|
||||
- Configure your workspace
|
||||
- Provide links to view traces, metrics, and logs in Google Cloud Console
|
||||
- Save collector logs to `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log`
|
||||
- Stop collector on exit (e.g. `Ctrl+C`)
|
||||
3. Run Gemini CLI and send prompts.
|
||||
4. View logs and metrics:
|
||||
- Open the Google Cloud Console in your browser after sending prompts:
|
||||
- Logs: https://console.cloud.google.com/logs/
|
||||
- Metrics: https://console.cloud.google.com/monitoring/metrics-explorer
|
||||
- Traces: https://console.cloud.google.com/traces/list
|
||||
- Open `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log` to view local
|
||||
collector logs.
|
||||
|
||||
# Run Gemini CLI with local telemetry
|
||||
# NOTE: --telemetry-otlp-endpoint="" is required to override the default
|
||||
# OTLP exporter and ensure telemetry is written to the local file.
|
||||
gemini --telemetry \
|
||||
--telemetry-target=local \
|
||||
--telemetry-otlp-endpoint="" \
|
||||
--telemetry-outfile="$TELEMETRY_FILE" \
|
||||
--prompt "What is OpenTelemetry?"
|
||||
```
|
||||
## Local Telemetry
|
||||
|
||||
## Running an OTEL Collector
|
||||
For local development and debugging, you can capture telemetry data locally:
|
||||
|
||||
An OTEL Collector is a service that receives, processes, and exports telemetry data.
|
||||
The CLI can send data using either the OTLP/gRPC or OTLP/HTTP protocol.
|
||||
You can specify which protocol to use via the `--telemetry-otlp-protocol` flag
|
||||
or the `telemetry.otlpProtocol` setting in your `settings.json` file. See the
|
||||
[configuration docs](./cli/configuration.md#--telemetry-otlp-protocol) for more
|
||||
details.
|
||||
### File-based Output (Recommended)
|
||||
|
||||
Learn more about OTEL exporter standard configuration in [documentation][otel-config-docs].
|
||||
1. Enable telemetry in your `.gemini/settings.json`:
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "",
|
||||
"outfile": ".gemini/telemetry.log"
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Run Gemini CLI and send prompts.
|
||||
3. View logs and metrics in the specified file (e.g., `.gemini/telemetry.log`).
|
||||
|
||||
[otel-config-docs]: https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
|
||||
### Collector-Based Export (Advanced)
|
||||
|
||||
### Local
|
||||
1. Run the automation script:
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
This will:
|
||||
- Download and start Jaeger and OTEL collector
|
||||
- Configure your workspace for local telemetry
|
||||
- Provide a Jaeger UI at http://localhost:16686
|
||||
- Save logs/metrics to `~/.gemini/tmp/<projectHash>/otel/collector.log`
|
||||
- Stop collector on exit (e.g. `Ctrl+C`)
|
||||
2. Run Gemini CLI and send prompts.
|
||||
3. View traces at http://localhost:16686 and logs/metrics in the collector log
|
||||
file.
|
||||
|
||||
Use the `npm run telemetry -- --target=local` command to automate the process of setting up a local telemetry pipeline, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib` (the OpenTelemetry Collector) and `jaeger` (The Jaeger UI for viewing traces). To use it:
|
||||
## Logs and Metrics
|
||||
|
||||
1. **Run the command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Download Jaeger and OTEL if needed.
|
||||
- Start a local Jaeger instance.
|
||||
- Start an OTEL collector configured to receive data from Gemini CLI.
|
||||
- Automatically enable telemetry in your workspace settings.
|
||||
- On exit, disable telemetry.
|
||||
|
||||
1. **View traces**:
|
||||
Open your web browser and navigate to **http://localhost:16686** to access the Jaeger UI. Here you can inspect detailed traces of Gemini CLI operations.
|
||||
|
||||
1. **Inspect logs and metrics**:
|
||||
The script redirects the OTEL collector output (which includes logs and metrics) to `~/.gemini/tmp/<projectHash>/otel/collector.log`. The script will provide links to view and a command to tail your telemetry data (traces, metrics, logs) locally.
|
||||
|
||||
1. **Stop the services**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector and Jaeger services.
|
||||
|
||||
### Google Cloud
|
||||
|
||||
Use the `npm run telemetry -- --target=gcp` command to automate setting up a local OpenTelemetry collector that forwards data to your Google Cloud project, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib`. To use it:
|
||||
|
||||
1. **Prerequisites**:
|
||||
- Have a Google Cloud project ID.
|
||||
- Export the `GOOGLE_CLOUD_PROJECT` environment variable to make it available to the OTEL collector.
|
||||
```bash
|
||||
export OTLP_GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
```
|
||||
- Authenticate with Google Cloud (e.g., run `gcloud auth application-default login` or ensure `GOOGLE_APPLICATION_CREDENTIALS` is set).
|
||||
- Ensure your Google Cloud account/service account has the necessary IAM roles: "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer".
|
||||
|
||||
1. **Run the command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Download the `otelcol-contrib` binary if needed.
|
||||
- Start an OTEL collector configured to receive data from 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.
|
||||
|
||||
1. **Run Gemini CLI:**
|
||||
In a separate terminal, run your Gemini CLI commands. This generates telemetry data that the collector captures.
|
||||
|
||||
1. **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.
|
||||
|
||||
1. **Inspect local collector logs**:
|
||||
The script redirects the local OTEL collector output to `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log`. The script provides links to view and command to tail your collector logs locally.
|
||||
|
||||
1. **Stop the service**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector.
|
||||
|
||||
## Logs and metric reference
|
||||
|
||||
The following section describes the structure of logs and metrics generated for Gemini CLI.
|
||||
The following section describes the structure of logs and metrics generated for
|
||||
Gemini CLI.
|
||||
|
||||
- A `sessionId` is included as a common attribute on all logs and metrics.
|
||||
|
||||
### Logs
|
||||
|
||||
Logs are timestamped records of specific events. The following events are logged for Gemini CLI:
|
||||
Logs are timestamped records of specific events. The following events are logged
|
||||
for Gemini CLI:
|
||||
|
||||
- `gemini_cli.config`: This event occurs once at startup with the CLI's configuration.
|
||||
- `gemini_cli.config`: This event occurs once at startup with the CLI's
|
||||
configuration.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `embedding_model` (string)
|
||||
@@ -182,7 +226,8 @@ Logs are timestamped records of specific events. The following events are logged
|
||||
- **Attributes**:
|
||||
- `prompt_length` (int)
|
||||
- `prompt_id` (string)
|
||||
- `prompt` (string, this attribute is excluded if `log_prompts_enabled` is configured to be `false`)
|
||||
- `prompt` (string, this attribute is excluded if `log_prompts_enabled` is
|
||||
configured to be `false`)
|
||||
- `auth_type` (string)
|
||||
|
||||
- `gemini_cli.tool_call`: This event occurs for each function call.
|
||||
@@ -191,7 +236,8 @@ Logs are timestamped records of specific events. The following events are logged
|
||||
- `function_args`
|
||||
- `duration_ms`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", "auto_accept", or "modify", if applicable)
|
||||
- `decision` (string: "accept", "reject", "auto_accept", or "modify", if
|
||||
applicable)
|
||||
- `error` (if applicable)
|
||||
- `error_type` (if applicable)
|
||||
- `content_length` (int, if applicable)
|
||||
|
||||
297
npm-shrinkwrap.json
generated
297
npm-shrinkwrap.json
generated
@@ -1235,6 +1235,145 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/common": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-5.0.2.tgz",
|
||||
"integrity": "sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google-cloud/projectify": "^4.0.0",
|
||||
"@google-cloud/promisify": "^4.0.0",
|
||||
"arrify": "^2.0.1",
|
||||
"duplexify": "^4.1.1",
|
||||
"extend": "^3.0.2",
|
||||
"google-auth-library": "^9.0.0",
|
||||
"html-entities": "^2.5.2",
|
||||
"retry-request": "^7.0.0",
|
||||
"teeny-request": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/logging": {
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-11.2.1.tgz",
|
||||
"integrity": "sha512-2h9HBJG3OAsvzXmb81qXmaTPfXYU7KJTQUxunoOKFGnY293YQ/eCkW1Y5mHLocwpEqeqQYT/Qvl6Tk+Q7PfStw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google-cloud/common": "^5.0.0",
|
||||
"@google-cloud/paginator": "^5.0.0",
|
||||
"@google-cloud/projectify": "^4.0.0",
|
||||
"@google-cloud/promisify": "4.0.0",
|
||||
"@opentelemetry/api": "^1.7.0",
|
||||
"arrify": "^2.0.1",
|
||||
"dot-prop": "^6.0.0",
|
||||
"eventid": "^2.0.0",
|
||||
"extend": "^3.0.2",
|
||||
"gcp-metadata": "^6.0.0",
|
||||
"google-auth-library": "^9.0.0",
|
||||
"google-gax": "^4.0.3",
|
||||
"on-finished": "^2.3.0",
|
||||
"pumpify": "^2.0.1",
|
||||
"stream-events": "^1.0.5",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/logging/node_modules/dot-prop": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
|
||||
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-obj": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-cloud-monitoring-exporter/-/opentelemetry-cloud-monitoring-exporter-0.21.0.tgz",
|
||||
"integrity": "sha512-+lAew44pWt6rA4l8dQ1gGhH7Uo95wZKfq/GBf9aEyuNDDLQ2XppGEEReu6ujesSqTtZ8ueQFt73+7SReSHbwqg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google-cloud/opentelemetry-resource-util": "^3.0.0",
|
||||
"@google-cloud/precise-date": "^4.0.0",
|
||||
"google-auth-library": "^9.0.0",
|
||||
"googleapis": "^137.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^2.0.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/opentelemetry-cloud-trace-exporter": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-cloud-trace-exporter/-/opentelemetry-cloud-trace-exporter-3.0.0.tgz",
|
||||
"integrity": "sha512-mUfLJBFo+ESbO0dAGboErx2VyZ7rbrHcQvTP99yH/J72dGaPbH2IzS+04TFbTbEd1VW5R9uK3xq2CqawQaG+1Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google-cloud/opentelemetry-resource-util": "^3.0.0",
|
||||
"@grpc/grpc-js": "^1.1.8",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"google-auth-library": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@opentelemetry/core": "^2.0.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/@grpc/proto-loader": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
|
||||
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.5.3",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/opentelemetry-resource-util": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-resource-util/-/opentelemetry-resource-util-3.0.0.tgz",
|
||||
"integrity": "sha512-CGR/lNzIfTKlZoZFfS6CkVzx+nsC9gzy6S8VcyaLegfEJbiPjxbMLP7csyhJTvZe/iRRcQJxSk0q8gfrGqD3/Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.22.0",
|
||||
"gcp-metadata": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/core": "^2.0.0",
|
||||
"@opentelemetry/resources": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/paginator": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
|
||||
@@ -1248,6 +1387,15 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/precise-date": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz",
|
||||
"integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/projectify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
|
||||
@@ -2657,6 +2805,24 @@
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resource-detector-gcp": {
|
||||
"version": "0.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.40.0.tgz",
|
||||
"integrity": "sha512-uAsUV8K4R9OJ3cgPUGYDqQByxOMTz4StmzJyofIv7+W+c1dTSEc1WVjWpTS2PAmywik++JlSmd8O4rMRJZpO8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "^2.0.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.27.0",
|
||||
"gcp-metadata": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resources": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||
@@ -3823,6 +3989,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/marked": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
||||
@@ -7763,6 +7935,27 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventid": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventid/-/eventid-2.0.1.tgz",
|
||||
"integrity": "sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"uuid": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/eventid/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||
@@ -8804,6 +8997,29 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/google-gax": {
|
||||
"version": "4.6.1",
|
||||
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz",
|
||||
"integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.10.9",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@types/long": "^4.0.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"duplexify": "^4.0.0",
|
||||
"google-auth-library": "^9.3.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"proto3-json-serializer": "^2.0.2",
|
||||
"protobufjs": "^7.3.2",
|
||||
"retry-request": "^7.0.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/google-logging-utils": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
|
||||
@@ -8813,6 +9029,36 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/googleapis": {
|
||||
"version": "137.1.0",
|
||||
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz",
|
||||
"integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^9.0.0",
|
||||
"googleapis-common": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/googleapis-common": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
|
||||
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"gaxios": "^6.0.3",
|
||||
"google-auth-library": "^9.7.0",
|
||||
"qs": "^6.7.0",
|
||||
"url-template": "^2.0.8",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -9896,6 +10142,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-obj": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
|
||||
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
|
||||
@@ -11493,6 +11748,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -12230,6 +12494,18 @@
|
||||
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/proto3-json-serializer": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
|
||||
"integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"protobufjs": "^7.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
|
||||
@@ -12290,6 +12566,17 @@
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pumpify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz",
|
||||
"integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"duplexify": "^4.1.1",
|
||||
"inherits": "^2.0.3",
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -15128,6 +15415,12 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-template": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -16451,6 +16744,9 @@
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.6.0-nightly",
|
||||
"dependencies": {
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
|
||||
"@google/genai": "1.16.0",
|
||||
"@joshua.litt/get-ripgrep": "^0.0.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
@@ -16462,6 +16758,7 @@
|
||||
"@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.203.0",
|
||||
"@opentelemetry/resource-detector-gcp": "^0.40.0",
|
||||
"@opentelemetry/sdk-node": "^0.203.0",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
|
||||
@@ -602,6 +602,7 @@ export async function loadCliConfig(
|
||||
),
|
||||
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
|
||||
outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile,
|
||||
useCollector: settings.telemetry?.useCollector,
|
||||
},
|
||||
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
|
||||
fileFiltering: settings.context?.fileFiltering,
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@google/genai": "1.16.0",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
"@joshua.litt/get-ripgrep": "^0.0.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
@@ -31,6 +34,7 @@
|
||||
"@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.203.0",
|
||||
"@opentelemetry/resource-detector-gcp": "^0.40.0",
|
||||
"@opentelemetry/sdk-node": "^0.203.0",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
|
||||
@@ -363,6 +363,33 @@ describe('Server Config (config.ts)', () => {
|
||||
expect(config.getTelemetryEnabled()).toBe(TELEMETRY_SETTINGS.enabled);
|
||||
});
|
||||
|
||||
it('Config constructor should set telemetry useCollector to true when provided', () => {
|
||||
const paramsWithTelemetry: ConfigParameters = {
|
||||
...baseParams,
|
||||
telemetry: { enabled: true, useCollector: true },
|
||||
};
|
||||
const config = new Config(paramsWithTelemetry);
|
||||
expect(config.getTelemetryUseCollector()).toBe(true);
|
||||
});
|
||||
|
||||
it('Config constructor should set telemetry useCollector to false when provided', () => {
|
||||
const paramsWithTelemetry: ConfigParameters = {
|
||||
...baseParams,
|
||||
telemetry: { enabled: true, useCollector: false },
|
||||
};
|
||||
const config = new Config(paramsWithTelemetry);
|
||||
expect(config.getTelemetryUseCollector()).toBe(false);
|
||||
});
|
||||
|
||||
it('Config constructor should default telemetry useCollector to false if not provided', () => {
|
||||
const paramsWithTelemetry: ConfigParameters = {
|
||||
...baseParams,
|
||||
telemetry: { enabled: true },
|
||||
};
|
||||
const config = new Config(paramsWithTelemetry);
|
||||
expect(config.getTelemetryUseCollector()).toBe(false);
|
||||
});
|
||||
|
||||
it('should have a getFileService method that returns FileDiscoveryService', () => {
|
||||
const config = new Config(baseParams);
|
||||
const fileService = config.getFileService();
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface TelemetrySettings {
|
||||
otlpProtocol?: 'grpc' | 'http';
|
||||
logPrompts?: boolean;
|
||||
outfile?: string;
|
||||
useCollector?: boolean;
|
||||
}
|
||||
|
||||
export interface OutputSettings {
|
||||
@@ -364,6 +365,7 @@ export class Config {
|
||||
otlpProtocol: params.telemetry?.otlpProtocol,
|
||||
logPrompts: params.telemetry?.logPrompts ?? true,
|
||||
outfile: params.telemetry?.outfile,
|
||||
useCollector: params.telemetry?.useCollector,
|
||||
};
|
||||
this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true;
|
||||
|
||||
@@ -708,6 +710,10 @@ export class Config {
|
||||
return this.telemetrySettings.outfile;
|
||||
}
|
||||
|
||||
getTelemetryUseCollector(): boolean {
|
||||
return this.telemetrySettings.useCollector ?? false;
|
||||
}
|
||||
|
||||
getGeminiClient(): GeminiClient {
|
||||
return this.geminiClient;
|
||||
}
|
||||
|
||||
405
packages/core/src/telemetry/gcp-exporters.test.ts
Normal file
405
packages/core/src/telemetry/gcp-exporters.test.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ExportResultCode } from '@opentelemetry/core';
|
||||
import type { ReadableLogRecord } from '@opentelemetry/sdk-logs';
|
||||
import {
|
||||
GcpTraceExporter,
|
||||
GcpMetricExporter,
|
||||
GcpLogExporter,
|
||||
} from './gcp-exporters.js';
|
||||
|
||||
const mockLogEntry = { test: 'entry' };
|
||||
const mockLogWrite = vi.fn().mockResolvedValue(undefined);
|
||||
const mockLog = {
|
||||
entry: vi.fn().mockReturnValue(mockLogEntry),
|
||||
write: mockLogWrite,
|
||||
};
|
||||
const mockLogging = {
|
||||
projectId: 'test-project',
|
||||
log: vi.fn().mockReturnValue(mockLog),
|
||||
};
|
||||
|
||||
vi.mock('@google-cloud/opentelemetry-cloud-trace-exporter', () => ({
|
||||
TraceExporter: vi.fn().mockImplementation(() => ({
|
||||
export: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
forceFlush: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@google-cloud/opentelemetry-cloud-monitoring-exporter', () => ({
|
||||
MetricExporter: vi.fn().mockImplementation(() => ({
|
||||
export: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
forceFlush: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@google-cloud/logging', () => ({
|
||||
Logging: vi.fn().mockImplementation(() => mockLogging),
|
||||
}));
|
||||
|
||||
describe('GCP Exporters', () => {
|
||||
describe('GcpTraceExporter', () => {
|
||||
it('should create a trace exporter with correct configuration', () => {
|
||||
const exporter = new GcpTraceExporter('test-project');
|
||||
expect(exporter).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a trace exporter without project ID', () => {
|
||||
const exporter = new GcpTraceExporter();
|
||||
expect(exporter).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GcpMetricExporter', () => {
|
||||
it('should create a metric exporter with correct configuration', () => {
|
||||
const exporter = new GcpMetricExporter('test-project');
|
||||
expect(exporter).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a metric exporter without project ID', () => {
|
||||
const exporter = new GcpMetricExporter();
|
||||
expect(exporter).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GcpLogExporter', () => {
|
||||
let exporter: GcpLogExporter;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLogWrite.mockResolvedValue(undefined);
|
||||
mockLog.entry.mockReturnValue(mockLogEntry);
|
||||
exporter = new GcpLogExporter('test-project');
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create a log exporter with project ID', () => {
|
||||
expect(exporter).toBeDefined();
|
||||
expect(mockLogging.log).toHaveBeenCalledWith('gemini_cli');
|
||||
});
|
||||
|
||||
it('should create a log exporter without project ID', () => {
|
||||
const exporterNoProject = new GcpLogExporter();
|
||||
expect(exporterNoProject).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('export', () => {
|
||||
it('should export logs successfully', async () => {
|
||||
const mockLogRecords: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
severityNumber: 9,
|
||||
severityText: 'INFO',
|
||||
body: 'Test log message',
|
||||
attributes: {
|
||||
'session.id': 'test-session',
|
||||
'custom.attribute': 'value',
|
||||
},
|
||||
resource: {
|
||||
attributes: {
|
||||
'service.name': 'test-service',
|
||||
},
|
||||
},
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
exporter.export(mockLogRecords, callback);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(mockLog.entry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
severity: 'INFO',
|
||||
timestamp: expect.any(Date),
|
||||
resource: {
|
||||
type: 'global',
|
||||
labels: {
|
||||
project_id: 'test-project',
|
||||
},
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
message: 'Test log message',
|
||||
session_id: 'test-session',
|
||||
'custom.attribute': 'value',
|
||||
'service.name': 'test-service',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockLog.write).toHaveBeenCalledWith([mockLogEntry]);
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
code: ExportResultCode.SUCCESS,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle export failures', async () => {
|
||||
const mockLogRecords: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
body: 'Test log message',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
const error = new Error('Write failed');
|
||||
mockLogWrite.mockRejectedValueOnce(error);
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
exporter.export(mockLogRecords, callback);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
code: ExportResultCode.FAILED,
|
||||
error,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle synchronous errors', () => {
|
||||
const mockLogRecords: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
body: 'Test log message',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
mockLog.entry.mockImplementation(() => {
|
||||
throw new Error('Entry creation failed');
|
||||
});
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
exporter.export(mockLogRecords, callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
code: ExportResultCode.FAILED,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('severity mapping', () => {
|
||||
it('should map OpenTelemetry severity numbers to Cloud Logging levels', () => {
|
||||
const testCases = [
|
||||
{ severityNumber: undefined, expected: 'DEFAULT' },
|
||||
{ severityNumber: 1, expected: 'DEFAULT' },
|
||||
{ severityNumber: 5, expected: 'DEBUG' },
|
||||
{ severityNumber: 9, expected: 'INFO' },
|
||||
{ severityNumber: 13, expected: 'WARNING' },
|
||||
{ severityNumber: 17, expected: 'ERROR' },
|
||||
{ severityNumber: 21, expected: 'CRITICAL' },
|
||||
{ severityNumber: 25, expected: 'CRITICAL' },
|
||||
];
|
||||
|
||||
testCases.forEach(({ severityNumber, expected }) => {
|
||||
const mockLogRecords: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
severityNumber,
|
||||
body: 'Test message',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
const callback = vi.fn();
|
||||
exporter.export(mockLogRecords, callback);
|
||||
|
||||
expect(mockLog.entry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
severity: expected,
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
mockLog.entry.mockClear();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('forceFlush', () => {
|
||||
it('should resolve immediately when no pending writes exist', async () => {
|
||||
await expect(exporter.forceFlush()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should wait for pending writes to complete', async () => {
|
||||
const mockLogRecords: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
body: 'Test log message',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
let resolveWrite: () => void;
|
||||
const writePromise = new Promise<void>((resolve) => {
|
||||
resolveWrite = resolve;
|
||||
});
|
||||
mockLogWrite.mockReturnValueOnce(writePromise);
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
exporter.export(mockLogRecords, callback);
|
||||
const flushPromise = exporter.forceFlush();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
|
||||
resolveWrite!();
|
||||
await writePromise;
|
||||
|
||||
await expect(flushPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle multiple pending writes', async () => {
|
||||
const mockLogRecords1: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
body: 'Test log message 1',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
const mockLogRecords2: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
body: 'Test log message 2',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
let resolveWrite1: () => void;
|
||||
let resolveWrite2: () => void;
|
||||
const writePromise1 = new Promise<void>((resolve) => {
|
||||
resolveWrite1 = resolve;
|
||||
});
|
||||
const writePromise2 = new Promise<void>((resolve) => {
|
||||
resolveWrite2 = resolve;
|
||||
});
|
||||
|
||||
mockLogWrite
|
||||
.mockReturnValueOnce(writePromise1)
|
||||
.mockReturnValueOnce(writePromise2);
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
exporter.export(mockLogRecords1, callback);
|
||||
exporter.export(mockLogRecords2, callback);
|
||||
|
||||
const flushPromise = exporter.forceFlush();
|
||||
|
||||
resolveWrite1!();
|
||||
await writePromise1;
|
||||
|
||||
resolveWrite2!();
|
||||
await writePromise2;
|
||||
|
||||
await expect(flushPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle write failures gracefully', async () => {
|
||||
const mockLogRecords: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
body: 'Test log message',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
const error = new Error('Write failed');
|
||||
mockLogWrite.mockRejectedValueOnce(error);
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
exporter.export(mockLogRecords, callback);
|
||||
|
||||
await expect(exporter.forceFlush()).resolves.toBeUndefined();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
code: ExportResultCode.FAILED,
|
||||
error,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdown', () => {
|
||||
it('should call forceFlush', async () => {
|
||||
const forceFlushSpy = vi.spyOn(exporter, 'forceFlush');
|
||||
|
||||
await exporter.shutdown();
|
||||
|
||||
expect(forceFlushSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle shutdown gracefully', async () => {
|
||||
const forceFlushSpy = vi.spyOn(exporter, 'forceFlush');
|
||||
|
||||
await expect(exporter.shutdown()).resolves.toBeUndefined();
|
||||
expect(forceFlushSpy).toHaveBeenCalled();
|
||||
});
|
||||
it('should wait for pending writes before shutting down', async () => {
|
||||
const mockLogRecords: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
body: 'Test log message',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
let resolveWrite: () => void;
|
||||
const writePromise = new Promise<void>((resolve) => {
|
||||
resolveWrite = resolve;
|
||||
});
|
||||
mockLogWrite.mockReturnValueOnce(writePromise);
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
exporter.export(mockLogRecords, callback);
|
||||
const shutdownPromise = exporter.shutdown();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
|
||||
resolveWrite!();
|
||||
await writePromise;
|
||||
|
||||
await expect(shutdownPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear pending writes array after shutdown', async () => {
|
||||
const mockLogRecords: ReadableLogRecord[] = [
|
||||
{
|
||||
hrTime: [1234567890, 123456789],
|
||||
hrTimeObserved: [1234567890, 123456789],
|
||||
body: 'Test log message',
|
||||
} as unknown as ReadableLogRecord,
|
||||
];
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
exporter.export(mockLogRecords, callback);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
await exporter.shutdown();
|
||||
|
||||
const start = Date.now();
|
||||
await exporter.forceFlush();
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(elapsed).toBeLessThan(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
132
packages/core/src/telemetry/gcp-exporters.ts
Normal file
132
packages/core/src/telemetry/gcp-exporters.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
|
||||
import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
|
||||
import { Logging } from '@google-cloud/logging';
|
||||
import type { Log } from '@google-cloud/logging';
|
||||
import { hrTimeToMilliseconds } from '@opentelemetry/core';
|
||||
import type { ExportResult } from '@opentelemetry/core';
|
||||
import { ExportResultCode } from '@opentelemetry/core';
|
||||
import type {
|
||||
ReadableLogRecord,
|
||||
LogRecordExporter,
|
||||
} from '@opentelemetry/sdk-logs';
|
||||
|
||||
/**
|
||||
* Google Cloud Trace exporter that extends the official trace exporter
|
||||
*/
|
||||
export class GcpTraceExporter extends TraceExporter {
|
||||
constructor(projectId?: string) {
|
||||
super({
|
||||
projectId,
|
||||
resourceFilter: /^gcp\./,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Cloud Monitoring exporter that extends the official metrics exporter
|
||||
*/
|
||||
export class GcpMetricExporter extends MetricExporter {
|
||||
constructor(projectId?: string) {
|
||||
super({
|
||||
projectId,
|
||||
prefix: 'custom.googleapis.com/gemini_cli',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Cloud Logging exporter that uses the Cloud Logging client
|
||||
*/
|
||||
export class GcpLogExporter implements LogRecordExporter {
|
||||
private logging: Logging;
|
||||
private log: Log;
|
||||
private pendingWrites: Array<Promise<void>> = [];
|
||||
|
||||
constructor(projectId?: string) {
|
||||
this.logging = new Logging({ projectId });
|
||||
this.log = this.logging.log('gemini_cli');
|
||||
}
|
||||
|
||||
export(
|
||||
logs: ReadableLogRecord[],
|
||||
resultCallback: (result: ExportResult) => void,
|
||||
): void {
|
||||
try {
|
||||
const entries = logs.map((log) => {
|
||||
const entry = this.log.entry(
|
||||
{
|
||||
severity: this.mapSeverityToCloudLogging(log.severityNumber),
|
||||
timestamp: new Date(hrTimeToMilliseconds(log.hrTime)),
|
||||
resource: {
|
||||
type: 'global',
|
||||
labels: {
|
||||
project_id: this.logging.projectId,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
session_id: log.attributes?.['session.id'],
|
||||
...log.attributes,
|
||||
...log.resource?.attributes,
|
||||
message: log.body,
|
||||
},
|
||||
);
|
||||
return entry;
|
||||
});
|
||||
|
||||
const writePromise = this.log
|
||||
.write(entries)
|
||||
.then(() => {
|
||||
resultCallback({ code: ExportResultCode.SUCCESS });
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
resultCallback({
|
||||
code: ExportResultCode.FAILED,
|
||||
error,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
const index = this.pendingWrites.indexOf(writePromise);
|
||||
if (index > -1) {
|
||||
this.pendingWrites.splice(index, 1);
|
||||
}
|
||||
});
|
||||
this.pendingWrites.push(writePromise);
|
||||
} catch (error) {
|
||||
resultCallback({
|
||||
code: ExportResultCode.FAILED,
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async forceFlush(): Promise<void> {
|
||||
if (this.pendingWrites.length > 0) {
|
||||
await Promise.all(this.pendingWrites);
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
await this.forceFlush();
|
||||
this.pendingWrites = [];
|
||||
}
|
||||
|
||||
private mapSeverityToCloudLogging(severityNumber?: number): string {
|
||||
if (!severityNumber) return 'DEFAULT';
|
||||
|
||||
// Map OpenTelemetry severity numbers to Cloud Logging severity levels
|
||||
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
||||
if (severityNumber >= 21) return 'CRITICAL';
|
||||
if (severityNumber >= 17) return 'ERROR';
|
||||
if (severityNumber >= 13) return 'WARNING';
|
||||
if (severityNumber >= 9) return 'INFO';
|
||||
if (severityNumber >= 5) return 'DEBUG';
|
||||
return 'DEFAULT';
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,11 @@ export {
|
||||
shutdownTelemetry,
|
||||
isTelemetrySdkInitialized,
|
||||
} from './sdk.js';
|
||||
export {
|
||||
GcpTraceExporter,
|
||||
GcpMetricExporter,
|
||||
GcpLogExporter,
|
||||
} from './gcp-exporters.js';
|
||||
export {
|
||||
logCliConfiguration,
|
||||
logUserPrompt,
|
||||
|
||||
@@ -14,6 +14,12 @@ import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/expor
|
||||
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
|
||||
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http';
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import {
|
||||
GcpTraceExporter,
|
||||
GcpLogExporter,
|
||||
GcpMetricExporter,
|
||||
} from './gcp-exporters.js';
|
||||
import { TelemetryTarget } from './index.js';
|
||||
|
||||
vi.mock('@opentelemetry/exporter-trace-otlp-grpc');
|
||||
vi.mock('@opentelemetry/exporter-logs-otlp-grpc');
|
||||
@@ -22,6 +28,7 @@ vi.mock('@opentelemetry/exporter-trace-otlp-http');
|
||||
vi.mock('@opentelemetry/exporter-logs-otlp-http');
|
||||
vi.mock('@opentelemetry/exporter-metrics-otlp-http');
|
||||
vi.mock('@opentelemetry/sdk-node');
|
||||
vi.mock('./gcp-exporters.js');
|
||||
|
||||
describe('Telemetry SDK', () => {
|
||||
let mockConfig: Config;
|
||||
@@ -32,6 +39,8 @@ describe('Telemetry SDK', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryOtlpEndpoint: () => 'http://localhost:4317',
|
||||
getTelemetryOtlpProtocol: () => 'grpc',
|
||||
getTelemetryTarget: () => 'local',
|
||||
getTelemetryUseCollector: () => false,
|
||||
getTelemetryOutfile: () => undefined,
|
||||
getDebugMode: () => false,
|
||||
getSessionId: () => 'test-session',
|
||||
@@ -101,4 +110,112 @@ describe('Telemetry SDK', () => {
|
||||
expect.objectContaining({ url: 'https://my-collector.com/' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use direct GCP exporters when target is gcp, project ID is set, and useCollector is false', () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');
|
||||
|
||||
const originalEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
|
||||
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = 'test-project';
|
||||
|
||||
try {
|
||||
initializeTelemetry(mockConfig);
|
||||
|
||||
expect(GcpTraceExporter).toHaveBeenCalledWith('test-project');
|
||||
expect(GcpLogExporter).toHaveBeenCalledWith('test-project');
|
||||
expect(GcpMetricExporter).toHaveBeenCalledWith('test-project');
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
} finally {
|
||||
if (originalEnv) {
|
||||
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalEnv;
|
||||
} else {
|
||||
delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should use OTLP exporters when target is gcp but useCollector is true', () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(true);
|
||||
|
||||
initializeTelemetry(mockConfig);
|
||||
|
||||
expect(OTLPTraceExporter).toHaveBeenCalledWith({
|
||||
url: 'http://localhost:4317',
|
||||
compression: 'gzip',
|
||||
});
|
||||
expect(OTLPLogExporter).toHaveBeenCalledWith({
|
||||
url: 'http://localhost:4317',
|
||||
compression: 'gzip',
|
||||
});
|
||||
expect(OTLPMetricExporter).toHaveBeenCalledWith({
|
||||
url: 'http://localhost:4317',
|
||||
compression: 'gzip',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use GCP exporters when project ID environment variable is not set', () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');
|
||||
|
||||
const originalOtlpEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
|
||||
const originalGoogleEnv = process.env['GOOGLE_CLOUD_PROJECT'];
|
||||
delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
|
||||
delete process.env['GOOGLE_CLOUD_PROJECT'];
|
||||
|
||||
try {
|
||||
initializeTelemetry(mockConfig);
|
||||
|
||||
expect(GcpTraceExporter).not.toHaveBeenCalled();
|
||||
expect(GcpLogExporter).not.toHaveBeenCalled();
|
||||
expect(GcpMetricExporter).not.toHaveBeenCalled();
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
} finally {
|
||||
if (originalOtlpEnv) {
|
||||
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalOtlpEnv;
|
||||
}
|
||||
if (originalGoogleEnv) {
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] = originalGoogleEnv;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should use GOOGLE_CLOUD_PROJECT as fallback when OTLP_GOOGLE_CLOUD_PROJECT is not set', () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');
|
||||
|
||||
const originalOtlpEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
|
||||
const originalGoogleEnv = process.env['GOOGLE_CLOUD_PROJECT'];
|
||||
delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] = 'fallback-project';
|
||||
|
||||
try {
|
||||
initializeTelemetry(mockConfig);
|
||||
|
||||
expect(GcpTraceExporter).toHaveBeenCalledWith('fallback-project');
|
||||
expect(GcpLogExporter).toHaveBeenCalledWith('fallback-project');
|
||||
expect(GcpMetricExporter).toHaveBeenCalledWith('fallback-project');
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
} finally {
|
||||
if (originalOtlpEnv) {
|
||||
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalOtlpEnv;
|
||||
}
|
||||
if (originalGoogleEnv) {
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] = originalGoogleEnv;
|
||||
} else {
|
||||
delete process.env['GOOGLE_CLOUD_PROJECT'];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,12 @@ import {
|
||||
FileMetricExporter,
|
||||
FileSpanExporter,
|
||||
} from './file-exporters.js';
|
||||
import {
|
||||
GcpTraceExporter,
|
||||
GcpMetricExporter,
|
||||
GcpLogExporter,
|
||||
} from './gcp-exporters.js';
|
||||
import { TelemetryTarget } from './index.js';
|
||||
|
||||
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
|
||||
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
|
||||
@@ -86,23 +92,40 @@ export function initializeTelemetry(config: Config): void {
|
||||
|
||||
const otlpEndpoint = config.getTelemetryOtlpEndpoint();
|
||||
const otlpProtocol = config.getTelemetryOtlpProtocol();
|
||||
const telemetryTarget = config.getTelemetryTarget();
|
||||
const useCollector = config.getTelemetryUseCollector();
|
||||
const parsedEndpoint = parseOtlpEndpoint(otlpEndpoint, otlpProtocol);
|
||||
const useOtlp = !!parsedEndpoint;
|
||||
const telemetryOutfile = config.getTelemetryOutfile();
|
||||
|
||||
const gcpProjectId =
|
||||
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] ||
|
||||
process.env['GOOGLE_CLOUD_PROJECT'];
|
||||
const useDirectGcpExport =
|
||||
telemetryTarget === TelemetryTarget.GCP && !!gcpProjectId && !useCollector;
|
||||
|
||||
let spanExporter:
|
||||
| OTLPTraceExporter
|
||||
| OTLPTraceExporterHttp
|
||||
| GcpTraceExporter
|
||||
| FileSpanExporter
|
||||
| ConsoleSpanExporter;
|
||||
let logExporter:
|
||||
| OTLPLogExporter
|
||||
| OTLPLogExporterHttp
|
||||
| GcpLogExporter
|
||||
| FileLogExporter
|
||||
| ConsoleLogRecordExporter;
|
||||
let metricReader: PeriodicExportingMetricReader;
|
||||
|
||||
if (useOtlp) {
|
||||
if (useDirectGcpExport) {
|
||||
spanExporter = new GcpTraceExporter(gcpProjectId);
|
||||
logExporter = new GcpLogExporter(gcpProjectId);
|
||||
metricReader = new PeriodicExportingMetricReader({
|
||||
exporter: new GcpMetricExporter(gcpProjectId),
|
||||
exportIntervalMillis: 30000,
|
||||
});
|
||||
} else if (useOtlp) {
|
||||
if (otlpProtocol === 'http') {
|
||||
spanExporter = new OTLPTraceExporterHttp({
|
||||
url: parsedEndpoint,
|
||||
|
||||
Reference in New Issue
Block a user