feat(config): Support telemetry configuration via environment variables (#9113)

This commit is contained in:
Jerop Kipruto
2025-09-23 03:23:06 +09:00
committed by GitHub
parent 10392ad344
commit c0c7ad10ca
7 changed files with 479 additions and 29 deletions
+155
View File
@@ -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,
);
});
});
});
+120
View File
@@ -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,
};
}
+5
View File
@@ -18,6 +18,11 @@ export {
shutdownTelemetry,
isTelemetrySdkInitialized,
} from './sdk.js';
export {
resolveTelemetrySettings,
parseBooleanEnvFlag,
parseTelemetryTargetValue,
} from './config.js';
export {
GcpTraceExporter,
GcpMetricExporter,