@loongsuite/otel-util-genai 用于创建符合 ARMS GenAI 语义规范的 OpenTelemetry Span。它负责生成核心 GenAI Span(Entry、Agent、ReAct Step、LLM、Tool)和扩展 GenAI Span(Embedding、Retrieval、Rerank、Memory),并写入模型、消息、Token、工具调用等语义属性。
本文重点说明最容易影响接入结果的三件事:
如何在"ARMS 探针自动采集"和"util 完全手动接入"之间选择。
如何保证
ENTRY -> AGENT -> STEP -> LLM/TOOL的调用链关系正确。如何在发布前验证属性、Token、消息内容和异常状态确实已上报。
手动埋点示例基于 Node.js 20/22 LTS、@loongsuite/otel-util-genai@0.1.0 和 OpenTelemetry JS 1.30.x 验证;探针示例基于 Node.js 20、@loongsuite/cms_node_sdk@1.0.4 和 openai@5.23.2 验证。该工具要求 Node.js 20 或更高版本。生产环境建议使用 LTS 版本。
1. 接入模式选择
@loongsuite/cms_node_sdk 探针和 @loongsuite/otel-util-genai 当前使用不同的 Trace/Context API。不要在同一进程中把二者当成同一套 Provider 组合使用,也不要初始化两套导出链路。
场景 | Trace 和 Exporter 由谁初始化 | 是否使用本 util | 能得到的 GenAI Span |
使用 | ARMS Node.js 探针 | 否 | 探针自动生成的 LLM Span |
需要完整的 Entry、Agent、Step、LLM、Tool 等自定义层级 | 应用初始化标准 OpenTelemetry JS | 是 | 应用按实际业务创建完整层级 |
1.1 使用 ARMS Node.js 探针自动采集
如果需求只是自动采集模型 SDK 调用,可使用 ARMS Node.js 探针。以下组合已经用 Node.js 20、真实 DashScope 请求和 ARMS 服务端数据验证:
npm install \
@loongsuite/cms_node_sdk@1.0.4 \
openai@5.23.2
export ARMS_LICENSE="<ARMS License>"
export CMS_SERVICE_NAME="weather-agent"
export ARMS_REGION_ID="cn-hongkong"
# 默认 workspace 不需要设置;非默认 workspace 才设置
# export ARMS_WORKSPACE="<workspace>"
node -r @loongsuite/cms_node_sdk/register app.jsARMS_LICENSE 是凭证,不能写入源码、镜像、日志或文档。服务名应在同一环境中唯一且稳定。
@loongsuite/cms_node_sdk@1.0.4 的 OpenAI 自动埋点声明支持 openai >=4 <6。OpenAI 6 不在该版本范围内;即使模型请求成功,也不能据此判断 LLM Span 已生成。升级 OpenAI 或探针后,必须重新做服务端验收。
此探针的预加载入口面向常驻服务。新服务首次启动时,探针还需要完成服务配置握手;CLI、一次性脚本、极短的 Serverless 进程可能在握手或批量导出前退出。短任务需要完整自定义层级或确定性 flush 时,请使用 1.2 的手动 OTLP 模式。
兼容边界:@loongsuite/cms_node_sdk@1.0.4 使用 CMS Trace/Context,并不会注册 @opentelemetry/api 的全局 TracerProvider。因此,直接调用 getExtendedTelemetryHandler() 会得到标准 OpenTelemetry API 一侧的默认 Handler,不能自动复用该探针,也不能保证父子关系和导出。本文不把这种组合列为受支持接入方式。
探针已经自动创建 LLM Span 时,不要再为同一次请求调用 startLlm()。如果还需要 Entry、Agent、Step、Tool 等业务 Span,请选择 1.2,用一套标准 OpenTelemetry Provider 完成全部埋点。
1.2 使用 util 完全手动接入
应用初始化一个标准 OpenTelemetry JS TracerProvider、SpanProcessor 和 OTLP Exporter,并把该 provider 显式传给 ExtendedTelemetryHandler。
这种模式下,应用手工创建所需 GenAI Span,并在退出前等待 forceFlush() 和 shutdown()。本文后续代码以该模式为主。
2. 前提条件
已开通并完成ARMS 应用监控接入。
使用 Node.js 20 或 22 LTS。
已从 ARMS 控制台取得当前应用和地域对应的 OTLP HTTP 接入地址及鉴权 Header。
已确认应用的
service.name在同一环境中唯一且稳定。埋点字段遵循LLM Trace 字段定义说明。
不要把 API Key、OTLP 鉴权 Header、完整用户输入或其他敏感数据写入源码和日志。
3. 安装依赖
为了保证示例可复现,本文固定安装已验证的正式版本,不使用裸包名。本文的验证基线是 @loongsuite/otel-util-genai@0.1.0;升级到后续版本后,必须按照第 17 节重新验证依赖安装、Span 树、属性和 OTLP 导出。所有预发布版本均不作为本文的生产接入基线。
npm install @loongsuite/otel-util-genai@0.1.0手动接入还需安装已经验证的 OpenTelemetry 1.30.x SDK 和 OTLP HTTP Exporter:
npm install \
@opentelemetry/api@1.9.1 \
@opentelemetry/sdk-trace-node@1.30.1 \
@opentelemetry/sdk-trace-base@1.30.1 \
@opentelemetry/resources@1.30.1 \
@opentelemetry/exporter-trace-otlp-http@0.57.2@loongsuite/otel-util-genai 当前 peer dependency 位于 OpenTelemetry JS 1.x 版本线。不要在没有完整兼容性验证的情况下把本文示例直接替换为 OpenTelemetry SDK 2.x。
3.1 OpenTelemetry 1.x 的安全边界
本文固定的 1.30.1 组合是 @loongsuite/otel-util-genai@0.1.0 的兼容性验收基线,不代表它包含 OpenTelemetry 2.x 的全部后续安全修复。依赖扫描会报告以下两个问题:
@opentelemetry/core <2.8.0的 W3C Baggage 入站解析没有在传播器内限制总大小和条目数(CVE-2026-54285)。Node.js 默认 16 KiB HTTP Header 上限会降低普通 HTTP 暴露面;网关仍应限制 Header 大小。自定义消息传输、提高 Header 上限或自定义TextMapGetter时,应在调用传播器前限制不可信 baggage 的大小和条目数。@opentelemetry/propagator-jaeger <2.9.0在解析畸形uber-trace-id或uberctx-*时可能导致进程退出(CVE-2026-59892)。该包会作为@opentelemetry/sdk-trace-node的传递依赖安装,但本文没有启用 Jaeger Propagator;默认 W3C TraceContext/Baggage 配置不受该问题影响。不要在此 1.x 组合中设置OTEL_PROPAGATORS=jaeger;如果历史系统必须接收 Jaeger Header,应在网关过滤或校验这些 Header,并规划经过完整兼容性测试的升级。
不要只为消除扫描告警就把部分依赖单独提升到 2.x,这可能造成 API/SDK 版本错配。应把升级作为独立变更,重新验证单元测试、OTLP 导出和 ARMS 服务端链路。安全公告见 GHSA-8988-4f7v-96qf 和 GHSA-45rx-2jwx-cxfr。
4. 配置 Resource 和内容采集策略
4.1 Resource
至少设置稳定的服务名,并为 ARMS 标记 GenAI 应用和埋点来源:
export OTEL_SERVICE_NAME="weather-agent"
export OTEL_RESOURCE_ATTRIBUTES="acs.arms.service.feature=genai_app,gen_ai.instrumentation.sdk.name=loongsuite-genai-utils"acs.arms.service.feature 和 gen_ai.instrumentation.sdk.name 是 Resource Attribute,不是普通 Span Attribute。若已经设置 OTEL_RESOURCE_ATTRIBUTES,请使用英文逗号追加,不能覆盖现有的 service.namespace、deployment.environment.name 等属性。
4.2 消息内容采集
本节环境变量只控制 util 的消息记录。应用直接使用 util 时,未设置环境变量会按 NO_CONTENT 处理,不采集完整消息内容。应在启动进程前显式设置所需模式:
export OTEL_SEMCONV_STABILITY_OPT_IN="gen_ai_latest_experimental"
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT="SPAN_ONLY"可选值如下:
值 | Span 属性 | Event | 建议用途 |
| 不记录 | 不记录 | 默认生产配置 |
| 记录 | 不记录 | 调试或经脱敏的业务 |
| 不记录 | 记录 | 已配置 OTel Logs 时使用 |
| 记录 | 记录 | 仅在确认重复存储和合规风险后使用 |
消息内容可能包含个人信息、业务机密、提示词和工具参数。生产环境开启前,应先完成脱敏、权限和保存周期评估。
@loongsuite/cms_node_sdk@1.0.4 的 OpenAI 自动埋点使用探针自己的内容采集配置,不能用上述 util 环境变量推断其行为。该版本默认会记录 OpenAI 请求和响应内容;如果业务不允许内容进入 Trace,又不能在探针侧确认关闭,请改用手动模式并保持 NO_CONTENT。
5. 完全手动接入时初始化 OpenTelemetry
以下初始化文件必须先于业务模块加载:
// telemetry.mjs
import { trace } from "@opentelemetry/api";
import { OTLPTraceExporter } from
"@opentelemetry/exporter-trace-otlp-http";
import {
Resource,
detectResourcesSync,
envDetectorSync,
} from "@opentelemetry/resources";
import {
BatchSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import {
NodeTracerProvider,
} from "@opentelemetry/sdk-trace-node";
import {
ExtendedTelemetryHandler,
} from "@loongsuite/otel-util-genai";
const detected = detectResourcesSync({
detectors: [envDetectorSync],
});
const resource = Resource.default()
.merge(detected)
.merge(new Resource({
"service.name":
process.env.OTEL_SERVICE_NAME ?? "weather-agent",
"acs.arms.service.feature": "genai_app",
"gen_ai.instrumentation.sdk.name":
"loongsuite-genai-utils",
}));
const provider = new NodeTracerProvider({ resource });
provider.addSpanProcessor(
new BatchSpanProcessor(new OTLPTraceExporter()),
);
provider.register();
export const handler = new ExtendedTelemetryHandler({
tracerProvider: provider,
});
export const tracer = trace.getTracer("weather-agent", "1.0.0");
export async function shutdownTelemetry() {
await provider.forceFlush();
await provider.shutdown();
}从 ARMS 控制台复制接入参数。变量名称和 URL 形式以控制台给出的示例为准,不要自行拼接地域、路径或鉴权参数。
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="<控制台提供的 traces endpoint>"
export OTEL_EXPORTER_OTLP_HEADERS="<控制台提供的鉴权 Header>"如果控制台给出的是通用 OTEL_EXPORTER_OTLP_ENDPOINT,应按控制台示例使用该变量。
应用入口必须先加载 telemetry.mjs,并在进程退出前刷新数据:
import {
handler,
shutdownTelemetry,
} from "./telemetry.mjs";
import { runRequest } from "./agent.mjs";
try {
await runRequest({ handler });
} finally {
await shutdownTelemetry();
}ConsoleSpanExporter 只能用于本地观察,不能把数据发送到 ARMS。
6. Span 类型与命名
Span 名称和 gen_ai.operation.name 是两个不同字段,不应混用。
类型 | Span 名称 |
| 工厂函数 |
Entry |
|
|
|
Agent |
|
|
|
ReAct Step |
|
|
|
LLM |
|
|
|
Tool |
|
|
|
Embedding |
|
|
|
Retrieval |
|
|
|
Rerank |
|
|
|
Memory |
|
|
|
典型 Agent 请求的链路应为:
ENTRY enter_ai_application_system
AGENT invoke_agent WeatherAgent
STEP react step
LLM chat qwen-plus
TOOL execute_tool get_weather
STEP react step
LLM chat qwen-plusTool 是否与 LLM 同属一个 Step,取决于应用的实际执行模型。不要为了得到固定截图而伪造父子关系。
7. Node.js 上下文传播规则
这是 Node.js 接入中最重要的规则。
7.1 不会自动激活上下文
startXxx() 会把新 Span 对应的 OTel Context 保存到 invocation.contextToken,但不会把它自动设为 context.active()。
创建手工子 Span 时,必须把父 invocation 的 contextToken 作为第二个参数传入:
handler.startInvokeAgent(agentInv, entryInv.contextToken);
handler.startReactStep(stepInv, agentInv.contextToken);
handler.startLlm(llmInv, stepInv.contextToken);
handler.startExecuteTool(toolInv, stepInv.contextToken);调用可能被自动埋点的模型 SDK、HTTP 客户端或数据库时,还必须使用 context.with() 激活对应上下文:
import { context } from "@opentelemetry/api";
const response = await context.with(
llmInv.contextToken,
() => modelClient.chat.completions.create(request),
);否则,自动生成的 SDK/HTTP Span 可能成为 LLM Span 的兄弟节点,甚至生成另一条 trace。
7.2 公共属性通过 Baggage 向下继承
Entry 上的 sessionId、userId 和 Agent 上的 agentName 会写入 OTel Baggage。以它们的 contextToken 为父上下文创建的后续 GenAI Span,可以继承:
gen_ai.session.idgen_ai.user.idgen_ai.agent.name
继承依赖正确的 parent Context。父子关系断开时,这些公共属性也可能缺失。
7.3 为何本文使用显式 start/stop
Handler 也提供 entry()、invokeAgent()、llm() 等回调式接口,但回调式接口本身不会自动以新 invocation 的 Context 执行回调。
只要回调内部还会调用自动埋点 SDK,或需要创建多层 GenAI Span,就应使用显式的 startXxx()、context.with()、stopXxx()/failXxx(),使上下文边界一目了然。
8. 创建 Entry、Agent 和 ReAct Step
import {
createEntryInvocation,
createInvokeAgentInvocation,
createReactStepInvocation,
} from "@loongsuite/otel-util-genai";
const entryInv = createEntryInvocation({
sessionId,
userId,
agentName: "WeatherAgent",
inputMessages: [{
role: "user",
parts: [{ type: "text", content: userMessage }],
}],
});
handler.startEntry(entryInv);
const agentInv = createInvokeAgentInvocation("dashscope", {
agentName: "WeatherAgent",
agentDescription: "先查询天气工具,再回答用户问题。",
requestModel: "qwen-plus",
});
handler.startInvokeAgent(agentInv, entryInv.contextToken);
const stepInv = createReactStepInvocation({ round: 1 });
handler.startReactStep(stepInv, agentInv.contextToken);完成时从内向外结束:
stepInv.finishReason = "stop";
handler.stopReactStep(stepInv);
agentInv.inputTokens = totalInputTokens;
agentInv.outputTokens = totalOutputTokens;
agentInv.outputMessages = finalOutputMessages;
handler.stopInvokeAgent(agentInv);
entryInv.outputMessages = finalOutputMessages;
handler.stopEntry(entryInv);如果上游返回可靠的 total_tokens,可以显式设置 totalTokens。未设置时,工具会根据 input 和 output Token 计算总量。
9. LLM Span:自动采集和手工采集二选一
9.1 模型 SDK 已被同一标准 OpenTelemetry Provider 自动采集
本节只适用于自动埋点和 util 明确复用同一个 @opentelemetry/api TracerProvider 的场景,不适用于 1.1 的 @loongsuite/cms_node_sdk@1.0.4。
不要再创建手工 LLM Span。只需让模型调用在 Step 的 Context 中执行:
const response = await context.with(
stepInv.contextToken,
() => modelClient.chat.completions.create(request),
);发布前必须确认一次模型请求只有一个 LLM Span,并确认该自动 LLM Span 的 parentSpanId 指向对应 Step。
9.2 完全手工创建 LLM Span
以下代码中的辅助函数 toGenAIInputMessages、toGenAIToolDefinitions 定义见第 10 节,toGenAIOutputMessage 定义见第 10 节,toSafeGenAIError 定义见第 12 节。
import {
createLLMInvocation,
} from "@loongsuite/otel-util-genai";
const llmInv = createLLMInvocation({
provider: "dashscope",
operationName: "chat",
requestModel: "qwen-plus",
inputMessages: toGenAIInputMessages(messages),
toolDefinitions: toGenAIToolDefinitions(tools),
});
handler.startLlm(llmInv, stepInv.contextToken);
try {
const response = await context.with(
llmInv.contextToken,
() => modelClient.chat.completions.create({
model: "qwen-plus",
messages,
tools,
}),
);
const choice = response.choices?.[0];
if (!choice?.message) {
throw new Error("The model response has no first choice");
}
llmInv.responseId = response.id ?? null;
llmInv.responseModelName = response.model ?? "qwen-plus";
llmInv.finishReasons = [
choice.finish_reason ?? "stop",
];
llmInv.outputMessages = [
toGenAIOutputMessage(
choice.message,
choice.finish_reason,
),
];
if (response.usage) {
llmInv.inputTokens =
response.usage.prompt_tokens ?? null;
llmInv.outputTokens =
response.usage.completion_tokens ?? null;
llmInv.totalTokens =
response.usage.total_tokens ?? null;
}
handler.stopLlm(llmInv);
} catch (error) {
handler.failLlm(
llmInv,
toSafeGenAIError(
error,
"LLMError",
"LLM request failed",
),
);
throw error;
}不要在发起请求前结束 LLM Span;否则耗时、自动子 Span 和异常状态都会失真。
10. 正确转换 OpenAI 兼容消息
不能只转换 { role, content }。Agent 的第二轮模型输入还包含 assistant tool_calls 和 role 为 tool 的工具结果;丢失这些字段会导致 ARMS 无法还原一次完整工具调用。
export function toGenAIMessageFinishReason(reason) {
return reason === "tool_calls" ? "tool_call" : reason || "stop";
}
function textPart(content) {
return { type: "text", content };
}
function toolCallPart(toolCall) {
return {
type: "tool_call",
id: toolCall.id ?? null,
name: toolCall.function.name,
arguments: toolCall.function.arguments,
};
}
export function toGenAIInputMessages(messages) {
return messages.map((message) => {
if (message.role === "tool") {
return {
role: "tool",
parts: [{
type: "tool_call_response",
id: message.tool_call_id ?? null,
response: message.content ?? "",
}],
};
}
if (
message.content != null &&
typeof message.content !== "string"
) {
throw new TypeError(
"This example only accepts string message content.",
);
}
const parts = [];
if (message.content) {
parts.push(textPart(message.content));
}
for (const toolCall of message.tool_calls ?? []) {
parts.push(toolCallPart(toolCall));
}
return { role: message.role, parts };
});
}
export function toGenAIOutputMessage(message, finishReason) {
const parts = [];
if (message.content) {
parts.push(textPart(message.content));
}
for (const toolCall of message.tool_calls ?? []) {
parts.push(toolCallPart(toolCall));
}
return {
role: "assistant",
parts,
finishReason: toGenAIMessageFinishReason(finishReason),
};
}
export function toGenAIToolDefinitions(tools) {
return tools.map((tool) => ({
type: "function",
name: tool.function.name,
description: tool.function.description ?? null,
parameters: tool.function.parameters ?? {},
}));
}Span 属性和消息 JSON 使用不同的 finish reason 取值。不要对两者应用同一转换:
模型返回 choice.finish_reason = "tool_calls"
gen_ai.response.finish_reasons = ["tool_calls"]
output message.finish_reason = "tool_call"gen_ai.response.finish_reasons 应保留模型提供商返回的原始值;只有写入 output message Schema 时,才把复数 tool_calls 转成单数 tool_call。
toolCall.function.arguments 通常是 JSON 字符串。除非业务已经成功解析并希望按对象记录,否则应保留模型的原始值,避免埋点数据与实际请求不一致。
11. 创建 Tool Span
import {
createExecuteToolInvocation,
} from "@loongsuite/otel-util-genai";
const toolInv = createExecuteToolInvocation(
toolCall.function.name,
{
toolCallId: toolCall.id ?? null,
toolDescription: "查询指定城市的天气",
toolType: "function",
toolCallArguments: toolCall.function.arguments,
},
);
handler.startExecuteTool(toolInv, stepInv.contextToken);
try {
const result = await context.with(
toolInv.contextToken,
() => dispatchTool(
toolCall.function.name,
toolCall.function.arguments,
),
);
toolInv.toolCallResult = result;
handler.stopExecuteTool(toolInv);
} catch (error) {
handler.failExecuteTool(
toolInv,
toSafeGenAIError(
error,
"ToolError",
"Tool execution failed",
),
);
throw error;
}工具调用完成后,还应把结果以 OpenAI 兼容格式追加到下一轮输入:
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result,
});toolCallId 必须在 assistant tool call、Tool Span 和 tool response 三处保持一致。
toolCallArguments 和 toolCallResult 会直接写入 Tool Span,不受消息内容采集开关控制。设置这两个字段前必须完成脱敏;不允许上报的内容应省略,而不是寄希望于 NO_CONTENT 自动过滤。
12. 异常处理必须逐层收口
每个已经开始且仍在 recording 的 invocation 都必须调用一次对应的 stopXxx() 或 failXxx()。
原始异常消息可能包含模型响应体、请求参数、文件路径或凭证。failXxx() 会把传入的 message 写入 Span Status,因此不能直接传入 error.message 或 String(error)。应使用固定的安全消息,并只保留经过约束的错误类型:
const SAFE_ERROR_TYPE_PATTERN =
/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
function toSafeGenAIError(error, fallbackType, safeMessage) {
const candidateType =
error instanceof Error
? error.constructor?.name
: null;
const type =
typeof candidateType === "string" &&
SAFE_ERROR_TYPE_PATTERN.test(candidateType)
? candidateType
: fallbackType;
return { type, message: safeMessage };
}嵌套调用失败时,应从内向外标记:
failLlm / failExecuteTool
-> failReactStep
-> failInvokeAgent
-> failEntry推荐在外层 catch 中先检查:
if (stepInv.span?.isRecording()) {
handler.failReactStep(
stepInv,
toSafeGenAIError(
error,
"StepError",
"Agent step failed",
),
);
}这样可以避免已经结束的 Span 被重复结束。failXxx() 会记录错误状态和 error.type,调用后仍需重新抛出原始异常,不能吞掉业务错误。应用如需记录日志,也应使用固定安全消息或经过审核的脱敏字段,不能直接输出原始异常。
13. 创建普通业务 Span
不属于 GenAI 语义的内部操作使用原生 OTel Span。例如在 Tool 内记录参数校验:
import {
context,
SpanStatusCode,
trace,
} from "@opentelemetry/api";
const validationSpan = tracer.startSpan(
"validate_weather_arguments",
{ attributes: { "app.validation.type": "json-schema" } },
toolInv.contextToken,
);
const validationContext = trace.setSpan(
toolInv.contextToken,
validationSpan,
);
try {
await context.with(
validationContext,
() => validateArguments(argumentsJson),
);
} catch (error) {
const safeError = toSafeGenAIError(
error,
"ValidationError",
"Argument validation failed",
);
validationSpan.recordException({
name: safeError.type,
message: safeError.message,
});
validationSpan.setAttribute(
"error.type",
safeError.type,
);
validationSpan.setStatus({
code: SpanStatusCode.ERROR,
message: safeError.message,
});
throw error;
} finally {
validationSpan.end();
}自定义属性应使用业务命名空间,例如 app.*。不要用自定义值覆盖 gen_ai.*、server.*、error.type 等由工具维护的字段。
14. 其他 GenAI Span 类型
Embedding、Retrieval、Rerank、Memory 等扩展 Span 类型遵循同一生命周期:创建 invocation、传入父 Context、在真实操作完成后补充结果、正常或异常结束。异常处理模式与第 12 节一致(try/catch + failXxx()),以下示例省略异常路径以突出核心字段。
const embeddingInv = createEmbeddingInvocation(
"text-embedding-v3",
{ provider: "dashscope" },
);
handler.startEmbedding(embeddingInv, stepInv.contextToken);
// 调用 embedding API
embeddingInv.inputTokens = usage.prompt_tokens;
embeddingInv.dimensionCount = vectors[0].length;
handler.stopEmbedding(embeddingInv);
const retrievalInv = createRetrievalInvocation({
dataSourceId: "product-docs",
query,
topK: 5,
});
handler.startRetrieval(retrievalInv, stepInv.contextToken);
retrievalInv.documents = documents.map((document) => ({
id: document.id,
score: document.score,
content: document.content,
metadata: document.metadata,
}));
handler.stopRetrieval(retrievalInv);14.1 多模态输入(图片 URL)
应用发送给模型的请求对象和写入 GenAI Span 的消息对象是两套 Schema,应分别构造。以下示例使用模型实际接收的图片 URL,因此在 GenAI 消息中映射为 Uri Part:
const prompt = "请用一句话描述这张图片。";
const imageUrl = "https://example.com/image.jpg";
const inputMessages = [{
role: "user",
parts: [
{ type: "text", content: prompt },
{
type: "uri",
mimeType: "image/jpeg",
modality: "image",
uri: imageUrl,
},
],
}];
const llmInv = createLLMInvocation({
provider: "dashscope",
operationName: "chat",
requestModel: "qwen3-vl-plus",
inputMessages,
outputType: "text",
});
handler.startLlm(llmInv, entryInv.contextToken);
try {
const response = await context.with(
llmInv.contextToken,
() => modelClient.chat.completions.create({
model: "qwen3-vl-plus",
messages: [{
role: "user",
content: [
{ type: "text", text: prompt },
{
type: "image_url",
image_url: { url: imageUrl },
},
],
}],
}),
);
const choice = response.choices?.[0];
if (!choice?.message) {
throw new Error("The model response has no first choice");
}
llmInv.responseId = response.id ?? null;
llmInv.responseModelName =
response.model ?? "qwen3-vl-plus";
llmInv.finishReasons = [
choice.finish_reason ?? "stop",
];
llmInv.inputTokens =
response.usage?.prompt_tokens ?? null;
llmInv.outputTokens =
response.usage?.completion_tokens ?? null;
llmInv.totalTokens =
response.usage?.total_tokens ?? null;
llmInv.outputMessages = [{
role: "assistant",
parts: [{
type: "text",
content: choice.message.content ?? "",
}],
finishReason: choice.finish_reason ?? "stop",
}];
handler.stopLlm(llmInv);
} catch (error) {
handler.failLlm(
llmInv,
toSafeGenAIError(
error,
"LLMError",
"Multimodal LLM request failed",
),
);
throw error;
}TypeScript 公共 API 使用 mimeType。在 SPAN_ONLY 或 SPAN_AND_EVENT 模式下,0.1.0 会自动生成:
gen_ai.input.messages
gen_ai.input.multimodal_metadata两个属性中的 URI 数据都使用 Schema 字段 mime_type:
[
{
"type": "uri",
"mime_type": "image/jpeg",
"uri": "https://example.com/image.jpg",
"modality": "image"
}
]multimodal metadata 只汇总最终消息中的 Uri Part,不包含 Text、Blob、Base64Blob 或 File。该工具只记录遥测数据,不负责上传媒体、调用模型或把 provider 文件对象转换成 URI。
14.2 File Part 的支持边界
File Part 在 TypeScript 中使用:
{
type: "file",
mimeType: "application/pdf",
modality: "document",
fileId: "file-123",
}写入消息 JSON 时会转换为 mime_type 和 file_id。但是不同模型提供商对文件上传和 file ID 引用的接口差异较大,本文没有把 File Part 作为已经完成真实模型端到端验证的示例。只有在应用确实使用 provider 返回的 file ID 发起模型请求时,才应按实际请求映射该 Part;不要为了展示字段而构造虚假的 file ID。
14.3 Event Log 的字段形式
直接调用 TypeScript 工厂函数时使用 camelCase:
{ type: "uri", mimeType: "image/png", modality: "image", uri }
{ type: "file", mimeType: "application/pdf", modality: "document", fileId }Event Log 是线上的 JSON Schema,输入记录必须使用 snake_case:
{
"type": "uri",
"mime_type": "image/png",
"modality": "image",
"uri": "https://example.com/input.png"
}{
"type": "file",
"mime_type": "application/pdf",
"modality": "document",
"file_id": "file-123"
}不要把 TypeScript 对象字段和 Event Log Schema 混用。convertEventLogToTrace() 会根据最终 input/output messages 分别生成:
gen_ai.input.multimodal_metadata
gen_ai.output.multimodal_metadata两个 metadata 都只汇总各自消息中的 URI Part。配套 Event Log Demo 使用 image/png 输入 URI 和 image/webp 输出 URI 验证双向序列化;它验证的是 Event Log 转换和 OTLP 上报,不代表某个模型真实生成了图片。
15. 本地测试和端到端验证
15.1 离线单元测试
测试不应只断言"生成了 Span",还应检查:
所有 Span 的 traceId 相同。
Agent 的 parentSpanId 指向 Entry。
Step 的 parentSpanId 指向 Agent。
LLM 和 Tool 的 parentSpanId 指向对应 Step。
自动生成的 SDK/HTTP Span 位于手工 LLM 或 Tool Span 下。
gen_ai.session.id、gen_ai.user.id、gen_ai.agent.name已继承。LLM 和 Agent 的 Token 值正确。
assistant tool call 和 tool response 都被保留。
多模态字段使用
mime_type/file_id,没有残留 camelCase。multimodal metadata 只包含 URI Part。
异常链上的 Span 均为 Error。
下载本文配套 Demo 后执行:
cd nodejs-genai-util-demo
npm ci
npm test
npm run demoDemo 的 package.json 应固定依赖本文验证的 npm 版本。对客 Demo 不应依赖 file:../... 本地源码路径。
15.2 真实模型验证
export DASHSCOPE_API_KEY="<your-api-key>"
npm run demo:dashscope这一步使用内存 Exporter,目的是单独确认模型、Tool Calling 和消息转换正确。检查模型确实完成了工具调用,并记录程序输出的 traceId。不要在日志中打印 API Key。
15.3 Tool Calling 到 OTLP 的组合验证
export OTEL_SERVICE_NAME="weather-agent-validation"
export OTEL_RESOURCE_ATTRIBUTES="service.name=weather-agent-validation"
export OTEL_EXPORTER_OTLP_ENDPOINT="<ARMS endpoint>"
export OTEL_EXPORTER_OTLP_HEADERS="<ARMS auth header>"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export GENAI_DEMO_ALLOW_CONTENT_EXPORT="true"
npm run demo:e2eGENAI_DEMO_ALLOW_CONTENT_EXPORT=true 是配套 Demo 的安全确认开关,表示操作者确认示例中的完整输入和输出可以发送到所配置的 OTLP 后端;它不会自动脱敏。只应使用公开、虚构或已经脱敏的数据。
只有 forceFlush() 和 shutdown() 都成功后,Demo 才输出:
export completed traceId=<trace-id>导出器返回成功只能证明客户端已完成导出请求,不能替代 ARMS 控制台验收。
如果控制台给出的是 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,应按控制台示例使用该变量,不能自行改写 URL。
15.4 图片 URL 到 OTLP 的组合验证
export DASHSCOPE_API_KEY="<your-api-key>"
export OTEL_SERVICE_NAME="multimodal-agent-validation"
export OTEL_RESOURCE_ATTRIBUTES="service.name=multimodal-agent-validation"
export OTEL_EXPORTER_OTLP_ENDPOINT="<ARMS endpoint>"
export OTEL_EXPORTER_OTLP_HEADERS="<ARMS auth header>"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export GENAI_DEMO_ALLOW_CONTENT_EXPORT="true"
npm run demo:multimodal-e2e图片必须是不含敏感内容的公开 HTTPS URL,不得包含用户名、密码、query、fragment 或临时签名参数。完整 URI 会同时写入 gen_ai.input.messages 和 gen_ai.input.multimodal_metadata。
程序应输出真实模型回答、util.version、模型名、response ID、finish reason、Token 和 traceId。模型回答必须体现图片内容;只得到 HTTP 200 不能证明模型读取了图片。
15.5 Event Log 到 OTLP 的组合验证
这个用例不调用模型,只验证 Event Log 转换、input/output 多模态字段和 OTLP 上报:
export OTEL_SERVICE_NAME="event-log-validation"
export OTEL_RESOURCE_ATTRIBUTES="service.name=event-log-validation"
export OTEL_EXPORTER_OTLP_ENDPOINT="<ARMS endpoint>"
export OTEL_EXPORTER_OTLP_HEADERS="<ARMS auth header>"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export GENAI_DEMO_ALLOW_CONTENT_EXPORT="true"
npm run demo:event-log-e2e程序应输出 span.count=4、input.mime_type=image/png、output.mime_type=image/webp 和 traceId。服务端应得到 ENTRY -> AGENT -> STEP -> LLM,并同时存在 gen_ai.input.multimodal_metadata 和 gen_ai.output.multimodal_metadata。
15.6 ARMS Node.js 探针自动采集验证
这个用例与手动 util Demo 分开安装,避免混入第二套 Provider:
cd nodejs-genai-util-demo/arms-probe-demo
npm ci
export DASHSCOPE_API_KEY="<your-api-key>"
export ARMS_LICENSE="<ARMS License>"
export CMS_SERVICE_NAME="probe-openai-validation"
export ARMS_REGION_ID="cn-hongkong"
# 默认 workspace 不需要设置
# export ARMS_WORKSPACE="<workspace>"
node -r @loongsuite/cms_node_sdk/register app.js配套验证程序固定 @loongsuite/cms_node_sdk@1.0.4 和 openai@5.23.2,默认等待 65 秒完成新服务的首次配置握手,再真实调用 DashScope。已注册服务可以设置 PROBE_WARMUP_MS=0 缩短验证。
服务端至少应得到 HTTP SERVER → openai.chat LLM → 模型 HTTP CLIENT 的父子关系。openai.chat 应包含:
gen_ai.span.kind=LLM和gen_ai.operation.name=chat。request/response model、response ID 和 finish reason。
input/output/total Token。
输入和输出消息(仅在业务允许记录内容时)。
otel.scope.name=openai、otel.scope.version=1.0.4。Resource 中的
telemetry.sdk.name=cms_node_sdk和正确service.name。
如果日志出现 BatchSpanProcessor: span export failed、HTTP 404/403,或 ARMS 中查不到服务,不得仅凭模型返回 200 判定接入成功。确认 License、地域、workspace 和服务配置一致,并为新服务保留首次配置握手时间;短任务改用手动 OTLP 模式。
15.7 验证接入结果
使用输出的 traceId 在 ARMS 控制台的LLM 应用监控页面下的调用链分析逐项确认:
trace 能在正确地域、正确应用和正确
service.name下检索到。调用链树与业务执行顺序一致。
一个模型请求只有一个 LLM Span。
每个 GenAI Span 均有正确的
gen_ai.span.kind和gen_ai.operation.name。LLM 包含
gen_ai.request.model、输入/输出/总 Token。Agent Token 等于其包含的模型调用汇总。
Tool 包含名称、call ID、参数和结果。
开启内容采集后,第二轮 LLM 输入包含 tool call 与 tool response。
Entry、Agent、Step、LLM、Tool 都有 session/user/agent 公共属性。
失败用例中的所有打开 Span 都标记为 Error。
图片验证的
gen_ai.input.messages包含type=uri、mime_type、modality和真实 URL。gen_ai.input.multimodal_metadata是 JSON 数组,且只汇总 URI Part。Event Log 验证的
gen_ai.output.messages和gen_ai.output.multimodal_metadata包含输出 URI,并使用mime_type。消息 JSON 和 metadata 中没有残留
mimeType/fileId。otel.scope.version与实际安装的 npm 版本一致。Resource 中存在
service.name、acs.arms.service.feature=genai_app和gen_ai.instrumentation.sdk.name=loongsuite-genai-utils。
网站示例维护方还应在 Node.js 20 和 22 的干净目录中安装相同 npm 版本分别复验。未完成服务端查询、仍依赖本地源码或版本不一致时,不能把示例标记为已验证。
16. 常见问题
ARMS 中完全没有数据
检查使用的 endpoint、地域和鉴权 Header 是否来自同一应用。
检查
service.name。确认使用的是 OTLP Exporter,而不是 ConsoleSpanExporter。
确认进程退出前执行了
forceFlush()和shutdown()。检查 Exporter 错误日志和网络出口。
使用 ARMS Node.js 探针时,还要检查 ARMS_LICENSE、ARMS_REGION_ID、ARMS_WORKSPACE 和 CMS_SERVICE_NAME。新服务首次启动应保留配置握手时间;模型调用成功但出现 exporter 404/403,仍属于上报失败。
ARMS 探针下没有 openai.chat
确认 Node.js 启动参数确实包含
-r @loongsuite/cms_node_sdk/register,并且探针先于openai加载。@loongsuite/cms_node_sdk@1.0.4只声明支持openai >=4 <6,OpenAI 6 不在已验证范围。检查启动日志是否列出
openaiinstrumentation。用 ARMS 服务端数据确认,而不是只看模型响应。
ExtendedTelemetryHandler 没有导出 Span
@loongsuite/cms_node_sdk@1.0.4 没有注册标准 @opentelemetry/api Provider,不能被默认 Handler 直接复用。不要把两条路线混接;需要 util 时,按第 5 节初始化标准 OpenTelemetry Provider 和 OTLP Exporter。
同一次模型调用出现两个 LLM Span
通常是模型 SDK 已被自动埋点采集,同时业务又调用了 startLlm()。同一 Provider 中只保留一种 LLM 埋点;@loongsuite/cms_node_sdk@1.0.4 和 util 则应按第 1 节完全分开接入。
Span 在同一条 trace 中,但层级错误
手工子 Span 是否显式传入父
contextToken。自动埋点操作是否在
context.with()内执行。是否跨越了未正确传播 AsyncLocalStorage Context 的自定义异步边界。
子 Span 缺少 session、user 或 agent
检查 Entry/Agent 是否设置了对应字段,并确认子 Span 使用了父 invocation 的 contextToken。
看不到输入和输出消息
确认环境变量在 Node.js 进程启动前设置为:
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY然后检查消息转换结果是否符合 { role, parts } 结构。
有图片消息,但没有 multimodal metadata
确认安装的是本文已验证的正式版
0.1.0;使用后续版本时应重新执行第 17 节的完整验证。确认内容采集模式为
SPAN_ONLY或SPAN_AND_EVENT。metadata 只汇总
type: "uri"的 Part,不汇总 Blob、Base64Blob 或 File。直接调用 TypeScript API 时检查
mimeType、modality和uri。Event Log 输入时检查 Schema 字段
mime_type、modality和uri。检查是否通过
invocation.attributes显式覆盖了自动生成的属性。
File Part 没有出现在 multimodal metadata
这是预期行为。File Part 会保留在消息 JSON 中,并把 fileId 序列化为 file_id,但 URI metadata 只汇总 Uri Part。只有 provider 的真实请求确实使用 file ID 时,才应记录 File Part。
安装时出现 ERESOLVE
检查项目是否混用了 OpenTelemetry SDK 1.x 和 2.x。本文验证组合为 API 1.9.1、trace/resources 1.30.1、OTLP exporter 0.57.2。
短任务偶发丢 Span
手动 OTLP 模式不要依赖进程自然退出。CLI、脚本、Serverless handler 和测试程序都应显式等待 forceFlush()/shutdown()。
@loongsuite/cms_node_sdk/register 的信号处理会在 SIGINT/SIGTERM 时执行 shutdown,但新服务还需要首次配置握手。对极短任务,优先使用可显式控制生命周期的手动 OTLP 模式。
17. 相关资料
18. 配套 Demo
离线模型和内存 Exporter。
真实 DashScope OpenAI 兼容接口调用。
真实 DashScope Tool Calling 到 OTLP 的组合验证。
真实 Qwen-VL 图片 URL 到 OTLP 的组合验证。
Event Log input/output 多模态 metadata 到 OTLP 的组合验证。
独立的 ARMS Node.js 探针 + OpenAI 5 自动埋点验证。
OTLP HTTP 导出。
Tool 消息转换。
多模态 URI、
mime_type和自动 metadata 验证。正常链路和异常链路单元测试。
context.with()对自动子 Span 父子关系的验证。
本文使用固定 commit 引用已经验收的 Demo,避免仓库后续变更导致文档代码和 npm 包行为漂移。更新本文验证基线时,应同步更新 Demo 的固定 tag 或 commit,并重新完成第 17 节的验证。Demo 中不得包含 API Key、OTLP 鉴权 Header、内部 Project、CLI profile 或历史 traceId。