add `--telemetry-outfile` flag (#4689)
This commit is contained in:
parent
209c8783b4
commit
9d3164621a
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,19 +79,24 @@ 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,
|
||||||
})
|
})
|
||||||
: new ConsoleSpanExporter();
|
: telemetryOutfile
|
||||||
|
? new FileSpanExporter(telemetryOutfile)
|
||||||
|
: new ConsoleSpanExporter();
|
||||||
const logExporter = useOtlp
|
const logExporter = useOtlp
|
||||||
? new OTLPLogExporter({
|
? new OTLPLogExporter({
|
||||||
url: grpcParsedEndpoint,
|
url: grpcParsedEndpoint,
|
||||||
compression: CompressionAlgorithm.GZIP,
|
compression: CompressionAlgorithm.GZIP,
|
||||||
})
|
})
|
||||||
: new ConsoleLogRecordExporter();
|
: telemetryOutfile
|
||||||
|
? new FileLogExporter(telemetryOutfile)
|
||||||
|
: new ConsoleLogRecordExporter();
|
||||||
const metricReader = useOtlp
|
const metricReader = useOtlp
|
||||||
? new PeriodicExportingMetricReader({
|
? new PeriodicExportingMetricReader({
|
||||||
exporter: new OTLPMetricExporter({
|
exporter: new OTLPMetricExporter({
|
||||||
|
@ -95,10 +105,15 @@ export function initializeTelemetry(config: Config): void {
|
||||||
}),
|
}),
|
||||||
exportIntervalMillis: 10000,
|
exportIntervalMillis: 10000,
|
||||||
})
|
})
|
||||||
: new PeriodicExportingMetricReader({
|
: telemetryOutfile
|
||||||
exporter: new ConsoleMetricExporter(),
|
? new PeriodicExportingMetricReader({
|
||||||
exportIntervalMillis: 10000,
|
exporter: new FileMetricExporter(telemetryOutfile),
|
||||||
});
|
exportIntervalMillis: 10000,
|
||||||
|
})
|
||||||
|
: new PeriodicExportingMetricReader({
|
||||||
|
exporter: new ConsoleMetricExporter(),
|
||||||
|
exportIntervalMillis: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
sdk = new NodeSDK({
|
sdk = new NodeSDK({
|
||||||
resource,
|
resource,
|
||||||
|
|
Loading…
Reference in New Issue