- 后端
- 微服务
- 云原生
【免费下载链接】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. 🌈
本篇技术指南以 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 使用注意
- 这种接入方式无需在
bootstrap.js中添加任何代码,对已有项目侵入性最低; - 默认 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.enable | boolean | true | 是否启用链路追踪,置为false时所有埋点调用直接跳过 |
tracing.onError | 'throw' \| 'ignore' | 'ignore' | 链路追踪内部操作(如 extract/inject)失败时的处理策略 |
tracing.logOnError | boolean | false | 链路追踪内部操作失败时是否打印告警日志 |
从测试用例(packages/core/test/trace.test.ts 中'should fallback to callback when tracing is disabled'等用例)可以看出,当追踪被禁用时,runWithEntrySpan/runWithExitSpan会直接执行回调而不创建 span,保证追踪故障不会影响业务主流程。这体现了 Midway 在可观测性与业务稳定性之间所做的权衡设计。
总结
Midway 接入 OpenTelemetry 的整体思路可以概括为两条并行的路径:
- 纯 OpenTelemetry 接入:在应用最早期(
bootstrap.js顶部、--require加载文件或-r预加载)初始化NodeSDK,配合auto-instrumentations-node获得零侵入的通用埋点,再按需选择 Jaeger、Zipkin、阿里云 ARMS 等 Exporter 完成数据导出。这是通用、可迁移的方案,与具体框架无关; - 框架能力增强:通过
@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. 🌈
相关推荐
Midway 链路追踪完全指南:从 `@midwayjs/core` 内置 Tracing 到 OpenTelemetry 平台对接
Midway 链路追踪完全指南:从 @midwayjs/core 内置 Tracing 到 OpenTelemetry 平台对接 导读 链路追踪(Distrib
后端微服务云原生agno Agent 输入输出实用指南:6 个机制控制它说什么、怎么说
agno Agent 输入输出实用指南:6 个机制控制它说什么、怎么说 agno 是一个用 Python 构建、运行和管理 Agent 平台的框架。实际用起来你
人工智能大模型AI AgentAgent 框架多智能体工具调用RAGAgent 工作流Agent 记忆easy-vibe 数据埋点实战指南:从采集方案设计到数据仓库入库的完整事件追踪链路
easy vibe 数据埋点实战指南:从采集方案设计到数据仓库入库的完整事件追踪链路 数据埋点(Event Tracking)是把用户"看不见"的操作行为转化为
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考