Node.js 分布式追踪实战:OpenTelemetry 自动埋点与自定义 Span
为 Node.js 应用接入 OpenTelemetry 分布式追踪:NodeSDK 初始化、自动埋点覆盖 Express/HTTP/MySQL/Redis、自定义 Span 补充业务语义、Collector 管道配置,最终导入观测云 APM 实现全链路分析。附完整代码。
Node.js 分布式追踪的接入路径是"NodeSDK + 自动埋点包"——@opentelemetry/auto-instrumentations-node 一个包覆盖 Express、HTTP、MySQL、PostgreSQL、Redis、MongoDB 等几十个常见库,支持的库和版本可减少业务代码修改;下面使用 CommonJS 预加载,ESM 应按对应埋点包的加载说明配置。
核心要点速览
- 自动埋点通过模块劫持实现,必须在应用代码加载前启动;
- 自定义 Span 用于标注关键业务步骤;上下文正确传播时可与自动 Span 建立父子关系;
- 链路经 Collector 汇聚转发,应用与后端解耦;
- 接入观测云时,核对接收协议与监听地址,再用已采集 Span 验证调用关系。
第一步:初始化 NodeSDK
安装兼容的 @opentelemetry/api、@opentelemetry/sdk-node、@opentelemetry/auto-instrumentations-node、@opentelemetry/exporter-trace-otlp-http 和 @opentelemetry/resources,锁定版本并核对 Node.js 要求。以下使用 resourceFromAttributes API;新建 tracing.cjs(必须在应用启动前加载):
// tracing.cjs
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const sdk = new NodeSDK({
resource: resourceFromAttributes({ 'service.name': 'api-gateway' }),
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
module.exports = sdk;
// 应用统一处理退出:先关闭服务并等待在途请求,再 await sdk.shutdown(),
// 设置总退出超时并处理失败;单独注册此信号回调不能保证 HTTP 服务退出。
CommonJS 应用的启动方式:node --require ./tracing.cjs app.cjs。service.name 决定 APM 中的服务名,务必规范。示例的 localhost 指应用所在网络命名空间;Collector 在另一个容器时,应改为可达地址。
第二步:配置 Collector 管道
应用先把链路发给 Collector,再由 Collector 批量转发:
receivers:
otlp:
protocols:
http: {endpoint: 0.0.0.0:4318}
processors:
batch: {timeout: 5s}
exporters:
otlp_http:
traces_endpoint: ${env:DATAKIT_OTLP_HTTP_TRACES_ENDPOINT}
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp_http]
DATAKIT_OTLP_HTTP_TRACES_ENDPOINT 需设置为 DataKit 实际启用的完整 HTTP 链路接收 URL;使用与 Collector 版本相符的 HTTP exporter,不能将 gRPC exporter 指向 HTTP 端口。较旧发行版可能使用 otlphttp 名称,需核对已安装组件。0.0.0.0 会接受其他接口上的连接,应按部署环境限制访问并配置所需的 TLS 与认证;batch 不等于持久缓冲。
第三步:定制自动埋点
自动埋点可按库精细控制——比如关掉嘈杂的 DNS 查询 Span、给 HTTP Span 附加自定义属性:
const instrumentations = [getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-dns': { enabled: false },
'@opentelemetry/instrumentation-http': {
requestHook: (span, request) => {
const value = request.headers?.['x-tenant-id']
?? request.getHeader?.('x-tenant-id');
if (typeof value === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(value)) {
span.setAttribute('app.tenant', value);
}
},
ignoreIncomingRequestHook: (req) =>
(req.url || '').split('?')[0] === '/health',
},
})];
在构造 NodeSDK 前定义此变量,再用 instrumentations 替换 SDK 中原来的数组。请求钩子需分别兼容入站和出站对象;租户字段只是经过格式限制的关联数据,不是可信的鉴权依据。忽略 HTTP 健康请求后,其他埋点是否仍产生 Span 需按实际版本验证。
第四步:补充自定义 Span
自动埋点覆盖框架层,业务关键步骤手动补 Span:
const opentelemetry = require('@opentelemetry/api');
const tracer = opentelemetry.trace.getTracer('checkout-service');
async function checkout(cartId, items, validateInventory, chargePayment) {
return tracer.startActiveSpan('checkout.process', async (span) => {
try {
span.setAttribute('cart.id', String(cartId));
span.setAttribute('cart.item_count', items.length);
await validateInventory(items);
const result = await chargePayment(cartId, items);
span.setStatus({ code: opentelemetry.SpanStatusCode.OK });
return result;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
span.recordException(error);
span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR, message: error.message });
throw err;
} finally {
span.end();
}
});
}
items 及库存校验、扣款函数由应用传入;示例返回扣款结果,并将原始错误继续抛给调用方。recordException 记录异常事件;还需像示例一样设置 ERROR 状态,后端检索行为取决于接收和字段映射。是否记录购物车标识与异常消息,应符合遥测数据的隐私和保留要求。
Node.js 异步上下文注意事项
AsyncLocalStorage 让 async/await、Promise 链中的上下文自动延续,但两个场景需留意:丢失上下文的旧式回调(可按调用方式使用 context.bind 或 context.with)、跨进程的异步任务(消息队列需支持相应埋点,或手动注入/提取上下文,见《上下文传播》)。
检查异步调用是否进入同一条链路
使用观测云后端时,先让 exporter 对接已启用的 DataKit OTLP 接收地址。发起包含 Promise、下游 HTTP 和自定义 Span 的测试请求,在链路详情核对父子关系与耗时;再用异常请求确认错误字段。日志需要写入与该请求一致的 trace_id 才能建立关联,不能用普通 request ID 替代。
常见问题(FAQ)
Q:自动埋点会影响性能吗? 开销取决于负载、埋点范围和采样,需在实际环境测量;如果应用本身高频创建大量短 Span(如高频 Redis 轮询),可按需关闭个别 instrumentation。
Q:Next.js / Serverless 环境适用吗? 应按平台和框架支持的 OTel 方式接入,可能使用框架钩子、Layer 或应用初始化;框架已管理 SDK 时不要重复初始化。还需验证冷启动开销,以及运行时冻结或调用结束前能否完成导出。
Q:为什么我的 Span 没有父级、链路是散的? 多半是自定义 Span 创建时上下文丢失——确认在 startActiveSpan 回调内执行业务逻辑,或在 Promise 链中检查 context 传递。
Q:本地开发怎么调试? 把 exporter 换成 ConsoleSpanExporter 直接打印,或指向本地 Jaeger,验证通过后再切观测云。
系列阅读
- 上一篇:《Node.js OpenTelemetry 监控》
- 下一篇:《PHP OpenTelemetry 指标》
- 相关:《分布式追踪详解》 · 《Go OpenTelemetry 链路追踪》 · 《返回索引》
[1]: 观测云文档《OpenTelemetry》https://docs.guance.com/integrations/opentelemetry/
[2]: 观测云《APM 平台》https://www.guance.com/product/application-performance-monitoring