Skip to content

Commit f6ee025

Browse files
authored
feat(hooks): Hook Telemetry Infrastructure (#9082)
1 parent e50bf6a commit f6ee025

File tree

6 files changed

+810
-0
lines changed

6 files changed

+810
-0
lines changed

packages/core/src/telemetry/loggers.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import type {
4949
WebFetchFallbackAttemptEvent,
5050
ExtensionUpdateEvent,
5151
LlmLoopCheckEvent,
52+
HookCallEvent,
5253
} from './types.js';
5354
import {
5455
recordApiErrorMetrics,
@@ -66,6 +67,7 @@ import {
6667
recordAgentRunMetrics,
6768
recordRecoveryAttemptMetrics,
6869
recordLinesChanged,
70+
recordHookCallMetrics,
6971
} from './metrics.js';
7072
import { isTelemetrySdkInitialized } from './sdk.js';
7173
import type { UiEvent } from './uiTelemetry.js';
@@ -670,3 +672,22 @@ export function logLlmLoopCheck(
670672
};
671673
logger.emit(logRecord);
672674
}
675+
676+
export function logHookCall(config: Config, event: HookCallEvent): void {
677+
if (!isTelemetrySdkInitialized()) return;
678+
679+
const logger = logs.getLogger(SERVICE_NAME);
680+
const logRecord: LogRecord = {
681+
body: event.toLogBody(),
682+
attributes: event.toOpenTelemetryAttributes(config),
683+
};
684+
logger.emit(logRecord);
685+
686+
recordHookCallMetrics(
687+
config,
688+
event.hook_event_name,
689+
event.hook_name,
690+
event.duration_ms,
691+
event.success,
692+
);
693+
}

packages/core/src/telemetry/metrics.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,5 +1342,115 @@ describe('Telemetry Metrics', () => {
13421342
expect(mockHistogramRecordFn).not.toHaveBeenCalled();
13431343
});
13441344
});
1345+
1346+
describe('recordHookCallMetrics', () => {
1347+
let recordHookCallMetricsModule: typeof import('./metrics.js').recordHookCallMetrics;
1348+
1349+
beforeEach(async () => {
1350+
recordHookCallMetricsModule = (await import('./metrics.js'))
1351+
.recordHookCallMetrics;
1352+
});
1353+
1354+
it('should record hook call metrics with counter and histogram', () => {
1355+
initializeMetricsModule(mockConfig);
1356+
mockCounterAddFn.mockClear();
1357+
mockHistogramRecordFn.mockClear();
1358+
1359+
recordHookCallMetricsModule(
1360+
mockConfig,
1361+
'BeforeTool',
1362+
'test-hook',
1363+
150,
1364+
true,
1365+
);
1366+
1367+
// Verify counter recorded
1368+
expect(mockCounterAddFn).toHaveBeenCalledWith(1, {
1369+
'session.id': 'test-session-id',
1370+
'installation.id': 'test-installation-id',
1371+
'user.email': '[email protected]',
1372+
hook_event_name: 'BeforeTool',
1373+
hook_name: 'test-hook',
1374+
success: true,
1375+
});
1376+
1377+
// Verify histogram recorded
1378+
expect(mockHistogramRecordFn).toHaveBeenCalledWith(150, {
1379+
'session.id': 'test-session-id',
1380+
'installation.id': 'test-installation-id',
1381+
'user.email': '[email protected]',
1382+
hook_event_name: 'BeforeTool',
1383+
hook_name: 'test-hook',
1384+
success: true,
1385+
});
1386+
});
1387+
1388+
it('should always sanitize hook names regardless of content', () => {
1389+
initializeMetricsModule(mockConfig);
1390+
mockCounterAddFn.mockClear();
1391+
1392+
// Test with a command that has sensitive information
1393+
recordHookCallMetricsModule(
1394+
mockConfig,
1395+
'BeforeTool',
1396+
'/path/to/.gemini/hooks/check-secrets.sh --api-key=abc123',
1397+
150,
1398+
true,
1399+
);
1400+
1401+
// Verify hook name is sanitized (detailed sanitization tested in hook-call-event.test.ts)
1402+
expect(mockCounterAddFn).toHaveBeenCalledWith(1, {
1403+
'session.id': 'test-session-id',
1404+
'installation.id': 'test-installation-id',
1405+
'user.email': '[email protected]',
1406+
hook_event_name: 'BeforeTool',
1407+
hook_name: 'check-secrets.sh', // Sanitized
1408+
success: true,
1409+
});
1410+
});
1411+
1412+
it('should track both success and failure', () => {
1413+
initializeMetricsModule(mockConfig);
1414+
mockCounterAddFn.mockClear();
1415+
1416+
// Success case
1417+
recordHookCallMetricsModule(
1418+
mockConfig,
1419+
'BeforeTool',
1420+
'test-hook',
1421+
100,
1422+
true,
1423+
);
1424+
1425+
expect(mockCounterAddFn).toHaveBeenNthCalledWith(
1426+
1,
1427+
1,
1428+
expect.objectContaining({
1429+
hook_event_name: 'BeforeTool',
1430+
hook_name: 'test-hook',
1431+
success: true,
1432+
}),
1433+
);
1434+
1435+
// Failure case
1436+
recordHookCallMetricsModule(
1437+
mockConfig,
1438+
'AfterTool',
1439+
'test-hook',
1440+
150,
1441+
false,
1442+
);
1443+
1444+
expect(mockCounterAddFn).toHaveBeenNthCalledWith(
1445+
2,
1446+
1,
1447+
expect.objectContaining({
1448+
hook_event_name: 'AfterTool',
1449+
hook_name: 'test-hook',
1450+
success: false,
1451+
}),
1452+
);
1453+
});
1454+
});
13451455
});
13461456
});

