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 等几十个常见库,启动时加 --require 即可生效,业务代码零改动。
核心要点速览
- 自动埋点通过模块劫持实现,必须在应用代码加载前启动;
- 自定义 Span 用于标注关键业务步骤,与自动 Span 自动串联;
- 链路经 Collector 汇聚转发,应用与后端解耦;
- 接入观测云时,核对接收协议与监听地址,再用已采集 Span 验证调用关系。
第一步:初始化 NodeSDK
新建 tracing.js(必须在应用启动前加载):
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { Resource } = require('@opentelemetry/resources');
const sdk = new NodeSDK({
resource: new Resource({ 'service.name': 'api-gateway' }),
traceExporter: new OTLPTraceExporter({
url: 'http://datakit-host:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown());
启动方式:node --require ./tracing.js app.js。service.name 决定 APM 中的服务名,务必规范。
第二步:配置 Collector 管道
应用先把链路发给 Collector,再由 Collector 批量转发:
receivers:
otlp:
protocols:
http: {endpoint: 0.0.0.0:4318}
processors:
batch: {timeout: 5s}
exporters:
otlp:
endpoint: "http://datakit:4318"
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
第三步:定制自动埋点
自动埋点可按库精细控制——比如关掉嘈杂的 DNS 查询 Span、给 HTTP Span 附加自定义属性:
instrumentations: [getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-dns': { enabled: false },
'@opentelemetry/instrumentation-http': {
requestHook: (span, request) => {
span.setAttribute('app.tenant', request.headers['x-tenant-id'] || 'unknown');
},
ignoreIncomingRequestHook: (req) => req.url === '/health', // 健康检查不生成 Span
},
})],
第四步:补充自定义 Span
自动埋点覆盖框架层,业务关键步骤手动补 Span:
const opentelemetry = require('@opentelemetry/api');
const tracer = opentelemetry.trace.getTracer('checkout-service');
async function checkout(cartId) {
return tracer.startActiveSpan('checkout.process', async (span) => {
try {
span.setAttribute('cart.id', cartId);
span.setAttribute('cart.item_count', items.length);
await validateInventory();
await chargePayment();
span.setStatus({ code: opentelemetry.SpanStatusCode.OK });
} catch (err) {
span.recordException(err);
span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end();
}
});
}
recordException 会把异常堆栈记进 Span,后端可直接检索含错误的链路。
Node.js 异步上下文注意事项
AsyncLocalStorage 让 async/await、Promise 链中的上下文自动延续,但两个场景需留意:回调风格的老代码(用 context.with 显式包裹)、跨进程的异步任务(消息队列需手动传播上下文,见《上下文传播》)。
检查异步调用是否进入同一条链路
使用观测云后端时,先让 exporter 对接已启用的 DataKit OTLP 接收地址。发起包含 Promise、下游 HTTP 和自定义 Span 的测试请求,在链路详情核对父子关系与耗时;再用异常请求确认错误字段。日志需要写入与该请求一致的 trace_id 才能建立关联,不能用普通 request ID 替代。
常见问题(FAQ)
Q:自动埋点会影响性能吗? 常规开销在 5% 以内;如果应用本身高频创建大量短 Span(如高频 Redis 轮询),可按需关闭个别 instrumentation。
Q:Next.js / Serverless 环境适用吗? Vercel 等平台内置 OTel 支持;传统 Serverless 需用平台的 OTel 层(Layer)或自行初始化,注意冷启动期 exporter 初始化的延迟。
Q:为什么我的 Span 没有父级、链路是散的? 多半是自定义 Span 创建时上下文丢失——确认在 startActiveSpan 回调内执行业务逻辑,或在 Promise 链中检查 context 传递。
Q:本地开发怎么调试? 把 exporter 换成 ConsoleSpanExporter 直接打印,或指向本地 Jaeger,验证通过后再切观测云。