☰
Midway 接入 OpenTelemetry 链路追踪实战指南:从埋点到 Jaeger 导出的完整方案
2026/10/9 11:05:20 网站建设 项目流程
  • 后端
  • 微服务
  • 云原生

【免费下载链接】midway

🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

本篇技术指南以 Midway 3.x 官方文档(英文版 / 中文版)为骨架,系统讲解如何在 Midway 应用中接入 OpenTelemetry 实现分布式链路追踪:涵盖基础依赖安装、bootstrap 与 egg-scripts 两种部署形态下的接入方式、核心概念拆解、Jaeger 与阿里云 ARMS 等 Exporter 配置,以及 Midway 提供的@Trace装饰器与ctx.traceId框架能力。读完本文,你将能够独立为自己的 Midway 应用搭建一套完整、可上生产的链路追踪体系。

为什么 Midway 选择 OpenTelemetry

Midway 采用社区最新的 OpenTelemetry 方案,其前身是知名的 OpenTracing 与 OpenCensus 规范。OpenTelemetry 目前是 CNCF 的孵化项目,社区中 Amazon、Dynatrace、Microsoft、Google、Datadog、Splunk 等公司均参与使用。

OpenTelemetry 的价值在于它提供了供应商无关(vendor-independent)的统一接入方案:以一套标准化的方式完成可观测数据的接收、处理与导出,支持将数据同时发送到一个或多个开源或商业化的采集端,例如 Jaeger、Prometheus、阿里云 SLS、Fluent Bit 等。这意味着你写一遍埋点代码,后续可以在不同后端之间切换而无需改动业务代码。

需要说明的是,OpenTelemetry 的 Tracing 部分其 Node.js SDK 已经发布 1.0.0,可以用于生产环境;而 Metrics 部分当时尚未正式发布(官方文档编写时仍在跟进编码中)。

使用须知:Node.js 版本与性能

OpenTelemetry 的 Node.js 实现基于Async_Hooks的稳定 API。根据官方文档的测试结论:

  • 在Node.js v14/v16及以上版本,性能影响已经很小,可以放心用于生产;
  • 在Node.js v12下虽然可以运行,但仍有不小的性能损失;
  • 因此建议尽可能在 Node.js >= v14 的环境下使用。

从当前仓库源码来看,核心框架中与链路追踪相关的实现位于 packages/core/src/service/traceService.ts 与 packages/core/src/decorator/common/tracer.ts,其功能依赖 OpenTelemetry 官方@opentelemetry/api包,因此上述版本约束同样适用于框架层的@Trace等能力。

安装基础依赖

OpenTelemetry 官方为 Node.js 提供了一套分层依赖包,接入 Midway 之前需要先安装以下基础包:

# Node.js 的 api 抽象(接口与类型定义) $ npm install --save @opentelemetry/api # Node.js 的 api 实现(SDK 本体) $ npm install --save @opentelemetry/sdk-node # 常用 Node.js 模块的埋点实现集合 $ npm install --save @opentelemetry/auto-instrumentations-node # jaeger 输出器(也可按需换成其他 Exporter) $ npm install --save @opentelemetry/exporter-jaeger

以上均为 OpenTelemetry 官方发布的包。其中@opentelemetry/api只是接口和空实现(对应下文"API"概念),真正的采集能力来自@opentelemetry/sdk-node,而@opentelemetry/auto-instrumentations-node一次性打包了大部分常用库的埋点,是接入成本最低的路径。

启用 OpenTelemetry:三种部署形态

OpenTelemetry 模块必须尽可能加在代码的最开始(比框架还早),因为埋点需要通过 monkey-patching 等方式在模块加载时就拦截方法。由于不同部署形态的入口不同,官方文档给出了三种添加方式。

方式一:bootstrap 部署

如果使用bootstrap.js部署,将 SDK 初始化代码放在bootstrap.js的最顶部。示例代码如下:

