add `--telemetry-outfile` flag (#4689)

This commit is contained in:
smhendrickson 2025-07-23 17:48:24 -04:00 committed by GitHub
parent 209c8783b4
commit 9d3164621a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 133 additions and 6 deletions

View File

@ -19,6 +19,7 @@ The following lists the precedence for applying telemetry settings, with items l
- `--telemetry-target <local|gcp>`: Overrides `telemetry.target`. - `--telemetry-target <local|gcp>`: Overrides `telemetry.target`.
- `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`. - `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`.
- `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`. - `--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:** 1. **Environment variables:**
- `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint`. - `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 ## Running an OTEL Collector
An OTEL Collector is a service that receives, processes, and exports telemetry data. An OTEL Collector is a service that receives, processes, and exports telemetry data.

View File

@ -55,6 +55,7 @@ export interface CliArgs {
telemetryTarget: string | undefined; telemetryTarget: string | undefined;
telemetryOtlpEndpoint: string | undefined; telemetryOtlpEndpoint: string | undefined;
telemetryLogPrompts: boolean | undefined; telemetryLogPrompts: boolean | undefined;
telemetryOutfile: string | undefined;
allowedMcpServerNames: string[] | undefined; allowedMcpServerNames: string[] | undefined;
experimentalAcp: boolean | undefined; experimentalAcp: boolean | undefined;
extensions: string[] | undefined; extensions: string[] | undefined;
@ -159,6 +160,10 @@ export async function parseArguments(): Promise<CliArgs> {
description: description:
'Enable or disable logging of user prompts for telemetry. Overrides settings files.', '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', { .option('checkpointing', {
alias: 'c', alias: 'c',
type: 'boolean', type: 'boolean',
@ -412,6 +417,7 @@ export async function loadCliConfig(
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
settings.telemetry?.otlpEndpoint, settings.telemetry?.otlpEndpoint,
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts, logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile,
}, },
usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true, usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true,
// Git-aware file filtering settings // Git-aware file filtering settings

View File

@ -73,6 +73,7 @@ export interface TelemetrySettings {
target?: TelemetryTarget; target?: TelemetryTarget;
otlpEndpoint?: string; otlpEndpoint?: string;
logPrompts?: boolean; logPrompts?: boolean;
outfile?: string;
} }
export interface GeminiCLIExtension { export interface GeminiCLIExtension {
@ -255,6 +256,7 @@ export class Config {
target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET, target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET,
otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT, otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT,
logPrompts: params.telemetry?.logPrompts ?? true, logPrompts: params.telemetry?.logPrompts ?? true,
outfile: params.telemetry?.outfile,
}; };
this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true; this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true;
@ -468,6 +470,10 @@ export class Config {
return this.telemetrySettings.target ?? DEFAULT_TELEMETRY_TARGET; return this.telemetrySettings.target ?? DEFAULT_TELEMETRY_TARGET;
} }
getTelemetryOutfile(): string | undefined {
return this.telemetrySettings.outfile;
}
getGeminiClient(): GeminiClient { getGeminiClient(): GeminiClient {
return this.geminiClient; return this.geminiClient;
} }

View File

@ -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();
}
}

View File

@ -29,6 +29,11 @@ import { Config } from '../config/config.js';
import { SERVICE_NAME } from './constants.js'; import { SERVICE_NAME } from './constants.js';
import { initializeMetrics } from './metrics.js'; import { initializeMetrics } from './metrics.js';
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.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 // For troubleshooting, set the log level to DiagLogLevel.DEBUG
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
@ -74,18 +79,23 @@ export function initializeTelemetry(config: Config): void {
const otlpEndpoint = config.getTelemetryOtlpEndpoint(); const otlpEndpoint = config.getTelemetryOtlpEndpoint();
const grpcParsedEndpoint = parseGrpcEndpoint(otlpEndpoint); const grpcParsedEndpoint = parseGrpcEndpoint(otlpEndpoint);
const useOtlp = !!grpcParsedEndpoint; const useOtlp = !!grpcParsedEndpoint;
const telemetryOutfile = config.getTelemetryOutfile();
const spanExporter = useOtlp const spanExporter = useOtlp
? new OTLPTraceExporter({ ? new OTLPTraceExporter({
url: grpcParsedEndpoint, url: grpcParsedEndpoint,
compression: CompressionAlgorithm.GZIP, compression: CompressionAlgorithm.GZIP,
}) })
: telemetryOutfile
? new FileSpanExporter(telemetryOutfile)
: new ConsoleSpanExporter(); : new ConsoleSpanExporter();
const logExporter = useOtlp const logExporter = useOtlp
? new OTLPLogExporter({ ? new OTLPLogExporter({
url: grpcParsedEndpoint, url: grpcParsedEndpoint,
compression: CompressionAlgorithm.GZIP, compression: CompressionAlgorithm.GZIP,
}) })
: telemetryOutfile
? new FileLogExporter(telemetryOutfile)
: new ConsoleLogRecordExporter(); : new ConsoleLogRecordExporter();
const metricReader = useOtlp const metricReader = useOtlp
? new PeriodicExportingMetricReader({ ? new PeriodicExportingMetricReader({
@ -95,6 +105,11 @@ export function initializeTelemetry(config: Config): void {
}), }),
exportIntervalMillis: 10000, exportIntervalMillis: 10000,
}) })
: telemetryOutfile
? new PeriodicExportingMetricReader({
exporter: new FileMetricExporter(telemetryOutfile),
exportIntervalMillis: 10000,
})
: new PeriodicExportingMetricReader({ : new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(), exporter: new ConsoleMetricExporter(),
exportIntervalMillis: 10000, exportIntervalMillis: 10000,