mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
feat(telemetry): add keychain availability and token storage metrics (#18971)
This commit is contained in:
@@ -47,6 +47,8 @@ import type {
|
||||
ApprovalModeDurationEvent,
|
||||
PlanExecutionEvent,
|
||||
ToolOutputMaskingEvent,
|
||||
KeychainAvailabilityEvent,
|
||||
TokenStorageInitializationEvent,
|
||||
} from '../types.js';
|
||||
import { EventMetadataKey } from './event-metadata-key.js';
|
||||
import type { Config } from '../../config/config.js';
|
||||
@@ -111,6 +113,8 @@ export enum EventNames {
|
||||
APPROVAL_MODE_DURATION = 'approval_mode_duration',
|
||||
PLAN_EXECUTION = 'plan_execution',
|
||||
TOOL_OUTPUT_MASKING = 'tool_output_masking',
|
||||
KEYCHAIN_AVAILABILITY = 'keychain_availability',
|
||||
TOKEN_STORAGE_INITIALIZATION = 'token_storage_initialization',
|
||||
}
|
||||
|
||||
export interface LogResponse {
|
||||
@@ -1613,6 +1617,40 @@ export class ClearcutLogger {
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logKeychainAvailabilityEvent(event: KeychainAvailabilityEvent): void {
|
||||
const data: EventValue[] = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_KEYCHAIN_AVAILABLE,
|
||||
value: JSON.stringify(event.available),
|
||||
},
|
||||
];
|
||||
|
||||
this.enqueueLogEvent(
|
||||
this.createLogEvent(EventNames.KEYCHAIN_AVAILABILITY, data),
|
||||
);
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logTokenStorageInitializationEvent(
|
||||
event: TokenStorageInitializationEvent,
|
||||
): void {
|
||||
const data: EventValue[] = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOKEN_STORAGE_TYPE,
|
||||
value: event.type,
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOKEN_STORAGE_FORCED,
|
||||
value: JSON.stringify(event.forced),
|
||||
},
|
||||
];
|
||||
|
||||
this.enqueueLogEvent(
|
||||
this.createLogEvent(EventNames.TOKEN_STORAGE_INITIALIZATION, data),
|
||||
);
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds default fields to data, and returns a new data array. This fields
|
||||
* should exist on all log events.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// Defines valid event metadata keys for Clearcut logging.
|
||||
export enum EventMetadataKey {
|
||||
// Deleted enums: 24
|
||||
// Next ID: 156
|
||||
// Next ID: 159
|
||||
|
||||
GEMINI_CLI_KEY_UNKNOWN = 0,
|
||||
|
||||
@@ -578,7 +578,6 @@ export enum EventMetadataKey {
|
||||
// Logs the total prunable tokens identified at the trigger point.
|
||||
GEMINI_CLI_TOOL_OUTPUT_MASKING_TOTAL_PRUNABLE_TOKENS = 151,
|
||||
|
||||
// ==========================================================================
|
||||
// Ask User Stats Event Keys
|
||||
// ==========================================================================
|
||||
|
||||
@@ -593,4 +592,17 @@ export enum EventMetadataKey {
|
||||
|
||||
// Logs the number of questions answered in the ask_user tool.
|
||||
GEMINI_CLI_ASK_USER_ANSWER_COUNT = 155,
|
||||
|
||||
// ==========================================================================
|
||||
// Keychain & Token Storage Event Keys
|
||||
// ==========================================================================
|
||||
|
||||
// Logs whether the keychain is available.
|
||||
GEMINI_CLI_KEYCHAIN_AVAILABLE = 156,
|
||||
|
||||
// Logs the type of token storage initialized.
|
||||
GEMINI_CLI_TOKEN_STORAGE_TYPE = 157,
|
||||
|
||||
// Logs whether the token storage type was forced by an environment variable.
|
||||
GEMINI_CLI_TOKEN_STORAGE_FORCED = 158,
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ import type {
|
||||
LlmLoopCheckEvent,
|
||||
PlanExecutionEvent,
|
||||
ToolOutputMaskingEvent,
|
||||
KeychainAvailabilityEvent,
|
||||
TokenStorageInitializationEvent,
|
||||
} from './types.js';
|
||||
import {
|
||||
recordApiErrorMetrics,
|
||||
@@ -76,6 +78,8 @@ import {
|
||||
recordLinesChanged,
|
||||
recordHookCallMetrics,
|
||||
recordPlanExecution,
|
||||
recordKeychainAvailability,
|
||||
recordTokenStorageInitialization,
|
||||
} from './metrics.js';
|
||||
import { bufferTelemetryEvent } from './sdk.js';
|
||||
import type { UiEvent } from './uiTelemetry.js';
|
||||
@@ -805,3 +809,37 @@ export function logStartupStats(
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function logKeychainAvailability(
|
||||
config: Config,
|
||||
event: KeychainAvailabilityEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logKeychainAvailabilityEvent(event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
recordKeychainAvailability(config, event);
|
||||
});
|
||||
}
|
||||
|
||||
export function logTokenStorageInitialization(
|
||||
config: Config,
|
||||
event: TokenStorageInitializationEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logTokenStorageInitializationEvent(event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
recordTokenStorageInitialization(config, event);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,12 @@ import {
|
||||
ApiRequestPhase,
|
||||
} from './metrics.js';
|
||||
import { makeFakeConfig } from '../test-utils/config.js';
|
||||
import { ModelRoutingEvent, AgentFinishEvent } from './types.js';
|
||||
import {
|
||||
ModelRoutingEvent,
|
||||
AgentFinishEvent,
|
||||
KeychainAvailabilityEvent,
|
||||
TokenStorageInitializationEvent,
|
||||
} from './types.js';
|
||||
import { AgentTerminateMode } from '../agents/types.js';
|
||||
|
||||
const mockCounterAddFn: Mock<
|
||||
@@ -97,6 +102,8 @@ describe('Telemetry Metrics', () => {
|
||||
let recordLinesChangedModule: typeof import('./metrics.js').recordLinesChanged;
|
||||
let recordSlowRenderModule: typeof import('./metrics.js').recordSlowRender;
|
||||
let recordPlanExecutionModule: typeof import('./metrics.js').recordPlanExecution;
|
||||
let recordKeychainAvailabilityModule: typeof import('./metrics.js').recordKeychainAvailability;
|
||||
let recordTokenStorageInitializationModule: typeof import('./metrics.js').recordTokenStorageInitialization;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
@@ -142,6 +149,10 @@ describe('Telemetry Metrics', () => {
|
||||
recordLinesChangedModule = metricsJsModule.recordLinesChanged;
|
||||
recordSlowRenderModule = metricsJsModule.recordSlowRender;
|
||||
recordPlanExecutionModule = metricsJsModule.recordPlanExecution;
|
||||
recordKeychainAvailabilityModule =
|
||||
metricsJsModule.recordKeychainAvailability;
|
||||
recordTokenStorageInitializationModule =
|
||||
metricsJsModule.recordTokenStorageInitialization;
|
||||
|
||||
const otelApiModule = await import('@opentelemetry/api');
|
||||
|
||||
@@ -1485,4 +1496,57 @@ describe('Telemetry Metrics', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keychain and Token Storage Metrics', () => {
|
||||
describe('recordKeychainAvailability', () => {
|
||||
it('should not record metrics if not initialized', () => {
|
||||
const config = makeFakeConfig({});
|
||||
const event = new KeychainAvailabilityEvent(true);
|
||||
recordKeychainAvailabilityModule(config, event);
|
||||
expect(mockCounterAddFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should record keychain availability when initialized', () => {
|
||||
const config = makeFakeConfig({});
|
||||
initializeMetricsModule(config);
|
||||
mockCounterAddFn.mockClear();
|
||||
|
||||
const event = new KeychainAvailabilityEvent(true);
|
||||
recordKeychainAvailabilityModule(config, event);
|
||||
|
||||
expect(mockCounterAddFn).toHaveBeenCalledWith(1, {
|
||||
'session.id': 'test-session-id',
|
||||
'installation.id': 'test-installation-id',
|
||||
'user.email': 'test@example.com',
|
||||
available: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordTokenStorageInitialization', () => {
|
||||
it('should not record metrics if not initialized', () => {
|
||||
const config = makeFakeConfig({});
|
||||
const event = new TokenStorageInitializationEvent('hybrid', false);
|
||||
recordTokenStorageInitializationModule(config, event);
|
||||
expect(mockCounterAddFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should record token storage initialization when initialized', () => {
|
||||
const config = makeFakeConfig({});
|
||||
initializeMetricsModule(config);
|
||||
mockCounterAddFn.mockClear();
|
||||
|
||||
const event = new TokenStorageInitializationEvent('keychain', true);
|
||||
recordTokenStorageInitializationModule(config, event);
|
||||
|
||||
expect(mockCounterAddFn).toHaveBeenCalledWith(1, {
|
||||
'session.id': 'test-session-id',
|
||||
'installation.id': 'test-installation-id',
|
||||
'user.email': 'test@example.com',
|
||||
type: 'keychain',
|
||||
forced: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ import type {
|
||||
ModelSlashCommandEvent,
|
||||
AgentFinishEvent,
|
||||
RecoveryAttemptEvent,
|
||||
KeychainAvailabilityEvent,
|
||||
TokenStorageInitializationEvent,
|
||||
} from './types.js';
|
||||
import { AuthType } from '../core/contentGenerator.js';
|
||||
import { getCommonAttributes } from './telemetryAttributes.js';
|
||||
@@ -37,6 +39,8 @@ const MODEL_SLASH_COMMAND_CALL_COUNT =
|
||||
'gemini_cli.slash_command.model.call_count';
|
||||
const EVENT_HOOK_CALL_COUNT = 'gemini_cli.hook_call.count';
|
||||
const EVENT_HOOK_CALL_LATENCY = 'gemini_cli.hook_call.latency';
|
||||
const KEYCHAIN_AVAILABILITY_COUNT = 'gemini_cli.keychain.availability.count';
|
||||
const TOKEN_STORAGE_TYPE_COUNT = 'gemini_cli.token_storage.type.count';
|
||||
|
||||
// Agent Metrics
|
||||
const AGENT_RUN_COUNT = 'gemini_cli.agent.run.count';
|
||||
@@ -236,6 +240,25 @@ const COUNTER_DEFINITIONS = {
|
||||
success: boolean;
|
||||
},
|
||||
},
|
||||
[KEYCHAIN_AVAILABILITY_COUNT]: {
|
||||
description: 'Counts keychain availability checks.',
|
||||
valueType: ValueType.INT,
|
||||
assign: (c: Counter) => (keychainAvailabilityCounter = c),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
attributes: {} as {
|
||||
available: boolean;
|
||||
},
|
||||
},
|
||||
[TOKEN_STORAGE_TYPE_COUNT]: {
|
||||
description: 'Counts token storage type initializations.',
|
||||
valueType: ValueType.INT,
|
||||
assign: (c: Counter) => (tokenStorageTypeCounter = c),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
attributes: {} as {
|
||||
type: string;
|
||||
forced: boolean;
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const HISTOGRAM_DEFINITIONS = {
|
||||
@@ -572,6 +595,8 @@ let planExecutionCounter: Counter | undefined;
|
||||
let slowRenderHistogram: Histogram | undefined;
|
||||
let hookCallCounter: Counter | undefined;
|
||||
let hookCallLatencyHistogram: Histogram | undefined;
|
||||
let keychainAvailabilityCounter: Counter | undefined;
|
||||
let tokenStorageTypeCounter: Counter | undefined;
|
||||
|
||||
// OpenTelemetry GenAI Semantic Convention Metrics
|
||||
let genAiClientTokenUsageHistogram: Histogram | undefined;
|
||||
@@ -1279,3 +1304,32 @@ export function recordHookCallMetrics(
|
||||
hookCallCounter.add(1, metricAttributes);
|
||||
hookCallLatencyHistogram.record(durationMs, metricAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a metric for keychain availability.
|
||||
*/
|
||||
export function recordKeychainAvailability(
|
||||
config: Config,
|
||||
event: KeychainAvailabilityEvent,
|
||||
): void {
|
||||
if (!keychainAvailabilityCounter || !isMetricsInitialized) return;
|
||||
keychainAvailabilityCounter.add(1, {
|
||||
...baseMetricDefinition.getCommonAttributes(config),
|
||||
available: event.available,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a metric for token storage type initialization.
|
||||
*/
|
||||
export function recordTokenStorageInitialization(
|
||||
config: Config,
|
||||
event: TokenStorageInitializationEvent,
|
||||
): void {
|
||||
if (!tokenStorageTypeCounter || !isMetricsInitialized) return;
|
||||
tokenStorageTypeCounter.add(1, {
|
||||
...baseMetricDefinition.getCommonAttributes(config),
|
||||
type: event.type,
|
||||
forced: event.forced,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,6 +53,15 @@ import {
|
||||
import { TelemetryTarget } from './index.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { authEvents } from '../code_assist/oauth2.js';
|
||||
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||
import {
|
||||
logKeychainAvailability,
|
||||
logTokenStorageInitialization,
|
||||
} from './loggers.js';
|
||||
import type {
|
||||
KeychainAvailabilityEvent,
|
||||
TokenStorageInitializationEvent,
|
||||
} from './types.js';
|
||||
|
||||
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
|
||||
class DiagLoggerAdapter {
|
||||
@@ -86,6 +95,12 @@ let telemetryInitialized = false;
|
||||
let callbackRegistered = false;
|
||||
let authListener: ((newCredentials: JWTInput) => Promise<void>) | undefined =
|
||||
undefined;
|
||||
let keychainAvailabilityListener:
|
||||
| ((event: KeychainAvailabilityEvent) => void)
|
||||
| undefined = undefined;
|
||||
let tokenStorageTypeListener:
|
||||
| ((event: TokenStorageInitializationEvent) => void)
|
||||
| undefined = undefined;
|
||||
const telemetryBuffer: Array<() => void | Promise<void>> = [];
|
||||
let activeTelemetryEmail: string | undefined;
|
||||
|
||||
@@ -196,6 +211,26 @@ export async function initializeTelemetry(
|
||||
'session.id': config.getSessionId(),
|
||||
});
|
||||
|
||||
if (!keychainAvailabilityListener) {
|
||||
keychainAvailabilityListener = (event: KeychainAvailabilityEvent) => {
|
||||
logKeychainAvailability(config, event);
|
||||
};
|
||||
coreEvents.on(
|
||||
CoreEvent.TelemetryKeychainAvailability,
|
||||
keychainAvailabilityListener,
|
||||
);
|
||||
}
|
||||
|
||||
if (!tokenStorageTypeListener) {
|
||||
tokenStorageTypeListener = (event: TokenStorageInitializationEvent) => {
|
||||
logTokenStorageInitialization(config, event);
|
||||
};
|
||||
coreEvents.on(
|
||||
CoreEvent.TelemetryTokenStorageType,
|
||||
tokenStorageTypeListener,
|
||||
);
|
||||
}
|
||||
|
||||
const otlpEndpoint = config.getTelemetryOtlpEndpoint();
|
||||
const otlpProtocol = config.getTelemetryOtlpProtocol();
|
||||
const telemetryTarget = config.getTelemetryTarget();
|
||||
@@ -376,6 +411,20 @@ export async function shutdownTelemetry(
|
||||
authEvents.off('post_auth', authListener);
|
||||
authListener = undefined;
|
||||
}
|
||||
if (keychainAvailabilityListener) {
|
||||
coreEvents.off(
|
||||
CoreEvent.TelemetryKeychainAvailability,
|
||||
keychainAvailabilityListener,
|
||||
);
|
||||
keychainAvailabilityListener = undefined;
|
||||
}
|
||||
if (tokenStorageTypeListener) {
|
||||
coreEvents.off(
|
||||
CoreEvent.TelemetryTokenStorageType,
|
||||
tokenStorageTypeListener,
|
||||
);
|
||||
tokenStorageTypeListener = undefined;
|
||||
}
|
||||
callbackRegistered = false;
|
||||
activeTelemetryEmail = undefined;
|
||||
}
|
||||
|
||||
@@ -2111,3 +2111,60 @@ export class HookCallEvent implements BaseTelemetryEvent {
|
||||
return `Hook call ${hookId} ${status} in ${this.duration_ms}ms`;
|
||||
}
|
||||
}
|
||||
|
||||
export const EVENT_KEYCHAIN_AVAILABILITY = 'gemini_cli.keychain.availability';
|
||||
export class KeychainAvailabilityEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'keychain_availability';
|
||||
'event.timestamp': string;
|
||||
available: boolean;
|
||||
|
||||
constructor(available: boolean) {
|
||||
this['event.name'] = 'keychain_availability';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.available = available;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_KEYCHAIN_AVAILABILITY,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
available: this.available,
|
||||
};
|
||||
return attributes;
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Keychain availability: ${this.available}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const EVENT_TOKEN_STORAGE_INITIALIZATION =
|
||||
'gemini_cli.token_storage.initialization';
|
||||
export class TokenStorageInitializationEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'token_storage_initialization';
|
||||
'event.timestamp': string;
|
||||
type: string;
|
||||
forced: boolean;
|
||||
|
||||
constructor(type: string, forced: boolean) {
|
||||
this['event.name'] = 'token_storage_initialization';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.type = type;
|
||||
this.forced = forced;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
return {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_TOKEN_STORAGE_INITIALIZATION,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
type: this.type,
|
||||
forced: this.forced,
|
||||
};
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Token storage initialized. Type: ${this.type}. Forced: ${this.forced}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user