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 汇聚转发,应用与后端解耦;
- 端点指向观测云 DataKit,APM 直接出服务拓扑与火焰图。
第一步:初始化 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 显式包裹)、跨进程的异步任务(消息队列需手动传播上下文,见《上下文传播》)。
观测云落地:链路进 APM 闭环
导出端点指向观测云 DataKit 后,观测云 APM 自动呈现:服务拓扑图展示 API 网关与各下游的调用关系,链路列表支持按耗时/状态过滤,火焰图逐层下钻到具体 Span,ERROR 链路自动标红。配合日志 trace_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,验证通过后再切观测云。