mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 10:34:35 -07:00
feat(config): Support telemetry configuration via environment variables (#9113)
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
parseBooleanEnvFlag,
|
||||
parseTelemetryTargetValue,
|
||||
resolveTelemetrySettings,
|
||||
} from './config.js';
|
||||
import { TelemetryTarget } from './index.js';
|
||||
|
||||
describe('telemetry/config helpers', () => {
|
||||
describe('parseBooleanEnvFlag', () => {
|
||||
it('returns undefined for undefined', () => {
|
||||
expect(parseBooleanEnvFlag(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('parses true values', () => {
|
||||
expect(parseBooleanEnvFlag('true')).toBe(true);
|
||||
expect(parseBooleanEnvFlag('1')).toBe(true);
|
||||
});
|
||||
|
||||
it('parses false/other values as false', () => {
|
||||
expect(parseBooleanEnvFlag('false')).toBe(false);
|
||||
expect(parseBooleanEnvFlag('0')).toBe(false);
|
||||
expect(parseBooleanEnvFlag('TRUE')).toBe(false);
|
||||
expect(parseBooleanEnvFlag('random')).toBe(false);
|
||||
expect(parseBooleanEnvFlag('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTelemetryTargetValue', () => {
|
||||
it('parses string values', () => {
|
||||
expect(parseTelemetryTargetValue('local')).toBe(TelemetryTarget.LOCAL);
|
||||
expect(parseTelemetryTargetValue('gcp')).toBe(TelemetryTarget.GCP);
|
||||
});
|
||||
|
||||
it('accepts enum values', () => {
|
||||
expect(parseTelemetryTargetValue(TelemetryTarget.LOCAL)).toBe(
|
||||
TelemetryTarget.LOCAL,
|
||||
);
|
||||
expect(parseTelemetryTargetValue(TelemetryTarget.GCP)).toBe(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined for unknown', () => {
|
||||
expect(parseTelemetryTargetValue('other')).toBeUndefined();
|
||||
expect(parseTelemetryTargetValue(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTelemetrySettings', () => {
|
||||
it('falls back to settings when no argv/env provided', async () => {
|
||||
const settings = {
|
||||
enabled: false,
|
||||
target: TelemetryTarget.LOCAL,
|
||||
otlpEndpoint: 'http://localhost:4317',
|
||||
otlpProtocol: 'grpc' as const,
|
||||
logPrompts: false,
|
||||
outfile: 'settings.log',
|
||||
useCollector: false,
|
||||
};
|
||||
const resolved = await resolveTelemetrySettings({ settings });
|
||||
expect(resolved).toEqual(settings);
|
||||
});
|
||||
|
||||
it('uses env over settings and argv over env', async () => {
|
||||
const settings = {
|
||||
enabled: false,
|
||||
target: TelemetryTarget.LOCAL,
|
||||
otlpEndpoint: 'http://settings:4317',
|
||||
otlpProtocol: 'grpc' as const,
|
||||
logPrompts: false,
|
||||
outfile: 'settings.log',
|
||||
useCollector: false,
|
||||
};
|
||||
const env = {
|
||||
GEMINI_TELEMETRY_ENABLED: '1',
|
||||
GEMINI_TELEMETRY_TARGET: 'gcp',
|
||||
GEMINI_TELEMETRY_OTLP_ENDPOINT: 'http://env:4317',
|
||||
GEMINI_TELEMETRY_OTLP_PROTOCOL: 'http',
|
||||
GEMINI_TELEMETRY_LOG_PROMPTS: 'true',
|
||||
GEMINI_TELEMETRY_OUTFILE: 'env.log',
|
||||
GEMINI_TELEMETRY_USE_COLLECTOR: 'true',
|
||||
} as Record<string, string>;
|
||||
const argv = {
|
||||
telemetry: false,
|
||||
telemetryTarget: 'local',
|
||||
telemetryOtlpEndpoint: 'http://argv:4317',
|
||||
telemetryOtlpProtocol: 'grpc',
|
||||
telemetryLogPrompts: false,
|
||||
telemetryOutfile: 'argv.log',
|
||||
};
|
||||
|
||||
const resolvedEnv = await resolveTelemetrySettings({ env, settings });
|
||||
expect(resolvedEnv).toEqual({
|
||||
enabled: true,
|
||||
target: TelemetryTarget.GCP,
|
||||
otlpEndpoint: 'http://env:4317',
|
||||
otlpProtocol: 'http',
|
||||
logPrompts: true,
|
||||
outfile: 'env.log',
|
||||
useCollector: true,
|
||||
});
|
||||
|
||||
const resolvedArgv = await resolveTelemetrySettings({
|
||||
argv,
|
||||
env,
|
||||
settings,
|
||||
});
|
||||
expect(resolvedArgv).toEqual({
|
||||
enabled: false,
|
||||
target: TelemetryTarget.LOCAL,
|
||||
otlpEndpoint: 'http://argv:4317',
|
||||
otlpProtocol: 'grpc',
|
||||
logPrompts: false,
|
||||
outfile: 'argv.log',
|
||||
useCollector: true, // from env as no argv option
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to OTEL_EXPORTER_OTLP_ENDPOINT when GEMINI var is missing', async () => {
|
||||
const settings = {};
|
||||
const env = {
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: 'http://otel:4317',
|
||||
} as Record<string, string>;
|
||||
const resolved = await resolveTelemetrySettings({ env, settings });
|
||||
expect(resolved.otlpEndpoint).toBe('http://otel:4317');
|
||||
});
|
||||
|
||||
it('throws on unknown protocol values', async () => {
|
||||
const env = { GEMINI_TELEMETRY_OTLP_PROTOCOL: 'unknown' } as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
await expect(resolveTelemetrySettings({ env })).rejects.toThrow(
|
||||
/Invalid telemetry OTLP protocol/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws on unknown target values', async () => {
|
||||
const env = { GEMINI_TELEMETRY_TARGET: 'unknown' } as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
await expect(resolveTelemetrySettings({ env })).rejects.toThrow(
|
||||
/Invalid telemetry target/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { TelemetrySettings } from '../config/config.js';
|
||||
import { FatalConfigError } from '../utils/errors.js';
|
||||
import { TelemetryTarget } from './index.js';
|
||||
|
||||
/**
|
||||
* Parse a boolean environment flag. Accepts 'true'/'1' as true.
|
||||
*/
|
||||
export function parseBooleanEnvFlag(
|
||||
value: string | undefined,
|
||||
): boolean | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
return value === 'true' || value === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a telemetry target value into TelemetryTarget or undefined.
|
||||
*/
|
||||
export function parseTelemetryTargetValue(
|
||||
value: string | TelemetryTarget | undefined,
|
||||
): TelemetryTarget | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (value === TelemetryTarget.LOCAL || value === 'local') {
|
||||
return TelemetryTarget.LOCAL;
|
||||
}
|
||||
if (value === TelemetryTarget.GCP || value === 'gcp') {
|
||||
return TelemetryTarget.GCP;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface TelemetryArgOverrides {
|
||||
telemetry?: boolean;
|
||||
telemetryTarget?: string | TelemetryTarget;
|
||||
telemetryOtlpEndpoint?: string;
|
||||
telemetryOtlpProtocol?: string;
|
||||
telemetryLogPrompts?: boolean;
|
||||
telemetryOutfile?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build TelemetrySettings by resolving from argv (highest), env, then settings.
|
||||
*/
|
||||
export async function resolveTelemetrySettings(options: {
|
||||
argv?: TelemetryArgOverrides;
|
||||
env?: Record<string, string | undefined>;
|
||||
settings?: TelemetrySettings;
|
||||
}): Promise<TelemetrySettings> {
|
||||
const argv = options.argv ?? {};
|
||||
const env = options.env ?? {};
|
||||
const settings = options.settings ?? {};
|
||||
|
||||
const enabled =
|
||||
argv.telemetry ??
|
||||
parseBooleanEnvFlag(env['GEMINI_TELEMETRY_ENABLED']) ??
|
||||
settings.enabled;
|
||||
|
||||
const rawTarget =
|
||||
(argv.telemetryTarget as string | TelemetryTarget | undefined) ??
|
||||
env['GEMINI_TELEMETRY_TARGET'] ??
|
||||
(settings.target as string | TelemetryTarget | undefined);
|
||||
const target = parseTelemetryTargetValue(rawTarget);
|
||||
if (rawTarget !== undefined && target === undefined) {
|
||||
throw new FatalConfigError(
|
||||
`Invalid telemetry target: ${String(
|
||||
rawTarget,
|
||||
)}. Valid values are: local, gcp`,
|
||||
);
|
||||
}
|
||||
|
||||
const otlpEndpoint =
|
||||
argv.telemetryOtlpEndpoint ??
|
||||
env['GEMINI_TELEMETRY_OTLP_ENDPOINT'] ??
|
||||
env['OTEL_EXPORTER_OTLP_ENDPOINT'] ??
|
||||
settings.otlpEndpoint;
|
||||
|
||||
const rawProtocol =
|
||||
(argv.telemetryOtlpProtocol as string | undefined) ??
|
||||
env['GEMINI_TELEMETRY_OTLP_PROTOCOL'] ??
|
||||
settings.otlpProtocol;
|
||||
const otlpProtocol = (['grpc', 'http'] as const).find(
|
||||
(p) => p === rawProtocol,
|
||||
);
|
||||
if (rawProtocol !== undefined && otlpProtocol === undefined) {
|
||||
throw new FatalConfigError(
|
||||
`Invalid telemetry OTLP protocol: ${String(
|
||||
rawProtocol,
|
||||
)}. Valid values are: grpc, http`,
|
||||
);
|
||||
}
|
||||
|
||||
const logPrompts =
|
||||
argv.telemetryLogPrompts ??
|
||||
parseBooleanEnvFlag(env['GEMINI_TELEMETRY_LOG_PROMPTS']) ??
|
||||
settings.logPrompts;
|
||||
|
||||
const outfile =
|
||||
argv.telemetryOutfile ??
|
||||
env['GEMINI_TELEMETRY_OUTFILE'] ??
|
||||
settings.outfile;
|
||||
|
||||
const useCollector =
|
||||
parseBooleanEnvFlag(env['GEMINI_TELEMETRY_USE_COLLECTOR']) ??
|
||||
settings.useCollector;
|
||||
|
||||
return {
|
||||
enabled,
|
||||
target,
|
||||
otlpEndpoint,
|
||||
otlpProtocol,
|
||||
logPrompts,
|
||||
outfile,
|
||||
useCollector,
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,11 @@ export {
|
||||
shutdownTelemetry,
|
||||
isTelemetrySdkInitialized,
|
||||
} from './sdk.js';
|
||||
export {
|
||||
resolveTelemetrySettings,
|
||||
parseBooleanEnvFlag,
|
||||
parseTelemetryTargetValue,
|
||||
} from './config.js';
|
||||
export {
|
||||
GcpTraceExporter,
|
||||
GcpMetricExporter,
|
||||
|
||||
Reference in New Issue
Block a user