mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 02:54:31 -07:00
feat(telemetry): add flag for enabling traces specifically (#25343)
This commit is contained in:
+18
-11
@@ -35,17 +35,18 @@ The observability system provides:
|
||||
You control telemetry behavior through the `.gemini/settings.json` file.
|
||||
Environment variables can override these settings.
|
||||
|
||||
| Setting | Environment Variable | Description | Values | Default |
|
||||
| -------------- | -------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
|
||||
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
|
||||
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
|
||||
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
|
||||
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
|
||||
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
|
||||
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
|
||||
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
|
||||
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
|
||||
| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - |
|
||||
| Setting | Environment Variable | Description | Values | Default |
|
||||
| -------------- | --------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
|
||||
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
|
||||
| `traces` | `GEMINI_TELEMETRY_TRACES_ENABLED` | Enable detailed attribute tracing | `true`/`false` | `false` |
|
||||
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
|
||||
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
|
||||
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
|
||||
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
|
||||
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
|
||||
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
|
||||
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
|
||||
| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - |
|
||||
|
||||
**Note on boolean environment variables:** For boolean settings like `enabled`,
|
||||
setting the environment variable to `true` or `1` enables the feature.
|
||||
@@ -1235,6 +1236,12 @@ These metrics follow standard [OpenTelemetry GenAI semantic conventions].
|
||||
Traces provide an "under-the-hood" view of agent and backend operations. Use
|
||||
traces to debug tool interactions and optimize performance.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Detailed trace attributes (like full prompts and tool outputs) are disabled by default
|
||||
> to minimize overhead. You must explicitly set `telemetry.traces` to `true` (or set
|
||||
> `GEMINI_TELEMETRY_TRACES_ENABLED=true`) to capture them.
|
||||
|
||||
Every trace captures rich metadata via standard span attributes.
|
||||
|
||||
<details open>
|
||||
|
||||
@@ -2012,6 +2012,8 @@ see [Telemetry](../cli/telemetry.md).
|
||||
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): Whether or not telemetry is enabled.
|
||||
- **`traces`** (boolean): Whether detailed traces with large attributes (like
|
||||
tool outputs and file reads) are captured. Defaults to `false`.
|
||||
- **`target`** (string): The destination for collected telemetry. Supported
|
||||
values are `local` and `gcp`.
|
||||
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
|
||||
@@ -2212,6 +2214,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- Set to `true` or `1` to enable telemetry. Any other value is treated as
|
||||
disabling it.
|
||||
- Overrides the `telemetry.enabled` setting.
|
||||
- **`GEMINI_TELEMETRY_TRACES_ENABLED`**:
|
||||
- Set to `true` or `1` to enable detailed tracing with large attributes. Any
|
||||
other value is treated as disabling it.
|
||||
- Overrides the `telemetry.traces` setting.
|
||||
- **`GEMINI_TELEMETRY_TARGET`**:
|
||||
- Sets the telemetry target (`local` or `gcp`).
|
||||
- Overrides the `telemetry.target` setting.
|
||||
|
||||
@@ -70,6 +70,7 @@ describe('ACP telemetry', () => {
|
||||
GEMINI_API_KEY: 'fake-key',
|
||||
GEMINI_CLI_HOME: rig.homeDir!,
|
||||
GEMINI_TELEMETRY_ENABLED: 'true',
|
||||
GEMINI_TELEMETRY_TRACES_ENABLED: 'true',
|
||||
GEMINI_TELEMETRY_TARGET: 'local',
|
||||
GEMINI_TELEMETRY_OUTFILE: telemetryPath,
|
||||
},
|
||||
|
||||
@@ -98,6 +98,7 @@ export function createMockConfig(
|
||||
getMcpServers: vi.fn().mockReturnValue({}),
|
||||
}),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
getGitService: vi.fn(),
|
||||
validatePathAccess: vi.fn().mockReturnValue(undefined),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({
|
||||
|
||||
@@ -3060,6 +3060,11 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
description: 'Protocol for OTLP exporters.',
|
||||
enum: ['grpc', 'http'],
|
||||
},
|
||||
traces: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether detailed traces with large attributes are captured.',
|
||||
},
|
||||
logPrompts: {
|
||||
type: 'boolean',
|
||||
description: 'Whether prompts are logged in telemetry payloads.',
|
||||
|
||||
@@ -89,6 +89,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getAccessibility: vi.fn().mockReturnValue({}),
|
||||
getTelemetryEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryOtlpEndpoint: vi.fn().mockReturnValue(''),
|
||||
getTelemetryOtlpProtocol: vi.fn().mockReturnValue('grpc'),
|
||||
getTelemetryTarget: vi.fn().mockReturnValue(''),
|
||||
|
||||
@@ -194,6 +194,7 @@ class DelegateInvocation extends BaseToolInvocation<
|
||||
{
|
||||
operation: GeminiCliOperation.AgentCall,
|
||||
logPrompts: this.context.config.getTelemetryLogPromptsEnabled(),
|
||||
tracesEnabled: this.context.config.getTelemetryTracesEnabled(),
|
||||
sessionId: this.context.config.getSessionId(),
|
||||
attributes: {
|
||||
[GEN_AI_AGENT_NAME]: this.definition.name,
|
||||
|
||||
@@ -205,6 +205,7 @@ export interface PlanSettings {
|
||||
|
||||
export interface TelemetrySettings {
|
||||
enabled?: boolean;
|
||||
traces?: boolean;
|
||||
target?: TelemetryTarget;
|
||||
otlpEndpoint?: string;
|
||||
otlpProtocol?: 'grpc' | 'http';
|
||||
@@ -1061,6 +1062,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.accessibility = params.accessibility ?? {};
|
||||
this.telemetrySettings = {
|
||||
enabled: params.telemetry?.enabled ?? false,
|
||||
traces: params.telemetry?.traces ?? false,
|
||||
target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET,
|
||||
otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT,
|
||||
otlpProtocol: params.telemetry?.otlpProtocol,
|
||||
@@ -2732,6 +2734,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.telemetrySettings.enabled ?? false;
|
||||
}
|
||||
|
||||
getTelemetryTracesEnabled(): boolean {
|
||||
return this.telemetrySettings.traces ?? false;
|
||||
}
|
||||
|
||||
getTelemetryLogPromptsEnabled(): boolean {
|
||||
return this.telemetrySettings.logPrompts ?? true;
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ describe('GeminiChat', () => {
|
||||
promptId: 'test-session-id',
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getDebugMode: () => false,
|
||||
getContentGeneratorConfig: vi.fn().mockImplementation(() => ({
|
||||
|
||||
@@ -96,6 +96,7 @@ describe('GeminiChat Network Retries', () => {
|
||||
promptId: 'test-session-id',
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getDebugMode: () => false,
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
||||
|
||||
@@ -73,6 +73,7 @@ describe('LoggingContentGenerator', () => {
|
||||
authType: 'API_KEY',
|
||||
}),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
refreshUserQuotaIfStale: vi.fn().mockResolvedValue(undefined),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -361,6 +361,7 @@ export class LoggingContentGenerator implements ContentGenerator {
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
logPrompts: this.config.getTelemetryLogPromptsEnabled(),
|
||||
tracesEnabled: this.config.getTelemetryTracesEnabled(),
|
||||
sessionId: this.config.getSessionId(),
|
||||
attributes: {
|
||||
[GEN_AI_REQUEST_MODEL]: req.model,
|
||||
@@ -452,6 +453,7 @@ export class LoggingContentGenerator implements ContentGenerator {
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
logPrompts: this.config.getTelemetryLogPromptsEnabled(),
|
||||
tracesEnabled: this.config.getTelemetryTracesEnabled(),
|
||||
sessionId: this.config.getSessionId(),
|
||||
attributes: {
|
||||
[GEN_AI_REQUEST_MODEL]: req.model,
|
||||
@@ -607,6 +609,7 @@ export class LoggingContentGenerator implements ContentGenerator {
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
logPrompts: this.config.getTelemetryLogPromptsEnabled(),
|
||||
tracesEnabled: this.config.getTelemetryTracesEnabled(),
|
||||
sessionId: this.config.getSessionId(),
|
||||
attributes: {
|
||||
[GEN_AI_REQUEST_MODEL]: req.model,
|
||||
|
||||
@@ -905,6 +905,7 @@ describe('Plan Mode Denial Consistency', () => {
|
||||
getEnableHooks: vi.fn().mockReturnValue(false),
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.PLAN), // Key: Plan Mode
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
setApprovalMode: vi.fn(),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
|
||||
|
||||
@@ -178,6 +178,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
setApprovalMode: vi.fn(),
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -1517,6 +1518,7 @@ describe('Scheduler MCP Progress', () => {
|
||||
setApprovalMode: vi.fn(),
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ export class Scheduler {
|
||||
{
|
||||
operation: GeminiCliOperation.ScheduleToolCalls,
|
||||
logPrompts: this.context.config.getTelemetryLogPromptsEnabled(),
|
||||
tracesEnabled: this.context.config.getTelemetryTracesEnabled(),
|
||||
sessionId: this.context.config.getSessionId(),
|
||||
},
|
||||
async ({ metadata: spanMetadata }) => {
|
||||
|
||||
@@ -71,6 +71,7 @@ function createMockConfig(overrides: Partial<Config> = {}): Config {
|
||||
getEnableHooks: () => true,
|
||||
getExperiments: () => {},
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getPolicyEngine: () =>
|
||||
({
|
||||
check: async () => ({ decision: 'allow' }),
|
||||
|
||||
@@ -218,6 +218,7 @@ describe('Scheduler Parallel Execution', () => {
|
||||
setApprovalMode: vi.fn(),
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ export class ToolExecutor {
|
||||
{
|
||||
operation: GeminiCliOperation.ToolCall,
|
||||
logPrompts: this.config.getTelemetryLogPromptsEnabled(),
|
||||
tracesEnabled: this.config.getTelemetryTracesEnabled(),
|
||||
sessionId: this.config.getSessionId(),
|
||||
attributes: {
|
||||
[GEN_AI_TOOL_NAME]: toolName,
|
||||
|
||||
@@ -60,6 +60,10 @@ export async function resolveTelemetrySettings(options: {
|
||||
parseBooleanEnvFlag(env['GEMINI_TELEMETRY_ENABLED']) ??
|
||||
settings.enabled;
|
||||
|
||||
const traces =
|
||||
parseBooleanEnvFlag(env['GEMINI_TELEMETRY_TRACES_ENABLED']) ??
|
||||
settings.traces;
|
||||
|
||||
const rawTarget =
|
||||
argv.telemetryTarget ??
|
||||
env['GEMINI_TELEMETRY_TARGET'] ??
|
||||
@@ -110,6 +114,7 @@ export async function resolveTelemetrySettings(options: {
|
||||
|
||||
return {
|
||||
enabled,
|
||||
traces,
|
||||
target,
|
||||
otlpEndpoint,
|
||||
otlpProtocol,
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('conseca-logger', () => {
|
||||
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||
|
||||
@@ -216,6 +216,7 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
getFileFilteringAllowBuildArtifacts: () => false,
|
||||
getDebugMode: () => true,
|
||||
@@ -313,6 +314,7 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
@@ -352,6 +354,7 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
@@ -392,6 +395,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -493,10 +497,10 @@ describe('loggers', () => {
|
||||
'gen_ai.output.messages':
|
||||
'[{"finish_reason":"stop","role":"system","parts":[{"type":"text","content":"candidate 1"}]}]',
|
||||
'gen_ai.response.finish_reasons': ['stop'],
|
||||
'gen_ai.operation.name': 'generate_content',
|
||||
'gen_ai.response.model': 'test-model',
|
||||
'gen_ai.usage.input_tokens': 17,
|
||||
'gen_ai.usage.output_tokens': 50,
|
||||
'gen_ai.operation.name': 'generate_content',
|
||||
'gen_ai.output.type': 'text',
|
||||
'gen_ai.request.choice.count': 1,
|
||||
'gen_ai.request.seed': 678,
|
||||
@@ -564,6 +568,57 @@ describe('loggers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not log input and output messages when traces are disabled', () => {
|
||||
const mockConfigNoTraces = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false, // Disabled
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const event = new ApiResponseEvent(
|
||||
'test-model',
|
||||
100,
|
||||
{ prompt_id: 'prompt-id-1', contents: [] },
|
||||
{ candidates: [] },
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
'test-response',
|
||||
);
|
||||
|
||||
logApiResponse(mockConfigNoTraces, event);
|
||||
|
||||
expect(mockLogger.emit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: 'GenAI operation details from test-model. Status: 200. Duration: 100ms.',
|
||||
attributes: expect.objectContaining({
|
||||
'event.name': 'gen_ai.client.inference.operation.details',
|
||||
'gen_ai.operation.name': 'generate_content',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const emitCalls = mockLogger.emit.mock.calls;
|
||||
const detailsCall = emitCalls.find(
|
||||
(call) =>
|
||||
call[0].attributes &&
|
||||
call[0].attributes['event.name'] ===
|
||||
'gen_ai.client.inference.operation.details',
|
||||
);
|
||||
expect(
|
||||
detailsCall![0].attributes['gen_ai.input.messages'],
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
detailsCall![0].attributes['gen_ai.output.messages'],
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should log an API response with a role', () => {
|
||||
const event = new ApiResponseEvent(
|
||||
'test-model',
|
||||
@@ -596,6 +651,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -674,8 +730,6 @@ describe('loggers', () => {
|
||||
'gen_ai.request.temperature': 1,
|
||||
'gen_ai.request.top_p': 2,
|
||||
'gen_ai.request.top_k': 3,
|
||||
'gen_ai.input.messages':
|
||||
'[{"role":"user","parts":[{"type":"text","content":"Hello"}]}]',
|
||||
'gen_ai.operation.name': 'generate_content',
|
||||
'gen_ai.output.type': 'text',
|
||||
'gen_ai.request.choice.count': 1,
|
||||
@@ -683,6 +737,8 @@ describe('loggers', () => {
|
||||
'gen_ai.request.frequency_penalty': 10,
|
||||
'gen_ai.request.presence_penalty': 6,
|
||||
'gen_ai.request.max_tokens': 8000,
|
||||
'gen_ai.input.messages':
|
||||
'[{"role":"user","parts":[{"type":"text","content":"Hello"}]}]',
|
||||
'server.address': 'foo.com',
|
||||
'server.port': 8080,
|
||||
'gen_ai.request.stop_sequences': ['stop', 'please stop'],
|
||||
@@ -724,6 +780,52 @@ describe('loggers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not log input messages when traces are disabled', () => {
|
||||
const mockConfigNoTraces = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false, // Disabled
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const event = new ApiErrorEvent(
|
||||
'test-model',
|
||||
'error',
|
||||
100,
|
||||
{ prompt_id: 'prompt-id-1', contents: [] },
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
'ApiError',
|
||||
500,
|
||||
);
|
||||
|
||||
logApiError(mockConfigNoTraces, event);
|
||||
|
||||
expect(mockLogger.emit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attributes: expect.objectContaining({
|
||||
'event.name': 'gen_ai.client.inference.operation.details',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const emitCalls = mockLogger.emit.mock.calls;
|
||||
const detailsCall = emitCalls.find(
|
||||
(call) =>
|
||||
call[0].attributes &&
|
||||
call[0].attributes['event.name'] ===
|
||||
'gen_ai.client.inference.operation.details',
|
||||
);
|
||||
expect(
|
||||
detailsCall![0].attributes['gen_ai.input.messages'],
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should log an API error with a role', () => {
|
||||
const event = new ApiErrorEvent(
|
||||
'test-model',
|
||||
@@ -756,6 +858,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -833,7 +936,8 @@ describe('loggers', () => {
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true, // Enabled
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => true, // Enabled
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -922,7 +1026,8 @@ describe('loggers', () => {
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => false, // Disabled
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getTelemetryTracesEnabled: () => false, // Disabled
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -978,6 +1083,7 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -1140,6 +1246,7 @@ describe('loggers', () => {
|
||||
getCoreTools: () => ['ls', 'read-file'],
|
||||
getApprovalMode: () => 'default',
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
getFileFilteringAllowBuildArtifacts: () => false,
|
||||
getDebugMode: () => true,
|
||||
@@ -1170,6 +1277,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -1829,6 +1937,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -2423,6 +2532,7 @@ describe('loggers', () => {
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
|
||||
@@ -115,7 +115,11 @@ describe('runInDevTraceSpan', () => {
|
||||
const fn = vi.fn(async () => 'result');
|
||||
|
||||
const result = await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
fn,
|
||||
);
|
||||
|
||||
@@ -123,14 +127,22 @@ describe('runInDevTraceSpan', () => {
|
||||
expect(trace.getTracer).toHaveBeenCalled();
|
||||
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
||||
GeminiCliOperation.LLMCall,
|
||||
{},
|
||||
{
|
||||
attributes: {
|
||||
[GEN_AI_CONVERSATION_ID]: 'test-session-id',
|
||||
},
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set default attributes on the span metadata', async () => {
|
||||
await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
async ({ metadata }) => {
|
||||
expect(metadata.attributes[GEN_AI_OPERATION_NAME]).toBe(
|
||||
GeminiCliOperation.LLMCall,
|
||||
@@ -148,7 +160,11 @@ describe('runInDevTraceSpan', () => {
|
||||
|
||||
it('should set span attributes from metadata on completion', async () => {
|
||||
await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
async ({ metadata }) => {
|
||||
metadata.input = { query: 'hello' };
|
||||
metadata.output = { response: 'world' };
|
||||
@@ -175,7 +191,11 @@ describe('runInDevTraceSpan', () => {
|
||||
const error = new Error('test error');
|
||||
await expect(
|
||||
runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
async () => {
|
||||
throw error;
|
||||
},
|
||||
@@ -197,7 +217,11 @@ describe('runInDevTraceSpan', () => {
|
||||
}
|
||||
|
||||
const resultStream = await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
async () => testStream(),
|
||||
);
|
||||
|
||||
@@ -219,7 +243,11 @@ describe('runInDevTraceSpan', () => {
|
||||
}
|
||||
|
||||
const resultStream = await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
async () => testStream(),
|
||||
);
|
||||
|
||||
@@ -233,7 +261,11 @@ describe('runInDevTraceSpan', () => {
|
||||
}
|
||||
|
||||
const resultStream = await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
async () => testStream(),
|
||||
);
|
||||
|
||||
@@ -259,7 +291,11 @@ describe('runInDevTraceSpan', () => {
|
||||
}
|
||||
|
||||
const resultStream = await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
async () => errorStream(),
|
||||
);
|
||||
|
||||
@@ -278,7 +314,11 @@ describe('runInDevTraceSpan', () => {
|
||||
});
|
||||
|
||||
await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
{
|
||||
operation: GeminiCliOperation.LLMCall,
|
||||
sessionId: 'test-session-id',
|
||||
tracesEnabled: true,
|
||||
},
|
||||
async ({ metadata }) => {
|
||||
metadata.input = 'trigger error';
|
||||
},
|
||||
|
||||
@@ -125,10 +125,17 @@ export async function runInDevTraceSpan<R>(
|
||||
operation: GeminiCliOperation;
|
||||
logPrompts?: boolean;
|
||||
sessionId: string;
|
||||
tracesEnabled?: boolean;
|
||||
},
|
||||
fn: ({ metadata }: { metadata: SpanMetadata }) => Promise<R>,
|
||||
): Promise<R> {
|
||||
const { operation, logPrompts, sessionId, ...restOfSpanOpts } = opts;
|
||||
const { operation, logPrompts, sessionId, tracesEnabled, ...restOfSpanOpts } =
|
||||
opts;
|
||||
|
||||
restOfSpanOpts.attributes = {
|
||||
...restOfSpanOpts.attributes,
|
||||
[GEN_AI_CONVERSATION_ID]: sessionId,
|
||||
};
|
||||
|
||||
const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);
|
||||
return tracer.startActiveSpan(operation, restOfSpanOpts, async (span) => {
|
||||
@@ -148,24 +155,41 @@ export async function runInDevTraceSpan<R>(
|
||||
}
|
||||
spanEnded = true;
|
||||
try {
|
||||
if (logPrompts !== false) {
|
||||
if (meta.input !== undefined) {
|
||||
const truncated = truncateForTelemetry(meta.input);
|
||||
if (truncated !== undefined) {
|
||||
span.setAttribute(GEN_AI_INPUT_MESSAGES, truncated);
|
||||
if (tracesEnabled) {
|
||||
if (logPrompts !== false) {
|
||||
if (meta.input !== undefined) {
|
||||
const truncated = truncateForTelemetry(meta.input);
|
||||
if (truncated !== undefined) {
|
||||
span.setAttribute(GEN_AI_INPUT_MESSAGES, truncated);
|
||||
}
|
||||
}
|
||||
if (meta.output !== undefined) {
|
||||
const truncated = truncateForTelemetry(meta.output);
|
||||
if (truncated !== undefined) {
|
||||
span.setAttribute(GEN_AI_OUTPUT_MESSAGES, truncated);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (meta.output !== undefined) {
|
||||
const truncated = truncateForTelemetry(meta.output);
|
||||
for (const [key, value] of Object.entries(meta.attributes)) {
|
||||
const truncated = truncateForTelemetry(value);
|
||||
if (truncated !== undefined) {
|
||||
span.setAttribute(GEN_AI_OUTPUT_MESSAGES, truncated);
|
||||
span.setAttribute(key, truncated);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(meta.attributes)) {
|
||||
const truncated = truncateForTelemetry(value);
|
||||
if (truncated !== undefined) {
|
||||
span.setAttribute(key, truncated);
|
||||
} else {
|
||||
// Add basic attributes even when traces are disabled
|
||||
for (const [key, value] of Object.entries(meta.attributes)) {
|
||||
if (
|
||||
key === GEN_AI_OPERATION_NAME ||
|
||||
key === GEN_AI_AGENT_NAME ||
|
||||
key === GEN_AI_AGENT_DESCRIPTION ||
|
||||
key === GEN_AI_CONVERSATION_ID
|
||||
) {
|
||||
const truncated = truncateForTelemetry(value);
|
||||
if (truncated !== undefined) {
|
||||
span.setAttribute(key, truncated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (meta.error) {
|
||||
|
||||
@@ -387,6 +387,13 @@ export class ToolCallEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
|
||||
export const EVENT_API_REQUEST = 'gemini_cli.api_request';
|
||||
|
||||
function shouldIncludePayloads(config: Config): boolean {
|
||||
return (
|
||||
config.getTelemetryTracesEnabled() && config.getTelemetryLogPromptsEnabled()
|
||||
);
|
||||
}
|
||||
|
||||
export class ApiRequestEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'api_request';
|
||||
'event.timestamp': string;
|
||||
@@ -443,7 +450,7 @@ export class ApiRequestEvent implements BaseTelemetryEvent {
|
||||
attributes['server.port'] = this.prompt.server.port;
|
||||
}
|
||||
|
||||
if (config.getTelemetryLogPromptsEnabled() && this.prompt.contents) {
|
||||
if (shouldIncludePayloads(config) && this.prompt.contents) {
|
||||
attributes['gen_ai.input.messages'] = JSON.stringify(
|
||||
toInputMessages(this.prompt.contents),
|
||||
);
|
||||
@@ -540,7 +547,7 @@ export class ApiErrorEvent implements BaseTelemetryEvent {
|
||||
attributes['server.port'] = this.prompt.server.port;
|
||||
}
|
||||
|
||||
if (config.getTelemetryLogPromptsEnabled() && this.prompt.contents) {
|
||||
if (shouldIncludePayloads(config) && this.prompt.contents) {
|
||||
attributes['gen_ai.input.messages'] = JSON.stringify(
|
||||
toInputMessages(this.prompt.contents),
|
||||
);
|
||||
@@ -707,9 +714,13 @@ export class ApiResponseEvent implements BaseTelemetryEvent {
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
'gen_ai.response.id': this.response.response_id,
|
||||
'gen_ai.response.finish_reasons': this.finish_reasons,
|
||||
'gen_ai.output.messages': JSON.stringify(
|
||||
toOutputMessages(this.response.candidates),
|
||||
),
|
||||
...(shouldIncludePayloads(config)
|
||||
? {
|
||||
'gen_ai.output.messages': JSON.stringify(
|
||||
toOutputMessages(this.response.candidates),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...toGenerateContentConfigAttributes(this.prompt.generate_content_config),
|
||||
...getConventionAttributes(this),
|
||||
};
|
||||
@@ -719,7 +730,7 @@ export class ApiResponseEvent implements BaseTelemetryEvent {
|
||||
attributes['server.port'] = this.prompt.server.port;
|
||||
}
|
||||
|
||||
if (config.getTelemetryLogPromptsEnabled() && this.prompt.contents) {
|
||||
if (shouldIncludePayloads(config) && this.prompt.contents) {
|
||||
attributes['gen_ai.input.messages'] = JSON.stringify(
|
||||
toInputMessages(this.prompt.contents),
|
||||
);
|
||||
|
||||
@@ -3620,6 +3620,10 @@
|
||||
"description": "Protocol for OTLP exporters.",
|
||||
"enum": ["grpc", "http"]
|
||||
},
|
||||
"traces": {
|
||||
"type": "boolean",
|
||||
"description": "Whether detailed traces with large attributes are captured."
|
||||
},
|
||||
"logPrompts": {
|
||||
"type": "boolean",
|
||||
"description": "Whether prompts are logged in telemetry payloads."
|
||||
|
||||
Reference in New Issue
Block a user