feat(ai) - add observability (#20850)

**AI Chat - Tool Executions (counters, tagged with model)**
ai-chat/tool-execution-succeeded: number of tool calls invoked by the AI
that completed without error
ai-chat/tool-execution-failed: number of tool calls invoked by the AI
that threw an error
**AI Chat - Token Usage (counters, tagged with model)**
ai-chat/input-tokens: total input tokens sent to the model across all
turns
ai-chat/output-tokens: total output tokens generated by the model
ai-chat/cache-read-tokens: input tokens served from the model's prompt
cache (cheaper)
ai-chat/cache-write-tokens: input tokens written into the prompt cache
for future reuse
**AI Chat - Latency (histograms in ms, tagged with model)**
ai-chat/turn-latency-ms: total duration of a full chat turn (from stream
start to stream end)
ai-chat/step-latency-ms: duration of a single reasoning/tool-call step
within a turn
ai-chat/ttft-ms: time-to-first-token, i.e. how long until the model
starts streaming output
**MCP - Tool Executions (counters)**
mcp/tool-execution-succeeded: number of MCP tool calls that completed
successfully
mcp/tool-execution-failed: number of MCP tool calls that threw an error
This commit is contained in:
Etienne
2026-05-22 17:32:51 +02:00
committed by GitHub
parent de044f4b45
commit eda41b4eba
23 changed files with 169 additions and 33 deletions
@@ -86,7 +86,7 @@ const createSignInUpServiceForTests = () => {
markEmailAsVerified: jest.fn(),
} as any,
{
incrementCounter: jest.fn(),
incrementCounterForEvent: jest.fn(),
} as any,
{
invalidateAndRecompute: jest.fn(),
@@ -384,7 +384,7 @@ export class SignInUpService {
undefined,
);
void this.metricsService.incrementCounter({
void this.metricsService.incrementCounterForEvent({
key: MetricsKeys.SignUpSuccess,
shouldStoreInCache: false,
});
@@ -32,7 +32,7 @@ export class CaptchaGuard implements CanActivate {
if (result.success) {
return true;
} else {
await this.metricsService.incrementCounter({
await this.metricsService.incrementCounterForEvent({
key: MetricsKeys.InvalidCaptcha,
eventId: token || '',
...(result.error ? { attributes: { error: result.error } } : {}),
@@ -116,7 +116,7 @@ export const useGraphQLErrorHandlerHook = <
setResult,
}) => {
if (!result.errors || result.errors.length === 0) {
void options.metricsService.incrementCounter({
void options.metricsService.incrementCounterForEvent({
key: MetricsKeys.GraphqlOperation200,
});
@@ -178,11 +178,11 @@ export const useGraphQLErrorHandlerHook = <
}
if (metricKey) {
void options.metricsService.incrementCounter({
void options.metricsService.incrementCounterForEvent({
key: metricKey,
});
} else {
void options.metricsService.incrementCounter({
void options.metricsService.incrementCounterForEvent({
key: MetricsKeys.GraphqlOperationUnknown,
});
}
@@ -282,7 +282,7 @@ export const useGraphQLErrorHandlerHook = <
isDefined(currentMetadataVersion) &&
requestMetadataVersion !== `${currentMetadataVersion}`
) {
void options.metricsService.incrementCounter({
void options.metricsService.incrementCounterForEvent({
key: MetricsKeys.SchemaVersionMismatch,
});
@@ -312,7 +312,7 @@ export const useGraphQLErrorHandlerHook = <
isDefined(backendMajor) &&
frontEndMajor < backendMajor
) {
void options.metricsService.incrementCounter({
void options.metricsService.incrementCounterForEvent({
key: MetricsKeys.AppVersionMismatch,
});
throw new GraphQLError(APP_VERSION_MISMATCH_ERROR, {
@@ -136,7 +136,7 @@ export class BullMQDriver
);
this.workerMap[queueName].on('completed', (job) => {
void this.metricsService.incrementCounter({
void this.metricsService.incrementCounterForEvent({
key: MetricsKeys.JobCompleted,
attributes: { queue: queueName, job_name: job?.name ?? '' },
shouldStoreInCache: false,
@@ -148,7 +148,7 @@ export class BullMQDriver
return;
}
void this.metricsService.incrementCounter({
void this.metricsService.incrementCounterForEvent({
key: MetricsKeys.JobFailed,
attributes: {
queue: queueName,
@@ -80,7 +80,7 @@ export class MetricsService {
return gauge;
}
async incrementCounter({
async incrementCounterForEvent({
key,
eventId,
attributes,
@@ -110,7 +110,7 @@ export class MetricsService {
}
}
async batchIncrementCounter({
async incrementCounterForEvents({
key,
eventIds,
attributes,
@@ -130,6 +130,32 @@ export class MetricsService {
}
}
incrementCounterBy({
key,
amount,
attributes,
}: {
key: MetricsKeys;
amount: number;
attributes?: Attributes;
}): void {
this.getMeter().createCounter(key).add(amount, attributes);
}
recordHistogram({
key,
value,
unit,
attributes,
}: {
key: MetricsKeys;
value: number;
unit?: string;
attributes?: Attributes;
}): void {
this.getMeter().createHistogram(key, { unit }).record(value, attributes);
}
async groupMetrics(
metrics: { name: string; cacheKey: MetricsKeys }[],
): Promise<Record<string, number>> {
@@ -23,8 +23,14 @@ export enum MetricsKeys {
WorkflowRunThrottled = 'workflow-run/throttled',
WorkflowRunFailedToEnqueue = 'workflow-run/failed/to-enqueue',
WorkflowRunSystemError = 'workflow-run/system-error',
AiToolExecutionFailed = 'ai-tool-execution/failed',
AiToolExecutionSucceeded = 'ai-tool-execution/succeeded',
AiChatToolExecutionSucceeded = 'ai-chat/tool-execution-succeeded',
AiChatToolExecutionFailed = 'ai-chat/tool-execution-failed',
McpToolExecutionSucceeded = 'mcp/tool-execution-succeeded',
McpToolExecutionFailed = 'mcp/tool-execution-failed',
AiChatInputTokens = 'ai-chat/input-tokens',
AiChatOutputTokens = 'ai-chat/output-tokens',
AiChatCacheReadTokens = 'ai-chat/cache-read-tokens',
AiChatCacheWriteTokens = 'ai-chat/cache-write-tokens',
SchemaVersionMismatch = 'schema-version/mismatch',
AppVersionMismatch = 'app-version/mismatch',
CronJobDeletedWorkspace = 'cron-job/deleted-workspace',
@@ -34,4 +40,7 @@ export enum MetricsKeys {
JobCompleted = 'job/completed',
JobFailed = 'job/failed',
JobWaiting = 'job/waiting',
AiChatTurnLatencyMs = 'ai-chat/turn-latency-ms',
AiChatStepLatencyMs = 'ai-chat/step-latency-ms',
AiChatTtftMs = 'ai-chat/ttft-ms',
}