UNPKG

@aws/aws-distro-opentelemetry-node-autoinstrumentation

Version:

This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.

369 lines 17.2 kB
"use strict"; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.VercelAISpanProcessor = void 0; const api_1 = require("@opentelemetry/api"); const semconv_1 = require("../common/semconv"); const instrumentation_utils_1 = require("../common/instrumentation-utils"); const version_1 = require("../../version"); const instrumentation_1 = require("./instrumentation"); class VercelAISpanProcessor { constructor() { // Span processor that translates VercelAI span attributes into OTel GenAI semantic conventions. // Vercel AI does not record whether a request was configured as an agentic workflow in its span attributes. // We detect agents by tracking child spans if it either has tool use or multiple LLM calls. this._spanIdToCounts = new Map(); } forceFlush() { this._spanIdToCounts.clear(); return Promise.resolve(); } shutdown() { this._spanIdToCounts.clear(); return Promise.resolve(); } onStart(_span, _parentContext) { } onEnd(span) { var _a, _b, _c; // https://github.com/vercel/ai/blob/5d0f18e52ed8e43e9916394aaf721585e0479d36/packages/otel/src/get-tracer.ts#L19 if (((_a = span.instrumentationScope) === null || _a === void 0 ? void 0 : _a.name) !== 'ai') return; const attrs = span.attributes; const operationId = attrs['ai.operationId']; if (!operationId || !operationId.startsWith('ai.')) return; span.instrumentationScope = { name: instrumentation_1.INSTRUMENTATION_NAME, version: version_1.LIB_VERSION }; if (operationId === 'ai.generateText.doGenerate' || operationId === 'ai.streamText.doStream' || operationId === 'ai.toolCall') { const parentSpanId = (_b = span.parentSpanContext) === null || _b === void 0 ? void 0 : _b.spanId; if (parentSpanId) { const signals = (_c = this._spanIdToCounts.get(parentSpanId)) !== null && _c !== void 0 ? _c : { llmCalls: 0, toolCalls: 0 }; if (operationId === 'ai.toolCall') { signals.toolCalls++; } else { signals.llmCalls++; } this._spanIdToCounts.set(parentSpanId, signals); } } const mutableAttrs = attrs; if (!mutableAttrs[semconv_1.ATTR_GEN_AI_OUTPUT_TYPE]) { const outputType = VercelAISpanProcessor.inferOutputType(operationId); if (outputType) { mutableAttrs[semconv_1.ATTR_GEN_AI_OUTPUT_TYPE] = outputType; } } for (const mapping of VercelAISpanProcessor.ATTRIBUTE_MAP) { if (!mapping.to) continue; const value = attrs[mapping.from]; if (value != null && !mutableAttrs[mapping.to]) { const mapped = mapping.transform ? mapping.transform(value, mutableAttrs) : value; if (mapped != null) { mutableAttrs[mapping.to] = mapped; } } } if (operationId === 'ai.generateText' || operationId === 'ai.streamText') { mutableAttrs[semconv_1.ATTR_GEN_AI_OPERATION_NAME] = this.isAgentSpan(span) ? semconv_1.GEN_AI_OPERATION_NAME_VALUE_INVOKE_AGENT : semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT; } else if (operationId === 'ai.toolCall') { mutableAttrs[semconv_1.ATTR_GEN_AI_OPERATION_NAME] = semconv_1.GEN_AI_OPERATION_NAME_VALUE_EXECUTE_TOOL; mutableAttrs[semconv_1.ATTR_GEN_AI_TOOL_TYPE] = 'function'; } else { const opName = VercelAISpanProcessor.OPERATION_MAP[operationId]; if (opName) { mutableAttrs[semconv_1.ATTR_GEN_AI_OPERATION_NAME] = opName; } } const opName = mutableAttrs[semconv_1.ATTR_GEN_AI_OPERATION_NAME]; if (opName === semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT || opName === semconv_1.GEN_AI_OPERATION_NAME_VALUE_EMBEDDINGS || opName === semconv_1.GEN_AI_OPERATION_NAME_VALUE_TEXT_COMPLETION || opName === semconv_1.GEN_AI_OPERATION_NAME_VALUE_GENERATE_CONTENT || opName === semconv_1.GEN_AI_OPERATION_NAME_VALUE_RETRIEVAL) { span.kind = api_1.SpanKind.CLIENT; } else if (opName === semconv_1.GEN_AI_OPERATION_NAME_VALUE_EXECUTE_TOOL || opName === semconv_1.GEN_AI_OPERATION_NAME_VALUE_INVOKE_AGENT) { span.kind = api_1.SpanKind.INTERNAL; } const spanName = VercelAISpanProcessor.createSpanName(mutableAttrs); if (spanName) { span.name = spanName; } for (const key of Object.keys(mutableAttrs)) { if ((key.startsWith('ai.') && !key.startsWith('ai.telemetry.metadata.')) || key === 'operation.name' || key === 'resource.name') { delete mutableAttrs[key]; } } } /** * Determines if a span represents an agent invocation. Per OTel GenAI semantic conventions, * "[the] combination of reasoning, logic, and access to external information that are all * connected to a Generative AI model invokes the concept of an agent." * See: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/ * * We detect this if the LLM used any tools or made more than one LLM call. */ isAgentSpan(span) { const spanId = span.spanContext().spanId; const signals = this._spanIdToCounts.get(spanId); this._spanIdToCounts.delete(spanId); if (!signals) return false; return signals.toolCalls > 0 || signals.llmCalls > 1; } static formatInputMessages(value) { try { const messages = typeof value === 'string' ? JSON.parse(value) : value; if (!Array.isArray(messages)) return value; const formatted = messages.map((msg) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const parts = []; if (typeof msg.content === 'string') { parts.push({ type: 'text', content: msg.content }); } else if (Array.isArray(msg.content)) { for (const part of msg.content) { if (part.type === 'text') { parts.push({ type: 'text', content: (_b = (_a = part.text) !== null && _a !== void 0 ? _a : part.content) !== null && _b !== void 0 ? _b : '' }); } else if (part.type === 'tool-call' || part.type === 'tool_call') { parts.push({ type: 'tool_call', id: (_d = (_c = part.toolCallId) !== null && _c !== void 0 ? _c : part.id) !== null && _d !== void 0 ? _d : null, name: (_f = (_e = part.toolName) !== null && _e !== void 0 ? _e : part.name) !== null && _f !== void 0 ? _f : '', arguments: typeof part.args === 'string' ? VercelAISpanProcessor.unwrapJsonString(part.args) : part.args, }); } else if (part.type === 'tool-result' || part.type === 'tool_call_response') { parts.push({ type: 'tool_call_response', id: (_h = (_g = part.toolCallId) !== null && _g !== void 0 ? _g : part.id) !== null && _h !== void 0 ? _h : null, response: (_k = (_j = part.result) !== null && _j !== void 0 ? _j : part.response) !== null && _k !== void 0 ? _k : '', }); } else { parts.push(part); } } } return { role: msg.role, parts }; }); return (0, instrumentation_utils_1.serializeToJson)(formatted); } catch (_a) { return value; } } static formatOutputMessages(value, attrs) { const finishReason = typeof attrs['ai.response.finishReason'] === 'string' ? VercelAISpanProcessor.mapFinishReason(attrs['ai.response.finishReason']) : 'stop'; return (0, instrumentation_utils_1.serializeToJson)([ { role: 'assistant', parts: [{ type: 'text', content: value }], finish_reason: finishReason, }, ]); } static formatToolDefinitions(tools) { if (!Array.isArray(tools)) return undefined; const parsed = tools.map((t) => { try { const def = JSON.parse(t); const result = { type: def.type || 'function', name: def.name, }; if (def.description) result.description = def.description; if (def.inputSchema) { const _a = def.inputSchema, { $schema, additionalProperties } = _a, params = __rest(_a, ["$schema", "additionalProperties"]); result.parameters = params; } return result; } catch (_b) { return t; } }); return (0, instrumentation_utils_1.serializeToJson)(parsed); } static createSpanName(attrs) { const op = attrs[semconv_1.ATTR_GEN_AI_OPERATION_NAME]; if (!op) return undefined; if (op === semconv_1.GEN_AI_OPERATION_NAME_VALUE_INVOKE_AGENT) { const agent = attrs[semconv_1.ATTR_GEN_AI_AGENT_NAME]; return agent ? `${op} ${agent}` : op; } if (op === semconv_1.GEN_AI_OPERATION_NAME_VALUE_EXECUTE_TOOL) { const tool = attrs[semconv_1.ATTR_GEN_AI_TOOL_NAME]; return tool ? `${op} ${tool}` : op; } const model = attrs[semconv_1.ATTR_GEN_AI_REQUEST_MODEL]; return model ? `${op} ${model}` : op; } static mapFinishReason(reason) { switch (reason) { case 'stop': return 'stop'; case 'length': return 'length'; case 'content-filter': return 'content_filter'; case 'tool-calls': return 'tool_call'; case 'error': return 'error'; case 'other': case 'unknown': return 'stop'; default: return reason; } } static mapProviderName(provider) { if (!provider) return provider; const lower = provider.toLowerCase(); if (instrumentation_utils_1.PROVIDER_MAP[lower]) return instrumentation_utils_1.PROVIDER_MAP[lower]; for (const [prefix, mapped] of Object.entries(instrumentation_utils_1.PROVIDER_MAP)) { if (lower.startsWith(prefix + '.') || lower.startsWith(prefix + '-')) { return mapped; } } return provider; } static inferOutputType(operationId) { if (operationId.startsWith('ai.generateText') || operationId.startsWith('ai.streamText')) { return semconv_1.GEN_AI_OUTPUT_TYPE_VALUE_TEXT; } if (operationId.startsWith('ai.generateObject') || operationId.startsWith('ai.streamObject')) { return semconv_1.GEN_AI_OUTPUT_TYPE_VALUE_JSON; } return undefined; } static unwrapJsonString(value) { try { const parsed = JSON.parse(value); if (typeof parsed === 'string') return parsed; if (typeof parsed === 'object' && parsed !== null) return JSON.stringify(parsed); return value; } catch (_a) { return value; } } } VercelAISpanProcessor.ATTRIBUTE_MAP = [ { from: 'ai.model.provider', to: semconv_1.ATTR_GEN_AI_PROVIDER_NAME, transform: (v) => VercelAISpanProcessor.mapProviderName(v), }, { from: 'ai.model.id', to: semconv_1.ATTR_GEN_AI_REQUEST_MODEL }, { from: 'ai.telemetry.functionId', to: semconv_1.ATTR_GEN_AI_AGENT_NAME }, { from: 'ai.usage.inputTokens', to: semconv_1.ATTR_GEN_AI_USAGE_INPUT_TOKENS }, { from: 'ai.usage.promptTokens', to: semconv_1.ATTR_GEN_AI_USAGE_INPUT_TOKENS }, { from: 'ai.usage.tokens', to: semconv_1.ATTR_GEN_AI_USAGE_INPUT_TOKENS }, { from: 'ai.usage.outputTokens', to: semconv_1.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS }, { from: 'ai.usage.completionTokens', to: semconv_1.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS }, { from: 'ai.usage.inputTokenDetails.cacheReadTokens', to: semconv_1.ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS }, { from: 'ai.usage.inputTokenDetails.cacheWriteTokens', to: semconv_1.ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS }, { from: 'ai.response.finishReason', to: semconv_1.ATTR_GEN_AI_RESPONSE_FINISH_REASONS, transform: (v) => [VercelAISpanProcessor.mapFinishReason(v)], }, { from: 'ai.response.id', to: semconv_1.ATTR_GEN_AI_RESPONSE_ID }, { from: 'ai.response.model', to: semconv_1.ATTR_GEN_AI_RESPONSE_MODEL }, { from: 'ai.settings.maxTokens', to: semconv_1.ATTR_GEN_AI_REQUEST_MAX_TOKENS }, { from: 'ai.settings.maxOutputTokens', to: semconv_1.ATTR_GEN_AI_REQUEST_MAX_TOKENS }, { from: 'ai.settings.temperature', to: semconv_1.ATTR_GEN_AI_REQUEST_TEMPERATURE }, { from: 'ai.settings.topP', to: semconv_1.ATTR_GEN_AI_REQUEST_TOP_P }, { from: 'ai.settings.topK', to: semconv_1.ATTR_GEN_AI_REQUEST_TOP_K }, { from: 'ai.settings.frequencyPenalty', to: semconv_1.ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY }, { from: 'ai.settings.presencePenalty', to: semconv_1.ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY }, { from: 'ai.settings.stopSequences', to: semconv_1.ATTR_GEN_AI_REQUEST_STOP_SEQUENCES }, { from: 'ai.prompt.messages', to: semconv_1.ATTR_GEN_AI_INPUT_MESSAGES, transform: (v) => VercelAISpanProcessor.formatInputMessages(v), }, { from: 'ai.prompt', to: semconv_1.ATTR_GEN_AI_INPUT_MESSAGES, transform: (v) => VercelAISpanProcessor.formatInputMessages(v), }, { from: 'ai.response.text', to: semconv_1.ATTR_GEN_AI_OUTPUT_MESSAGES, transform: (v, attrs) => VercelAISpanProcessor.formatOutputMessages(v, attrs), }, { from: 'ai.response.object', to: semconv_1.ATTR_GEN_AI_OUTPUT_MESSAGES, transform: (v, attrs) => VercelAISpanProcessor.formatOutputMessages(v, attrs), }, { from: 'ai.prompt.tools', to: semconv_1.ATTR_GEN_AI_TOOL_DEFINITIONS, transform: (v) => VercelAISpanProcessor.formatToolDefinitions(v), }, { from: 'ai.toolCall.name', to: semconv_1.ATTR_GEN_AI_TOOL_NAME }, { from: 'ai.toolCall.id', to: semconv_1.ATTR_GEN_AI_TOOL_CALL_ID }, { from: 'ai.toolCall.args', to: semconv_1.ATTR_GEN_AI_TOOL_CALL_ARGUMENTS, transform: (v) => VercelAISpanProcessor.unwrapJsonString(v), }, { from: 'ai.toolCall.result', to: semconv_1.ATTR_GEN_AI_TOOL_CALL_RESULT, transform: (v) => VercelAISpanProcessor.unwrapJsonString(v), }, ]; VercelAISpanProcessor.OPERATION_MAP = { 'ai.generateObject': semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT, 'ai.streamObject': semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT, 'ai.generateText.doGenerate': semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT, 'ai.generateText.doStream': semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT, 'ai.streamText.doStream': semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT, 'ai.generateObject.doGenerate': semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT, 'ai.streamObject.doStream': semconv_1.GEN_AI_OPERATION_NAME_VALUE_CHAT, 'ai.embed': semconv_1.GEN_AI_OPERATION_NAME_VALUE_EMBEDDINGS, 'ai.embed.doEmbed': semconv_1.GEN_AI_OPERATION_NAME_VALUE_EMBEDDINGS, 'ai.embedMany': semconv_1.GEN_AI_OPERATION_NAME_VALUE_EMBEDDINGS, 'ai.embedMany.doEmbed': semconv_1.GEN_AI_OPERATION_NAME_VALUE_EMBEDDINGS, 'ai.toolCall': semconv_1.GEN_AI_OPERATION_NAME_VALUE_EXECUTE_TOOL, }; exports.VercelAISpanProcessor = VercelAISpanProcessor; //# sourceMappingURL=span-processor.js.map