const process = require('process'); const { NodeSDK, node, resources } = require('@opentelemetry/sdk-node'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions') const { JaegerExporter } = require('@opentelemetry/exporter-jaeger') // Midway 启动文件 const { Bootstrap } = require('@midwayjs/bootstrap'); // Jaeger agent 地址可通过环境变量注入,默认本机 const tracerAgentHost = process.env['TRACER_AGENT_HOST'] || '127.0.0.1' const jaegerExporter = new JaegerExporter({ host: tracerAgentHost, }); // 初始化一个 open-telemetry 的 SDK const sdk = new NodeSDK({ // 设置追踪服务名(用于在链路后端区分不同服务) resource: new resources.Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'my-app', }), // 配置当前的导出方式,比如这里配置了一个输出到控制台的,也可以配置其他的 Exporter,比如 Jaeger traceExporter: new node.ConsoleSpanExporter(), // 配置当前导出为 jaeger(二选一,取消注释即可切换) // traceExporter: jaegerExporter, // 这里配置了默认自带的一些监控模块,比如 http 模块等 // 若初始化时间很长,可注销此行,单独配置需要的 instrumentation 条目 instrumentations: [getNodeAutoInstrumentations()] }); // 初始化 SDK,成功启动之后,再启动 Midway 框架 sdk.start() // 在进程关闭时,同时关闭数据采集 process.on('SIGTERM', () => { sdk.shutdown() .then(() => console.log('Tracing terminated')) .catch((error) => console.log('Error terminating tracing', error)) .finally(() => process.exit(0)); }); Bootstrap .configure(/**/) .run();

这段代码的关键点值得展开说明:

  • resource中的SERVICE_NAME:用来标识当前服务的名称,在 Jaeger 等后端中会作为服务列表的名字展示,务必改成你的真实应用名;
  • traceExporter:决定链路数据发往哪里。示例中先使用ConsoleSpanExporter输出到控制台便于本地验证,生产环境再切换为JaegerExporter;
  • instrumentations:getNodeAutoInstrumentations()返回一批默认埋点(http、gRPC、redis、mysql 等)。如果发现初始化时间过长,可以注销这行,改为按需配置单独的 instrumentation(见下文"添加三方 instrumentation");
  • 优雅退出:监听SIGTERM信号,在进程关闭时调用sdk.shutdown()确保未导出的数据被刷新,之后通过process.exit(0)结束进程。

方式二:egg-scripts 部署

egg-scripts 没有提供自定义入口文件的能力,因此必须使用--require的形式在进程启动前加载额外文件。

首先在项目根目录添加一个otel.js(注意是js 文件,且不能被 TypeScript 编译流程影响),内容如下:

const process = require('process'); const { NodeSDK, node, resources } = require('@opentelemetry/sdk-node'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); // 初始化一个 open-telemetry 的 SDK const sdk = new NodeSDK({ // 配置当前的导出方式,比如这里配置了一个输出到控制台的,也可以配置其他的 Exporter,比如 Jaeger traceExporter: new node.ConsoleSpanExporter(), // 这里配置了默认自带的一些监控模块,比如 http 模块等 instrumentations: [getNodeAutoInstrumentations()] }); // 初始化 SDK sdk.start() // 在进程关闭时,同时关闭数据采集 process.on('SIGTERM', () => { sdk.shutdown() .then(() => console.log('Tracing terminated')) .catch((error) => console.log('Error terminating tracing', error)) .finally(() => process.exit(0)); });

然后修改package.json中的启动命令,通过--require参数预先加载该文件:

{ // ... "scripts": { "start": "egg-scripts start --daemon --title=**** --framework=@midwayjs/web --require=./otel.js", }, }

--require=./otel.js会在 Node 进程启动框架之前先执行otel.js,从而保证埋点在框架模块加载前完成注入,这与"尽可能早于框架加载"的原则一致。

方式三:开发调试入口

本地开发时,midway-bin通过--entryFile参数指定入口文件。在package.json中配置如下:

{ "scripts": { "start": "cross-env NODE_ENV=local midway-bin dev --ts --entryFile=bootstrap.js" } }

这样本地开发也使用与生产(bootstrap 方式)相同的入口,bootstrap.js顶部的 OpenTelemetry 初始化代码会同样生效,保证开发环境与生产环境的埋点行为一致。

常用概念拆解

OpenTelemetry 将监控的整个过程抽象封装为几个步骤,每一步都可以自定义配置。理解下面四个核心概念,是正确配置的前提(更完整的英文概念体系可查阅 OpenTelemetry 官方 Concepts 文档)。

API

用于生成和关联 Tracing、Metrics、Logs 记录数据的数据类型和操作的一组 API 抽象,具体表现为@opentelemetry/api这个包,里面是接口与空实现。它是"标准契约",业务代码只需依赖它即可写出与具体实现解耦的埋点逻辑。

SDK

API 的特定语言实现,例如 Node.js 的实现就是@opentelemetry/sdk-node;除此之外,其他监控平台也各自提供采集 SDK 实现。应用运行时真正干活的是 SDK 这一层。

Instrumentations

OpenTelemetry 为常见库提供了一套 shim 代码(埋点实现),使用 hooks 或 monkey-patching 的方法拦截方法调用,在特定方法被调用时自动保存链路数据,目前支持 http、gRPC、redis、mysql 等模块,用户直接配置即可使用,无需改动业务代码。

上面示例中引入的@opentelemetry/auto-instrumentations-node就是一个已经默认封装好常用库的 instrumentations 集合包,其中已包含大部分常见库的埋点,具体依赖清单见其官方package.json。

Exporter

将接收到的链路数据发送到特定采集端的实现,比如 Jaeger、Zipkin 等。Exporter 与 Instrumentations 相互独立:前者负责"把数据送出去",后者负责"把数据采进来",你可以自由组合。

示例:自定义埋点与 Exporter 配置

添加三方 instrumentation

当内置的auto-instrumentations-node集合不满足需求时,可以在 SDK 初始化时向instrumentations数组追加单独的埋点。以 redis 为例:

const { RedisInstrumentation } = require('@opentelemetry/instrumentation-redis'); // ... // 初始化一个 open-telemetry 的 SDK const sdk = new NodeSDK({ // ... // 这里仅是添加的示例,如果使用了 auto-instrumentations-node,已经包含了下面的 instrumentation instrumentations: [ new RedisInstrumentation(), ] });

注意:如果已经启用了getNodeAutoInstrumentations(),redis 等常用库的埋点已被包含,此时无需重复添加,否则可能造成重复埋点。

添加 Jaeger Exporter

这里以 Jaeger 为例说明 Exporter 的接入方式,其他 Exporter(Zipkin、OTLP 等)流程类似。

第一步,添加依赖:

$ npm install --save @opentelemetry/exporter-jaeger @opentelemetry/propagator-jaeger

第二步,在 SDK 中配置 JaegerExporter 与 JaegerPropagator:

const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); const { JaegerPropagator } = require('@opentelemetry/propagator-jaeger'); // ... const exporter = new JaegerExporter({ tags: [], // optional,附加到链路上的额外标签 // 默认使用 UDPSender(通过 UDP 协议发送) host: 'localhost', // optional,Jaeger agent 主机地址 port: 6832, // optional,Jaeger agent UDP 端口 // 或者使用 HTTPSender,通过 HTTP 发送(与 UDP 二选一) // endpoint: 'http://localhost:14268/api/traces', maxPacketSize: 65000 // optional,UDP 报文最大字节数 }); // 初始化一个 open-telemetry 的 SDK const sdk = new NodeSDK({ traceExporter: exporter, textMapPropagator: new JaegerPropagator() // ... });

这里有两个值得注意的配置点:

  • UDPSender vs HTTPSender:默认走 UDP(host+port,对应 Jaeger agent 的6832端口,UDP 适合低延迟场景);如需 HTTP 方式,改用endpoint指向 Jaeger collector 的/api/traces接口即可;
  • textMapPropagator:使用JaegerPropagator让上下文在服务间传递时携带 Jaeger 的传播格式(如uber-trace-id),这决定了跨服务调用能否串联成一条完整链路。

阿里云 ARMS 接入

阿里云应用实时监控服务(ARMS)已经支持 OpenTelemetry 格式的指标,并提供 SDK 直接接入。

第一步,安装opentelemetry-arms:

# arms sdk $ npm install --save opentelemetry-arms

第二步,在启动时通过环境变量注入配置,并使用 Node 的-r(require)参数在进程启动前加载 SDK,无需修改任何业务代码:

$ SERVICE_NAME=nodejs-opentelemetry-express AUTHENTICATION=**** ENDPOINT=grpc://**** node -r opentelemetry-arms bootstrap.js

参数说明:SERVICE_NAME为服务名,AUTHENTICATION为 ARMS 的认证信息(由控制台生成),ENDPOINT为 ARMS 上报地址。

:::tip 使用注意

  1. 这种接入方式无需在bootstrap.js中添加任何代码,对已有项目侵入性最低;
  2. 默认 SDK 仅提供了 http/express/koa 模块的链路支持,未包含其他 instrumentations;如有更多需求,可以拷贝opentelemetry-arms的源码到bootstrap.js中自行定制。

:::

框架能力支持:@midwayjs/otel 组件

前面介绍的都是纯 OpenTelemetry 层面的接入。Midway 还封装了一个otel组件,对外提供便捷的框架内 API。注意:组件只是包裹了 otel 的接口,如果不需要下述接口,无需安装本组件。

安装依赖:

$ npm i @midwayjs/otel@3 --save

启用otel组件:

import { Configuration } from '@midwayjs/core'; import * as otel from '@midwayjs/otel'; @Configuration({ imports: [ // ... otel ] }) export class MainConfiguration { }

ctx.traceId:请求维度的链路标识

组件为请求上下文提供了ctx.traceId字段,在支持的组件(egg/koa)下可直接获取:

ctx.traceId => *****

从当前仓库源码可以印证其实现方式:在 packages/core/src/baseFramework.ts 中,框架通过Object.defineProperty为 ctx 定义了只读的traceId属性,其 getter 会从应用上下文中取出MidwayTraceService并调用getTraceId()方法;而getTraceId()的实现(见 packages/core/src/service/traceService.ts)会读取当前活跃 span 的spanContext().traceId。因此该值本质上是当前请求在链路系统中的全局标识,可方便地打印到日志中用于日志与链路关联检索。

@Trace 装饰器:为方法增加链路节点

针对用户侧的埋点需求,Midway 提供了@Trace装饰器,可以添加在任意方法上:

export class UserService { @Trace('user.get') async getUser() { // ... } }

该装饰器需要传入一个节点名称(span 名称),这样链路会自动为该方法添加一个链路节点,并记录执行时间与方法执行成功或失败的状态。

从源码看,@Trace的实现非常简洁:在 packages/core/src/decorator/common/tracer.ts 中,Trace(spanName)只是一个携带TRACE_KEY元数据的自定义方法装饰器;真正的拦截逻辑在 packages/core/src/service/traceService.ts 的init()中注册——框架使用装饰器服务(MidwayDecoratorService)注册了around拦截器,调用createSpan创建 span:

  • 方法正常返回时,将 span 状态置为SpanStatusCode.OK并end();
  • 方法抛出异常时,将 span 状态置为SpanStatusCode.ERROR,调用recordException(err)记录异常,然后重新抛出异常(不吞错)。

这与仓库中的测试用例完全对应:在 packages/core/test/fixtures/base-app-trace/src/user.service.ts 中,UserService用@Trace('user.invoke')标记正常方法、用@Trace('user.invoke_error')标记抛错方法;packages/core/test/trace.test.ts 验证了成功与失败两种场景下 span 状态的设置,以及ctx.traceId与traceService.getTraceId()的一致性。

进阶:MidwayTraceService 的编程式埋点 API

除了@Trace装饰器,核心框架还暴露了MidwayTraceService服务(通过@Provide()提供、单例作用域),它封装了比装饰器更灵活的编程式埋点能力,适合在框架或组件内部使用:

  • getTraceId():获取当前活跃 span 的 traceId;
  • createSpan(name, callback):以SpanKind.CLIENT类型创建 span 并执行回调;
  • runWithEntrySpan(name, options, callback):创建入口 span(默认SpanKind.SERVER),支持从carrier中通过propagation.extract提取父上下文,并在回调结束后通过propagation.inject将上下文写回responseCarrier,实现跨服务传递;
  • runWithExitSpan(name, options, callback):创建出口 span(默认SpanKind.CLIENT),执行前将当前上下文注入到carrier,用于调用下游服务时传递链路信息;
  • injectContext(carrier, setter):手动将当前上下文注入到指定载体。

以上方法的具体签名与实现见 packages/core/src/service/traceService.ts,其相关的tracing配置项定义在 packages/core/src/interface.ts。该服务支持通过配置文件控制链路追踪行为:

配置项类型默认值说明
tracing.enablebooleantrue是否启用链路追踪,置为false时所有埋点调用直接跳过
tracing.onError'throw' \| 'ignore''ignore'链路追踪内部操作(如 extract/inject)失败时的处理策略
tracing.logOnErrorbooleanfalse链路追踪内部操作失败时是否打印告警日志

从测试用例(packages/core/test/trace.test.ts 中'should fallback to callback when tracing is disabled'等用例)可以看出,当追踪被禁用时,runWithEntrySpan/runWithExitSpan会直接执行回调而不创建 span,保证追踪故障不会影响业务主流程。这体现了 Midway 在可观测性与业务稳定性之间所做的权衡设计。

总结

Midway 接入 OpenTelemetry 的整体思路可以概括为两条并行的路径:

  1. 纯 OpenTelemetry 接入:在应用最早期(bootstrap.js顶部、--require加载文件或-r预加载)初始化NodeSDK,配合auto-instrumentations-node获得零侵入的通用埋点,再按需选择 Jaeger、Zipkin、阿里云 ARMS 等 Exporter 完成数据导出。这是通用、可迁移的方案,与具体框架无关;
  2. 框架能力增强:通过@midwayjs/otel组件获得ctx.traceId与@Trace装饰器,让链路信息与 Midway 的请求上下文、IoC 容器深度结合;更进阶的场景可以直接注入MidwayTraceService,使用runWithEntrySpan/runWithExitSpan等编程式 API 实现跨服务传播与自定义埋点。

无论选择哪条路径,都需要牢记两个前提:一是初始化时机必须早于框架加载,二是尽可能运行在 Node.js >= v14 的环境中。掌握本文的配置与源码实现,你就可以为 Midway 应用构建出从请求入口到数据库访问、再到下游服务调用的完整链路视图了。

  • 后端
  • 微服务
  • 云原生

【免费下载链接】midway

🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

相关推荐

上一篇:从 37 条告警到 1 起事件:Keep 开源 AIOps 告警管理平台完整指南
下一篇:VidBee 如何在手机上下载视频:零基础上手完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询