Node.js 分布式追踪实战:OpenTelemetry 自动埋点与自定义 Span

为 Node.js 应用接入 OpenTelemetry 分布式追踪:NodeSDK 初始化、自动埋点覆盖 Express/HTTP/MySQL/Redis、自定义 Span 补充业务语义、Collector 管道配置,最终导入观测云 APM 实现全链路分析。附完整代码。

最佳实践
Node.js 分布式追踪实战:OpenTelemetry 自动埋点与自定义 Span封面

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,验证通过后再切观测云。

系列阅读

[1]: 观测云文档《OpenTelemetry》https://docs.guance.com/integrations/opentelemetry/
[2]: 观测云《APM 平台》https://www.guance.com/product/application-performance-monitoring

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台