@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
JavaScript
;
// 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