packages/core/src/telemetry/metrics.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
} from './types.js';
1717
import { AuthType } from '../core/contentGenerator.js';
1818
import { getCommonAttributes } from './telemetryAttributes.js';
19+
import { sanitizeHookName } from './sanitize.js';
1920

2021
const EVENT_CHAT_COMPRESSION = 'gemini_cli.chat_compression';
2122
const TOOL_CALL_COUNT = 'gemini_cli.tool.call.count';
@@ -34,6 +35,8 @@ const MODEL_ROUTING_LATENCY = 'gemini_cli.model_routing.latency';
3435
const MODEL_ROUTING_FAILURE_COUNT = 'gemini_cli.model_routing.failure.count';
3536
const MODEL_SLASH_COMMAND_CALL_COUNT =
3637
'gemini_cli.slash_command.model.call_count';
38+
const EVENT_HOOK_CALL_COUNT = 'gemini_cli.hook_call.count';
39+
const EVENT_HOOK_CALL_LATENCY = 'gemini_cli.hook_call.latency';
3740

3841
// Agent Metrics
3942
const AGENT_RUN_COUNT = 'gemini_cli.agent.run.count';
@@ -202,6 +205,16 @@ const COUNTER_DEFINITIONS = {
202205
assign: (c: Counter) => (exitFailCounter = c),
203206
attributes: {} as Record<string, never>,
204207
},
208+
[EVENT_HOOK_CALL_COUNT]: {
209+
description: 'Counts hook calls, tagged by hook event name and success.',
210+
valueType: ValueType.INT,
211+
assign: (c: Counter) => (hookCallCounter = c),
212+
attributes: {} as {
213+
hook_event_name: string;
214+
hook_name: string;
215+
success: boolean;
216+
},
217+
},
205218
} as const;
206219

207220
const HISTOGRAM_DEFINITIONS = {
@@ -297,6 +310,17 @@ const HISTOGRAM_DEFINITIONS = {
297310
'error.type'?: string;
298311
},
299312
},
313+
[EVENT_HOOK_CALL_LATENCY]: {
314+
description: 'Latency of hook calls in milliseconds.',
315+
unit: 'ms',
316+
valueType: ValueType.INT,
317+
assign: (c: Histogram) => (hookCallLatencyHistogram = c),
318+
attributes: {} as {
319+
hook_event_name: string;
320+
hook_name: string;
321+
success: boolean;
322+
},
323+
},
300324
} as const;
301325

302326
const PERFORMANCE_COUNTER_DEFINITIONS = {
@@ -506,6 +530,8 @@ let agentRecoveryAttemptDurationHistogram: Histogram | undefined;
506530
let flickerFrameCounter: Counter | undefined;
507531
let exitFailCounter: Counter | undefined;
508532
let slowRenderHistogram: Histogram | undefined;
533+
let hookCallCounter: Counter | undefined;
534+
let hookCallLatencyHistogram: Histogram | undefined;
509535

510536
// OpenTelemetry GenAI Semantic Convention Metrics
511537
let genAiClientTokenUsageHistogram: Histogram | undefined;
@@ -1162,3 +1188,27 @@ export function recordApiResponseMetrics(
11621188
});
11631189
}
11641190
}
1191+
1192+
export function recordHookCallMetrics(
1193+
config: Config,
1194+
hookEventName: string,
1195+
hookName: string,
1196+
durationMs: number,
1197+
success: boolean,
1198+
): void {
1199+
if (!hookCallCounter || !hookCallLatencyHistogram || !isMetricsInitialized)
1200+
return;
1201+
1202+
// Always sanitize hook names in metrics (metrics are aggregated and exposed)
1203+
const sanitizedHookName = sanitizeHookName(hookName);
1204+
1205+
const metricAttributes: Attributes = {
1206+
...baseMetricDefinition.getCommonAttributes(config),
1207+
hook_event_name: hookEventName,
1208+
hook_name: sanitizedHookName,
1209+
success,
1210+
};
1211+
1212+
hookCallCounter.add(1, metricAttributes);
1213+
hookCallLatencyHistogram.record(durationMs, metricAttributes);
1214+
}

0 commit comments

Comments
 (0)