diff --git a/src/internal-agents/run-stream.ts b/src/internal-agents/run-stream.ts index 2e0494121d..1bf1a5f28c 100644 --- a/src/internal-agents/run-stream.ts +++ b/src/internal-agents/run-stream.ts @@ -304,6 +304,41 @@ function resolveChildRunToolNames(mergedTools: Agent["config"]["tools"]): Set) { + const childRunToolCallIds = new Set(); + let terminalErrorCode: string | undefined; + let toolErrorCount = 0; + let childRunErrorCount = 0; + return { + observe(event: string, payload: Record): void { + const { toolCallId } = payload; + if ( + event === "ToolCallStart" && typeof toolCallId === "string" && + typeof payload.toolCallName === "string" && childRunToolNames.has(payload.toolCallName) + ) { + childRunToolCallIds.add(toolCallId); + } else if (event === "ToolCallResult" && payload.isError === true) { + toolErrorCount++; + if (typeof toolCallId === "string" && childRunToolCallIds.has(toolCallId)) { + childRunErrorCount++; + } + } else if (event === "RunError") { + terminalErrorCode ??= toStableRunErrorCode(payload.code); + } + }, + terminalErrorCode: () => terminalErrorCode, + spanAttributes: () => ({ + "agent.run.tool_error_count": toolErrorCount, + "agent.run.child_run_error_count": childRunErrorCount, + }), + logFields: () => ({ toolErrorCount, childRunErrorCount }), + }; +} + function isExplicitlyDeniedToolName( agent: Agent, deniedToolNames: ReadonlySet, @@ -1401,31 +1436,7 @@ export async function createRuntimeAgentStreamResponse( "Internal agent runtime stream stopped before EOF", ); let readerCancellation: Promise | undefined; - let terminalRunErrorCode: string | undefined; - let toolErrorCount = 0; - let childRunErrorCount = 0; - const childRunToolCallIds = new Set(); - const observeRunOutcomeEvent = (event: string, payload: Record) => { - if ( - event === "ToolCallStart" && typeof payload.toolCallId === "string" && - typeof payload.toolCallName === "string" && - childRunToolNames.has(payload.toolCallName) - ) { - childRunToolCallIds.add(payload.toolCallId); - } - if (event === "ToolCallResult" && payload.isError === true) { - toolErrorCount++; - if ( - typeof payload.toolCallId === "string" && - childRunToolCallIds.has(payload.toolCallId) - ) { - childRunErrorCount++; - } - } - if (event === "RunError") { - terminalRunErrorCode ??= toStableRunErrorCode(payload.code); - } - }; + const runOutcome = createRunOutcomeTracker(childRunToolNames); let heartbeatTimer: ReturnType | undefined; stopHeartbeat = () => { if (heartbeatTimer) { @@ -1570,7 +1581,7 @@ export async function createRuntimeAgentStreamResponse( providerReplayStepOpen = false; } prepareToolResultIfNeeded(mappedEvent.event, mappedEvent.payload); - observeRunOutcomeEvent(mappedEvent.event, mappedEvent.payload); + runOutcome.observe(mappedEvent.event, mappedEvent.payload); enqueueIfAttached(mappedEvent.event, mappedEvent.payload); }; heartbeatTimer = setInterval( @@ -1629,12 +1640,12 @@ export async function createRuntimeAgentStreamResponse( } for (const mappedEvent of finalizeRunEvents(state, completedResponse)) { - observeRunOutcomeEvent(mappedEvent.event, mappedEvent.payload); + runOutcome.observe(mappedEvent.event, mappedEvent.payload); enqueueIfAttached(mappedEvent.event, mappedEvent.payload); } const finalStatus = state.sawTerminalError ? "failed" : "completed"; const terminalErrorCode = state.sawTerminalError - ? terminalRunErrorCode ?? "AgentRunTerminalError" + ? runOutcome.terminalErrorCode() ?? "AgentRunTerminalError" : undefined; if (state.sawTerminalError) { deps.sessionManager.failRun(input.runId); @@ -1651,8 +1662,7 @@ export async function createRuntimeAgentStreamResponse( "agent.run.final_status": finalStatus, "agent.run.saw_visible_output": state.sawVisibleOutput, "agent.run.saw_terminal_error": state.sawTerminalError, - "agent.run.tool_error_count": toolErrorCount, - "agent.run.child_run_error_count": childRunErrorCount, + ...runOutcome.spanAttributes(), // The RunError message can carry unclassified framework error text, so only // the stable code leaves the process. ...(terminalErrorCode ? { "error.type": terminalErrorCode } : {}), @@ -1679,8 +1689,7 @@ export async function createRuntimeAgentStreamResponse( sawVisibleOutput: state.sawVisibleOutput, sawTerminalError: state.sawTerminalError, finishReason: state.metadata.finishReason, - toolErrorCount, - childRunErrorCount, + ...runOutcome.logFields(), }; if (terminalErrorCode) { markSpanFailed(runSpan, terminalErrorCode); @@ -1703,8 +1712,7 @@ export async function createRuntimeAgentStreamResponse( status: "cancelled", }), "agent.run.final_status": "cancelled", - "agent.run.tool_error_count": toolErrorCount, - "agent.run.child_run_error_count": childRunErrorCount, + ...runOutcome.spanAttributes(), "error.type": "AgentRunCancelledError", "error.message": error.message, // The model call in flight at the abort may have been billed without @@ -1736,8 +1744,7 @@ export async function createRuntimeAgentStreamResponse( status: "failed", }), "agent.run.final_status": "failed", - "agent.run.tool_error_count": toolErrorCount, - "agent.run.child_run_error_count": childRunErrorCount, + ...runOutcome.spanAttributes(), "error.type": runErrorCode, "error.cause.type": error instanceof Error ? error.name : "Error", "error.message": errorMessage, diff --git a/src/metrics/index.ts b/src/metrics/index.ts index 8194ce318d..469b28af5c 100644 --- a/src/metrics/index.ts +++ b/src/metrics/index.ts @@ -1114,18 +1114,18 @@ function isBoundedTenantSample( ): boolean { if (!apply(regExpTest, DIRECT_METRIC_NAME, [name])) return false; const entries = apply(objectEntries, Object, [attributes]) as Array<[string, AttributeValue]>; - let projectAttributes = 0; + let ownAttributes = 0; for (let index = 0; index < entries.length; index++) { const entry = entries[index]; if (entry === undefined) continue; if (apply(arrayIncludes, PROJECT_LABEL_KEYS, [entry[0]])) continue; - projectAttributes++; + ownAttributes++; if (!apply(regExpTest, DIRECT_LABEL_KEY, [entry[0]])) return false; if (typeof entry[1] === "string" && entry[1].length > DIRECT_MAX_ATTRIBUTE_VALUE_LENGTH) { return false; } } - return projectAttributes <= DIRECT_MAX_TENANT_ATTRIBUTES; + return ownAttributes <= DIRECT_MAX_TENANT_ATTRIBUTES; } // Bounds what one tenant can add to the shared metrics backend from one diff --git a/src/proxy/logger.ts b/src/proxy/logger.ts index 1c5feb0ddb..56e58b40af 100644 --- a/src/proxy/logger.ts +++ b/src/proxy/logger.ts @@ -111,22 +111,23 @@ interface LogEntry { service: string; veryfrontVersion: string; message: string; - /** @deprecated Use `trace_id` instead. Kept for Grafana dashboard transition. */ + // camelCase aliases of the snake_case fields below. Remove each one once no + // Grafana dashboard, alert rule or saved Loki query filters on it. + /** @deprecated Use `trace_id`. */ traceId?: string; - /** @deprecated Use `span_id` instead. Kept for Grafana dashboard transition. */ + /** @deprecated Use `span_id`. */ spanId?: string; - // Request context fields (at top level for Grafana filtering) - /** @deprecated Use `request_id` instead. Kept for Grafana dashboard transition. */ + /** @deprecated Use `request_id`. */ requestId?: string; - /** @deprecated Use `project_slug` instead. Kept for Grafana dashboard transition. */ + /** @deprecated Use `project_slug`. */ projectSlug?: string; - /** @deprecated Use `project_id` instead. Kept for Grafana dashboard transition. */ + /** @deprecated Use `project_id`. */ projectId?: string; - /** @deprecated Use `release_id` instead. Kept for Grafana dashboard transition. */ + /** @deprecated Use `release_id`. */ releaseId?: string; - /** @deprecated Use `branch_id` instead. Kept for Grafana dashboard transition. */ + /** @deprecated Use `branch_id`. */ branchId?: string; - /** @deprecated Use `branch_name` instead. Kept for Grafana dashboard transition. */ + /** @deprecated Use `branch_name`. */ branchName?: string; // Standard snake_case fields shared with the runtime logger and the API, so // one Loki filter scopes lines from every service to a project. diff --git a/src/routing/registry/registry.ts b/src/routing/registry/registry.ts index ca5bc1d6b8..e8a3b57ad3 100644 --- a/src/routing/registry/registry.ts +++ b/src/routing/registry/registry.ts @@ -31,20 +31,18 @@ export function buildRouteRegistrySpanAttributes( attributes["project.id"] = projectId; } - if ((projectSlug || projectId) && ctx.environmentName) { - attributes["veryfront.environment_name"] = ctx.environmentName; - } - - if (projectSlug || projectId) { - const signedBranchName = ctx.branchId ? ctx.branchName : ctx.defaultBranchName; - if (ctx.branchId || signedBranchName) { - if (ctx.branchId) attributes["branch.id"] = ctx.branchId; - if (signedBranchName) attributes["branch.name"] = signedBranchName; - } else if (ctx.resolvedEnvironment === "production") { - if (ctx.releaseId) attributes["release.id"] = ctx.releaseId; - } else if (ctx.requestContext?.branch) { - attributes["branch.name"] = ctx.requestContext.branch; - } + if (!projectSlug && !projectId) return attributes; + + if (ctx.environmentName) attributes["veryfront.environment_name"] = ctx.environmentName; + + const signedBranchName = ctx.branchId ? ctx.branchName : ctx.defaultBranchName; + if (ctx.branchId || signedBranchName) { + if (ctx.branchId) attributes["branch.id"] = ctx.branchId; + if (signedBranchName) attributes["branch.name"] = signedBranchName; + } else if (ctx.resolvedEnvironment === "production") { + if (ctx.releaseId) attributes["release.id"] = ctx.releaseId; + } else if (ctx.requestContext?.branch) { + attributes["branch.name"] = ctx.requestContext.branch; } return attributes;