summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsmhendrickson <[email protected]>2025-07-23 17:48:24 -0400
committerGitHub <[email protected]>2025-07-23 21:48:24 +0000
commit9d3164621a8bb0e2bbf8d2309fba1d8678c0abe2 (patch)
tree0a70a72e6735380b327ee883711c27b5345de65b
parent209c8783b4f2cdc39e4ce1ac05b3416d32c373c5 (diff)
add `--telemetry-outfile` flag (#4689)
-rw-r--r--docs/telemetry.md11
-rw-r--r--packages/cli/src/config/config.ts6
-rw-r--r--packages/core/src/config/config.ts6
-rw-r--r--packages/core/src/telemetry/file-exporters.ts89
-rw-r--r--packages/core/src/telemetry/sdk.ts27
5 files changed, 133 insertions, 6 deletions
diff --git a/docs/telemetry.md b/docs/telemetry.md
index 76958794..2209ee0b 100644
--- a/docs/telemetry.md
+++ b/docs/telemetry.md
@@ -19,6 +19,7 @@ The following lists the precedence for applying telemetry settings, with items l
- `--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).
1. **Environment variables:**
- `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint`.
@@ -50,6 +51,16 @@ The following code can be added to your workspace (`.gemini/settings.json`) or u
}
```
+### Exporting to a file
+
+You can export all telemetry data to a file for local inspection.
+
+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`.
+
+```bash
+gemini --telemetry --telemetry-target=local --telemetry-outfile=/path/to/telemetry.log "your prompt"
+```
+
## Running an OTEL Collector
An OTEL Collector is a service that receives, processes, and exports telemetry data.
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index fd4907d0..ec84db52 100644
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -55,6 +55,7 @@ export interface CliArgs {
telemetryTarget: string | undefined;
telemetryOtlpEndpoint: string | undefined;
telemetryLogPrompts: boolean | undefined;
+ telemetryOutfile: string | undefined;
allowedMcpServerNames: string[] | undefined;
experimentalAcp: boolean | undefined;
extensions: string[] | undefined;
@@ -159,6 +160,10 @@ export async function parseArguments(): Promise<CliArgs> {
description:
'Enable or disable logging of user prompts for telemetry. Overrides settings files.',
})
+ .option('telemetry-outfile', {
+ type: 'string',
+ description: 'Redirect all telemetry output to the specified file.',
+ })
.option('checkpointing', {
alias: 'c',
type: 'boolean',
@@ -412,6 +417,7 @@ export async function loadCliConfig(
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
settings.telemetry?.otlpEndpoint,
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
+ outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile,
},
usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true,
// Git-aware file filtering settings
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 0e3171bf..d4427093 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -73,6 +73,7 @@ export interface TelemetrySettings {
target?: TelemetryTarget;
otlpEndpoint?: string;
logPrompts?: boolean;
+ outfile?: string;
}
export interface GeminiCLIExtension {
@@ -255,6 +256,7 @@ export class Config {
target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET,
otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT,
logPrompts: params.telemetry?.logPrompts ?? true,
+ outfile: params.telemetry?.outfile,
};
this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true;
@@ -468,6 +470,10 @@ export class Config {
return this.telemetrySettings.target ?? DEFAULT_TELEMETRY_TARGET;
}
+ getTelemetryOutfile(): string | undefined {
+ return this.telemetrySettings.outfile;
+ }
+
getGeminiClient(): GeminiClient {
return this.geminiClient;
}
diff --git a/packages/core/src/telemetry/file-exporters.ts b/packages/core/src/telemetry/file-exporters.ts
new file mode 100644
index 00000000..aee3dfd6
--- /dev/null
+++ b/packages/core/src/telemetry/file-exporters.ts
@@ -0,0 +1,89 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as fs from 'node:fs';
+import { ExportResult, ExportResultCode } from '@opentelemetry/core';
+import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
+import { ReadableLogRecord, LogRecordExporter } from '@opentelemetry/sdk-logs';
+import {
+ ResourceMetrics,
+ PushMetricExporter,
+ AggregationTemporality,
+} from '@opentelemetry/sdk-metrics';
+
+class FileExporter {
+ protected writeStream: fs.WriteStream;
+
+ constructor(filePath: string) {
+ this.writeStream = fs.createWriteStream(filePath, { flags: 'a' });
+ }
+
+ protected serialize(data: unknown): string {
+ return JSON.stringify(data, null, 2) + '\n';
+ }
+
+ shutdown(): Promise<void> {
+ return new Promise((resolve) => {
+ this.writeStream.end(resolve);
+ });
+ }
+}
+
+export class FileSpanExporter extends FileExporter implements SpanExporter {
+ export(
+ spans: ReadableSpan[],
+ resultCallback: (result: ExportResult) => void,
+ ): void {
+ const data = spans.map((span) => this.serialize(span)).join('');
+ this.writeStream.write(data, (err) => {
+ resultCallback({
+ code: err ? ExportResultCode.FAILED : ExportResultCode.SUCCESS,
+ error: err || undefined,
+ });
+ });
+ }
+}
+
+export class FileLogExporter extends FileExporter implements LogRecordExporter {
+ export(
+ logs: ReadableLogRecord[],
+ resultCallback: (result: ExportResult) => void,
+ ): void {
+ const data = logs.map((log) => this.serialize(log)).join('');
+ this.writeStream.write(data, (err) => {
+ resultCallback({
+ code: err ? ExportResultCode.FAILED : ExportResultCode.SUCCESS,
+ error: err || undefined,
+ });
+ });
+ }
+}
+
+export class FileMetricExporter
+ extends FileExporter
+ implements PushMetricExporter
+{
+ export(
+ metrics: ResourceMetrics,
+ resultCallback: (result: ExportResult) => void,
+ ): void {
+ const data = this.serialize(metrics);
+ this.writeStream.write(data, (err) => {
+ resultCallback({
+ code: err ? ExportResultCode.FAILED : ExportResultCode.SUCCESS,
+ error: err || undefined,
+ });
+ });
+ }
+
+ getPreferredAggregationTemporality(): AggregationTemporality {
+ return AggregationTemporality.CUMULATIVE;
+ }
+
+ async forceFlush(): Promise<void> {
+ return Promise.resolve();
+ }
+}
diff --git a/packages/core/src/telemetry/sdk.ts b/packages/core/src/telemetry/sdk.ts
index 83294651..1167750a 100644
--- a/packages/core/src/telemetry/sdk.ts
+++ b/packages/core/src/telemetry/sdk.ts
@@ -29,6 +29,11 @@ import { Config } from '../config/config.js';
import { SERVICE_NAME } from './constants.js';
import { initializeMetrics } from './metrics.js';
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
+import {
+ FileLogExporter,
+ FileMetricExporter,
+ FileSpanExporter,
+} from './file-exporters.js';
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
@@ -74,19 +79,24 @@ export function initializeTelemetry(config: Config): void {
const otlpEndpoint = config.getTelemetryOtlpEndpoint();
const grpcParsedEndpoint = parseGrpcEndpoint(otlpEndpoint);
const useOtlp = !!grpcParsedEndpoint;
+ const telemetryOutfile = config.getTelemetryOutfile();
const spanExporter = useOtlp
? new OTLPTraceExporter({
url: grpcParsedEndpoint,
compression: CompressionAlgorithm.GZIP,
})
- : new ConsoleSpanExporter();
+ : telemetryOutfile
+ ? new FileSpanExporter(telemetryOutfile)
+ : new ConsoleSpanExporter();
const logExporter = useOtlp
? new OTLPLogExporter({
url: grpcParsedEndpoint,
compression: CompressionAlgorithm.GZIP,
})
- : new ConsoleLogRecordExporter();
+ : telemetryOutfile
+ ? new FileLogExporter(telemetryOutfile)
+ : new ConsoleLogRecordExporter();
const metricReader = useOtlp
? new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
@@ -95,10 +105,15 @@ export function initializeTelemetry(config: Config): void {
}),
exportIntervalMillis: 10000,
})
- : new PeriodicExportingMetricReader({
- exporter: new ConsoleMetricExporter(),
- exportIntervalMillis: 10000,
- });
+ : telemetryOutfile
+ ? new PeriodicExportingMetricReader({
+ exporter: new FileMetricExporter(telemetryOutfile),
+ exportIntervalMillis: 10000,
+ })
+ : new PeriodicExportingMetricReader({
+ exporter: new ConsoleMetricExporter(),
+ exportIntervalMillis: 10000,
+ });
sdk = new NodeSDK({
resource,