mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 12:57:12 -07:00
fix(telemetry): implement bounded structural truncation and decouple traces
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -2966,6 +2966,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.',
|
||||
|
||||
@@ -241,6 +241,7 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
getCoreTools: vi.fn(() => []),
|
||||
getTelemetryEnabled: vi.fn(() => false),
|
||||
getTelemetryLogPromptsEnabled: vi.fn(() => false),
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getFileFilteringRespectGitIgnore: vi.fn(() => true),
|
||||
getOutputFormat: vi.fn(() => 'text'),
|
||||
getUsageStatisticsEnabled: vi.fn(() => false),
|
||||
@@ -325,6 +326,7 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
getCoreTools: vi.fn(() => []),
|
||||
getTelemetryEnabled: vi.fn(() => false),
|
||||
getTelemetryLogPromptsEnabled: vi.fn(() => false),
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getFileFilteringRespectGitIgnore: vi.fn(() => true),
|
||||
getOutputFormat: vi.fn(() => 'text'),
|
||||
getUsageStatisticsEnabled: vi.fn(() => false),
|
||||
|
||||
@@ -87,6 +87,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(''),
|
||||
|
||||
@@ -308,6 +308,8 @@ describe('useGeminiStream', () => {
|
||||
sandbox: false,
|
||||
targetDir: '/test/dir',
|
||||
debugMode: false,
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
question: undefined,
|
||||
coreTools: [],
|
||||
toolDiscoveryCommand: undefined,
|
||||
|
||||
@@ -1559,6 +1559,8 @@ export const useGeminiStream = (
|
||||
operation: options?.isContinuation
|
||||
? GeminiCliOperation.SystemPrompt
|
||||
: GeminiCliOperation.UserPrompt,
|
||||
logPrompts: config.getTelemetryLogPromptsEnabled(),
|
||||
tracesEnabled: config.getTelemetryTracesEnabled(),
|
||||
sessionId: config.getSessionId(),
|
||||
},
|
||||
async ({ metadata: spanMetadata }) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -202,6 +202,7 @@ export interface PlanSettings {
|
||||
|
||||
export interface TelemetrySettings {
|
||||
enabled?: boolean;
|
||||
traces?: boolean;
|
||||
target?: TelemetryTarget;
|
||||
otlpEndpoint?: string;
|
||||
otlpProtocol?: 'grpc' | 'http';
|
||||
@@ -1050,6 +1051,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,
|
||||
@@ -2660,6 +2662,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: () => 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,
|
||||
|
||||
@@ -858,6 +858,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: () => 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: () => 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: () => 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: () => 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: () => 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: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -596,6 +600,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -756,6 +761,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -834,6 +840,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true, // Enabled
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -923,6 +930,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => false, // Disabled
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -978,6 +986,7 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -1140,6 +1149,7 @@ describe('loggers', () => {
|
||||
getCoreTools: () => ['ls', 'read-file'],
|
||||
getApprovalMode: () => 'default',
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
getFileFilteringAllowBuildArtifacts: () => false,
|
||||
getDebugMode: () => true,
|
||||
@@ -1170,6 +1180,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -1829,6 +1840,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
@@ -2423,6 +2435,7 @@ describe('loggers', () => {
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { Config } from '../config/config.js';
|
||||
function createMockConfig(logPromptsEnabled: boolean): Config {
|
||||
return {
|
||||
getTelemetryLogPromptsEnabled: () => logPromptsEnabled,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getSessionId: () => 'test-session-id',
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
|
||||
@@ -62,13 +62,28 @@ describe('truncateForTelemetry', () => {
|
||||
expect(result).toBe('👋🌍...[TRUNCATED: original length 10]');
|
||||
});
|
||||
|
||||
it('should stringify and truncate objects if exceeding maxLength', () => {
|
||||
it('should stringify and structurally truncate objects if exceeding limits', () => {
|
||||
const obj = { message: 'hello world', nested: { a: 1 } };
|
||||
const stringified = JSON.stringify(obj);
|
||||
const result = truncateForTelemetry(obj, 10);
|
||||
expect(result).toBe(
|
||||
stringified.substring(0, 10) +
|
||||
`...[TRUNCATED: original length ${stringified.length}]`,
|
||||
JSON.stringify({
|
||||
message: 'hello worl...[TRUNCATED: original length 11]',
|
||||
nested: { a: 1 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should structurally truncate arrays and depth', () => {
|
||||
const obj = {
|
||||
arr: [1, 2, 3],
|
||||
deep: { level1: { level2: { level3: { a: 1 } } } },
|
||||
};
|
||||
const result = truncateForTelemetry(obj, 100, 2, 3);
|
||||
expect(result).toBe(
|
||||
JSON.stringify({
|
||||
arr: [1, 2, '[TRUNCATED: Array of length 3]'],
|
||||
deep: { level1: { level2: '[TRUNCATED: Max Depth Reached]' } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
import { truncateString } from '../utils/textUtils.js';
|
||||
import {
|
||||
GEN_AI_AGENT_DESCRIPTION,
|
||||
GEN_AI_AGENT_NAME,
|
||||
@@ -54,27 +53,78 @@ export const spanRegistry = new FinalizationRegistry((endSpan: () => void) => {
|
||||
*/
|
||||
export function truncateForTelemetry(
|
||||
value: unknown,
|
||||
maxLength = 10000,
|
||||
maxStringLength = 10000,
|
||||
maxArrayLength = 100,
|
||||
maxDepth = 4,
|
||||
): AttributeValue | undefined {
|
||||
if (typeof value === 'string') {
|
||||
return truncateString(
|
||||
value,
|
||||
maxLength,
|
||||
`...[TRUNCATED: original length ${value.length}]`,
|
||||
) as AttributeValue;
|
||||
const truncateObj = (v: unknown, depth: number): unknown => {
|
||||
if (typeof v === 'string') {
|
||||
const graphemes = Array.from(v);
|
||||
if (graphemes.length > maxStringLength) {
|
||||
return (
|
||||
graphemes.slice(0, maxStringLength).join('') +
|
||||
`...[TRUNCATED: original length ${graphemes.length}]`
|
||||
);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
if (
|
||||
typeof v === 'number' ||
|
||||
typeof v === 'boolean' ||
|
||||
v === null ||
|
||||
v === undefined
|
||||
) {
|
||||
return v;
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
if (depth >= maxDepth) {
|
||||
return `[TRUNCATED: Max Depth Reached]`;
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
if (v.length > maxArrayLength) {
|
||||
const truncatedArray = v
|
||||
.slice(0, maxArrayLength)
|
||||
.map((item) => truncateObj(item, depth + 1));
|
||||
truncatedArray.push(`[TRUNCATED: Array of length ${v.length}]`);
|
||||
return truncatedArray;
|
||||
}
|
||||
return v.map((item) => truncateObj(item, depth + 1));
|
||||
}
|
||||
|
||||
const newObj: Record<string, unknown> = {};
|
||||
let numKeys = 0;
|
||||
const MAX_KEYS = 100;
|
||||
for (const key in v) {
|
||||
if (!Object.prototype.hasOwnProperty.call(v, key)) continue;
|
||||
if (numKeys >= MAX_KEYS) {
|
||||
newObj['__truncated'] = `[TRUNCATED: Object with >${MAX_KEYS} keys]`;
|
||||
break;
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(v, key);
|
||||
if (descriptor) {
|
||||
newObj[key] = truncateObj(descriptor.value, depth + 1);
|
||||
}
|
||||
numKeys++;
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const truncated = truncateObj(value, 0);
|
||||
|
||||
if (
|
||||
typeof truncated === 'string' ||
|
||||
typeof truncated === 'number' ||
|
||||
typeof truncated === 'boolean'
|
||||
) {
|
||||
return truncated as AttributeValue;
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const stringified = safeJsonStringify(value);
|
||||
return truncateString(
|
||||
stringified,
|
||||
maxLength,
|
||||
`...[TRUNCATED: original length ${stringified.length}]`,
|
||||
) as AttributeValue;
|
||||
if (truncated === null || truncated === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value as AttributeValue;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
return safeJsonStringify(truncated) as AttributeValue;
|
||||
}
|
||||
|
||||
function isAsyncIterable<T>(value: T): value is T & AsyncIterable<unknown> {
|
||||
@@ -125,13 +175,14 @@ 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;
|
||||
|
||||
const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);
|
||||
return tracer.startActiveSpan(operation, restOfSpanOpts, async (span) => {
|
||||
if (tracesEnabled === false) {
|
||||
const meta: SpanMetadata = {
|
||||
name: operation,
|
||||
attributes: {
|
||||
@@ -141,87 +192,113 @@ export async function runInDevTraceSpan<R>(
|
||||
[GEN_AI_CONVERSATION_ID]: sessionId,
|
||||
},
|
||||
};
|
||||
let spanEnded = false;
|
||||
const endSpan = () => {
|
||||
if (spanEnded) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
return fn({ metadata: meta });
|
||||
}
|
||||
|
||||
const spanOptsWithSession: SpanOptions = {
|
||||
...restOfSpanOpts,
|
||||
attributes: {
|
||||
...restOfSpanOpts.attributes,
|
||||
[GEN_AI_CONVERSATION_ID]: sessionId,
|
||||
},
|
||||
};
|
||||
|
||||
const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);
|
||||
return tracer.startActiveSpan(
|
||||
operation,
|
||||
spanOptsWithSession,
|
||||
async (span) => {
|
||||
const meta: SpanMetadata = {
|
||||
name: operation,
|
||||
attributes: {
|
||||
[GEN_AI_OPERATION_NAME]: operation,
|
||||
[GEN_AI_AGENT_NAME]: SERVICE_NAME,
|
||||
[GEN_AI_AGENT_DESCRIPTION]: SERVICE_DESCRIPTION,
|
||||
[GEN_AI_CONVERSATION_ID]: sessionId,
|
||||
},
|
||||
};
|
||||
let spanEnded = false;
|
||||
const endSpan = () => {
|
||||
if (spanEnded) {
|
||||
return;
|
||||
}
|
||||
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 (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);
|
||||
if (meta.error) {
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: getErrorMessage(meta.error),
|
||||
});
|
||||
if (meta.error instanceof Error) {
|
||||
span.recordException(meta.error);
|
||||
}
|
||||
} else {
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
}
|
||||
}
|
||||
if (meta.error) {
|
||||
} catch (e) {
|
||||
// Log the error but don't rethrow, to ensure span.end() is called.
|
||||
diag.error('Error setting span attributes in endSpan', e);
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: getErrorMessage(meta.error),
|
||||
message: `Error in endSpan: ${getErrorMessage(e)}`,
|
||||
});
|
||||
if (meta.error instanceof Error) {
|
||||
span.recordException(meta.error);
|
||||
}
|
||||
} else {
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
} catch (e) {
|
||||
// Log the error but don't rethrow, to ensure span.end() is called.
|
||||
diag.error('Error setting span attributes in endSpan', e);
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: `Error in endSpan: ${getErrorMessage(e)}`,
|
||||
});
|
||||
};
|
||||
|
||||
let isStream = false;
|
||||
try {
|
||||
const result = await fn({ metadata: meta });
|
||||
|
||||
if (isAsyncIterable(result)) {
|
||||
isStream = true;
|
||||
const streamWrapper = (async function* () {
|
||||
try {
|
||||
yield* result;
|
||||
} catch (e: unknown) {
|
||||
meta.error = e;
|
||||
throw e;
|
||||
} finally {
|
||||
endSpan();
|
||||
}
|
||||
})();
|
||||
|
||||
const finalResult = Object.assign(streamWrapper, result);
|
||||
spanRegistry.register(finalResult, endSpan);
|
||||
return finalResult;
|
||||
}
|
||||
return result;
|
||||
} catch (e: unknown) {
|
||||
meta.error = e;
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
if (!isStream) {
|
||||
endSpan();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let isStream = false;
|
||||
try {
|
||||
const result = await fn({ metadata: meta });
|
||||
|
||||
if (isAsyncIterable(result)) {
|
||||
isStream = true;
|
||||
const streamWrapper = (async function* () {
|
||||
try {
|
||||
yield* result;
|
||||
} catch (e: unknown) {
|
||||
meta.error = e;
|
||||
throw e;
|
||||
} finally {
|
||||
endSpan();
|
||||
}
|
||||
})();
|
||||
|
||||
const finalResult = Object.assign(streamWrapper, result);
|
||||
spanRegistry.register(finalResult, endSpan);
|
||||
return finalResult;
|
||||
}
|
||||
return result;
|
||||
} catch (e: unknown) {
|
||||
meta.error = e;
|
||||
throw e;
|
||||
} finally {
|
||||
if (!isStream) {
|
||||
endSpan();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3561,6 +3561,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