OpenTelemetry 可观测性
本页介绍 VextJS 的 OpenTelemetry 接入:先确认插件可用,再用本地文件验证 Traces / Metrics / Logs,最后连接 Collector。前置条件是已按快速开始创建 TypeScript API 项目。2026-09-25 核对的发布包为 @devcodex/opentelemetry@2.1.17,Vext peer 范围 >=0.2.5;版本信息用于说明验证范围,安装命令不固定框架版本。
其他框架(Egg.js / Koa / Express / Hono / Fastify)的接入说明,请直接查看 GitHub 仓库:
devcodex-labs/opentelemetry
目录速览(VextJS-only)
- 快速开始(VextJS 框架)
- 先理解:VextJS 配置入口与初始化顺序
- 本地测试(无需 Docker)
/_otel/status状态检查接口- 配置方式(VextJS)
- 声明式采集(
capture) - 完整配置参考
- 生产最佳实践
- 常见问题
本页只保留 VextJS 的正式接入路径;如果你正在查 Egg.js / Koa / Express / Hono / Fastify,请直接跳转到 GitHub README 获取对应框架版本的说明。
快速开始(VextJS 框架)
1. 安装
@devcodex/opentelemetry已内置@opentelemetry/api、@opentelemetry/sdk-node、常用 OTLP exporter 与自动检测依赖; 对于 VextJS 默认接入,不需要再重复安装这些包。 只有当你的应用代码要直接 import 某个 OTel 包时,才建议把它声明成应用自己的直接依赖。
2. 创建插件
注意:
opentelemetryPlugin通过@devcodex/opentelemetry/vextjs子路径导入(VextJS 专属)。 主入口@devcodex/opentelemetry提供框架无关工具(例如createWithSpan、getOtelStatus)。
3. 添加可验证路由并启动
合并配置:
下面路由只执行一次本地演示操作,不访问外部支付或数据库;它以本页插件保持启用为前提:
生产流程是先停止开发服务,再执行 npm run build -- --typecheck 和 npm start。不要并行启动两者占用同一端口。
CLI 会从已安装依赖的 vext.preload 发现并注入 instrumentation 入口。当前包的默认自动预加载只准备入口,SDK 可以延后至 plugin setup;未配置导出目标且未强制预加载 SDK 时,不初始化 SDK。要在应用模块加载前启用自动检测,按下文设置 preloadSdk: true。
4. 验证默认状态
在没有其他 OTel 环境配置的情况下:
状态的关键字段应为 sdk: "noop"、exportMode: "none",业务请求仍返回200。此时没有导出遥测,不能据此声称 Collector 已收到数据。要验证真实输出,继续下面的文件导出流程。
先理解:VextJS 配置入口与初始化顺序
配置有三个读取位置,SDK 生命周期和请求观测职责要分别理解:
推荐顺序
- 在 package.json 一处设置 serviceName 和默认导出目标;需要早期自动检测时设置 preloadSdk:true。
- 插件补充请求观测行为,导出地址与 package 保持一致。
- 已配置的导出器不会被后续调用覆盖。当前 attachExporterToSdk 只给未配置的 delegate 赋值,但状态展示环境变量可能随后改变;因此状态接口不是实际投递目标的完整证据。变更目标或采样后应重启,再检查输出文件或 Collector。
- 默认延迟模式下,插件 exporter 参数依次读取 options → app.config.otel → package;它不会完整重读所有 OTel 环境变量。仅依赖环境变量时,要显式启用早期 SDK,并核对实际输出。
endpoint / protocol 速查
当前包的 gRPC 有两条实现路径:早期 SDK 的 Trace/Metrics 使用 gRPC exporter,而 Logs 仍构造 HTTP exporter;插件补充导出器时默认 insecure:true 使用 h2c,false 使用 TLS。h2c 分支没有转发配置 headers。需要三个信号统一接入或鉴权时,优先验证本页 OTLP/HTTP 路径;不能只根据 protocol:grpc 或状态字段推断各信号都成功。
不配置上报地址会怎样?
默认不发送到任何 Collector,也不写本地文件。设置 none 不是可靠的运行期关闭开关:若导出器已由其他入口初始化,必须统一配置并重启。
本地测试(无需 Docker)
不想装 Jaeger/Collector?可以将数据导出到本地文件,直接看原始数据格式。
方案一:导出到本地文件(推荐)
在项目 package.json 中配置上报地址(由 SDK 初始化脚本读取,控制实际导出):
package.json vext.otel.endpoint是 VextJS 模式下推荐的预加载配置源,能让启动阶段和运行阶段从一开始就保持一致。 插件只能补充未配置的导出器,不能覆盖已初始化的目标。相对路径基于 process.cwd(),从应用根目录启动。将片段合并到现有 package.json,保留 scripts 与 dependencies。
创建插件(保持 serviceName 与 package.json 一致即可):
修改 package.json 后,先停止已有服务再重启,使早期 SDK 读取新配置。
插件自动创建目录;为避免 cluster / 多 worker 进程并发写同一文件,当前实现会按 process.pid 分文件写入:
traces.<pid>.jsonlmetrics.<pid>.jsonllogs.<pid>.jsonl
等待批处理和指标周期(默认15秒)后查看;仅启用插件但没有业务请求不保证三个文件都有记录。PowerShell 使用 Get-Content ./otel-data/traces.*.jsonl,并分别查看 metrics/logs;类 Unix 终端可用 cat。确认业务 span demo.work、HTTP 指标和 otel demo completed 日志均存在。文件仅用于调试,轮转与保留由应用管理。
实际文件格式
- traces:每行一个 span,使用 traceId / spanId / name / attributes,时间与 duration 为 SDK 的高精度时间数组;不是旧示例的 id / timestamp 微秒结构。
- metrics:每行包含 timestamp 和 SDK ResourceMetrics 对象,指标位于 metrics.scopeMetrics[].metrics;不是顶层指标数组。
- logs:每行序列化一个 SDK LogRecord,字段和 Resource 表达跟随已安装 SDK;不应把调试 JSONL 当作固定 OTLP 网络协议。
- 父 span、resource 等可选字段可能缺失。用实际安装版本的输出建立读取器,查看三个文件而非只看状态接口。
方案二:本地 Jaeger(有 Docker 时)
按 Jaeger 官方文档启动提供 OTLP/HTTP 接收端口的服务,并将4318映射到本机;镜像和配置以所用 Jaeger 版本为准。Jaeger 主要用于 Trace,Metrics/Logs 需要对应接收后端。
在项目 package.json 中配置本地 Jaeger:
将 Jaeger 的 endpoint 与上面文件方案二选一,保留 serviceName / preloadSdk:true;启动后在 Jaeger UI 中查询该服务的 demo.work。插件保持最简即可:
其他框架接入
Vext 官网只保留 VextJS 场景的接入说明。
如果你需要查看以下内容:
- Egg.js / Koa / Express / Hono / Fastify 的接入方式
initOtel()的 CJS 预加载模式- 多框架
HttpOtelOptions/startAttributes/endAttributes/metrics.labels/createEggMiddleware说明 - 完整的发布记录与版本差异
请直接查看 GitHub 仓库:
建议优先阅读仓库中的:
README.mdchangelogs/
/_otel/status 状态检查接口
用于验证 OTel SDK 当前运行状态:
插件启用后通过 adapter 直接注册 GET /_otel/status,早于普通全局中间件;不要依赖普通 Route auth 或后续中间件保护它,本页 native 默认响应包装下,上述字段位于响应的 data 中,路径仅作示意。关闭或自定义包装时按实际配置读取。字段来自状态变量,不能证明后端已经接收数据;samplingRatio 在当前包中遇到0会回退显示1,不能只用这个字段判断零采样。
生产环境建议在网关层限制内网访问。
上报的数据内容
Traces(链路追踪)
HTTP 自动检测负责创建请求 span,插件负责补属性;需 SDK 已启用、目标库受支持且采样允许。以下是可能出现的属性:
默认包已依赖 auto-instrumentations-node,无需重复安装。早期初始化、模块加载顺序、具体库版本和采样共同决定是否产生子 span,不能仅凭已安装判断成功。
Metrics(指标监控)
ignorePaths 抑制本插件对匹配路径的 span 属性处理与 HTTP 指标记录;不会删除 HTTP 自动检测已创建的 span,也不会跳过 lifecycle 回调。需完全过滤底层 span 时配置对应 instrumentation/exporter。request.size 当前使用原始 req.path 标签,其他指标优先使用匹配路由;高基数路径需要额外评估。
Node.js Runtime 指标
由随包安装的 runtime-node instrumentation 提供,具体名称随版本变化。例如当前包包含 nodejs.eventloop.delay.*、nodejs.eventloop.utilization、v8js.memory.heap.used 等定义。CPU、RSS 与 GC 的名字不能从旧版示例推定;应在本地 metrics 文件或 Collector 中确认当前实际出现的指标。
Logs(日志关联)
已采样且 active span 正在 recording 的请求,经插件写入 requestContext 后,框架日志可以包含 trace_id / span_id。无活跃上下文、被忽略路径或未采样请求不保证这些字段:
通过 trace_id 可在 Grafana Loki / ELK 中关联日志与链路。
结构化日志(Schema A + Schema B)
当日志需同时落地(Schema A)并上报至 OTLP Collector(Schema B)时,使用 @devcodex/opentelemetry/log 提供的两个工厂函数:
createStructuredLogFormatter— Schema A 结构化 JSON 格式化器(固定字段顺序)createOtelLogBridge— Schema B OTel LogRecord 桥接(通过当前 OTel Logs API provider)
Schema A — 落地日志 JSON(完整字段)
VextJS 推荐写法
在 VextJS 中,通常不需要照搬其他框架的 logger formatter / middleware 拼装方式。更推荐:
- 在
opentelemetryPlugin()中开启logs.bridgeAppLogger - 在
config.logger.mixin中补稳定字段
如果你需要 Egg.js / Koa / Express / Hono / Fastify 的日志桥接写法,请直接查看 GitHub README;官网这里不再展开多框架分支。
配置方式(VextJS)
VextJS 的 OTel 配置分两层,目的不同:
第一层:预加载阶段默认导出配置(package.json,推荐)
由 SDK 初始化脚本(instrumentation.ts,通过 vext.preload 在应用代码前执行)优先读取,决定进程启动时的默认导出配置。
默认 CLI 会延迟 SDK;设置 preloadSdk:true 才按这个入口在应用模块前启动。插件只能补尚未配置的导出器,不能覆盖已有目标。
配置读取优先级(高 → 低):
package.jsonvext.otel.*- OpenTelemetry 标准环境变量(如
OTEL_SERVICE_NAME、OTEL_EXPORTER_OTLP_ENDPOINT) - 项目
package.json.name(仅用于serviceName回退) - 内置默认值(
serviceName: "vext-app"、protocol: "http"、endpoint: "none")
第二层:运行期插件行为(src/plugins/otel.ts)
插件层负责运行期 tracer / meter / logger 行为,例如 ignorePaths、指标桶、日志桥接,以及在 setup 阶段补充尚未配置的 exporter。下面及 capture 小节的选项片段沿用快速开始中的导入,替换同一个插件的参数,不要同时注册多份插件。
插件层的
endpoint/protocol/headers建议与package.json vext.otel保持一致,方便/_otel/status与实际导出目标统一。
声明式采集(capture)
如果你只想补充少量 headers / query / params / body 字段,不想为每个字段都手写 startAttributes / endAttributes resolver,可以直接使用 capture:
生成的属性前缀固定为:
http.request.header.*http.request.query.*http.request.param.*http.request.body.*
关键约束:
query: true/params: true表示显式开启全量模式;默认并不会自动采全量。- 当前版本 headers/body 同样支持显式全量模式;默认不采集,示例选择白名单。可用 fields、exclude、sensitiveKeys、maxValueLength、maxDepth、maxItems 与 output 调整采集范围、脱敏及快照。body 只读取已解析数据,不会重新消费请求流。
capture生成的是 Span attributes,不会自动进入metrics.labels;指标维度仍应单独通过metrics.labels提供,并保持低基数。
完整配置参考
opentelemetryPlugin() 选项
当前统一公开模型是
startAttributes / endAttributes / metrics.labels / lifecycle。 VextJS 适配器的raw参数就是req;其他框架则会透传各自的原始上下文(如 Express 的{ req, res }、Koa/Egg 的ctx)。
package.json vext.otel
环境变量
下表区分插件包和 SDK 读取的环境变量;VextJS 场景推荐优先通过
package.json vext.otel固化导出配置。
插件额外选项:enabled 默认为启用;insecure 仅在插件配置 gRPC exporter 时使用;resourceAttributes 当前是兼容占位,package reader 也没有读取同名字段,需 SDK Resource 属性时使用已支持的 OTEL_RESOURCE_ATTRIBUTES 并在实际产物核实。statusEndpoint 不支持自定义路径。tracing/metrics 默认开启,ignorePaths 默认空,logs.bridgeAppLogger 在 endpoint 非 none 时默认开启。
lifecycle 回调应同步完成,异常会警告并继续,不会作为业务授权/事务钩子;抛错路径的观测状态按500记录,可能与后续业务错误转换出的 HTTP 状态不同。metrics.labels 只附加到 duration/total;capture 只补 span 属性。
接入后端
本地开发
云厂商
以下是地址形态示例;实际区域、租户地址、接收协议和鉴权字段以厂商控制台及官方接入文档为准。表格不表示本页已验证这些远端服务。
云厂商 token 建议通过环境变量注入(K8s Secret),不要硬编码到代码中。
自动检测(Auto-Instrumentation)
@devcodex/opentelemetry 自带 @opentelemetry/auto-instrumentations-node,提供常见库的自动检测。能否获得数据库查询、HTTP 外调或消息队列的链路追踪,取决于 SDK 初始化顺序、库版本兼容性、检测项和采样配置。
安装
需要自动检测时设置 package.json 的 vext.otel.preloadSdk:true,再使用 vext dev/start;确认 SDK 在目标业务库加载前初始化。仅插件阶段才启动时,已经加载的库不保证被补充检测。
如果你的应用代码需要直接 import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node" 做深度定制,再把它声明为应用自己的直接依赖即可。
支持的库
效果示例
安装后,一次 GET /users/:id 请求在 Jaeger 中可能产生如下 Span 树:
上图是可能的业务调用链,前提是路由实际调用这些依赖且对应 instrumentation 成功启用;演示 /otel-demo 不会产生不存在的数据库/Redis操作。
禁用特定检测
优先使用 auto-instrumentations-node 支持的环境变量选择检测项,无需再创建第二个 NodeSDK。例如在 PowerShell 启动前设置:
这里使用不含包名前缀的名称。当前包已默认关闭 fs;该设置也会触发早期 SDK。完整配置见 OpenTelemetry 官方配置说明,并核对已安装 instrumentation 的支持范围。
未安装时的行为
如果未安装 @opentelemetry/auto-instrumentations-node:
- 控制台输出一行 warning 提示
- 手动 withSpan 与 SDK 指标仍可使用;插件只补充已有 active span,没有 HTTP 自动检测时不能保证请求 span 或日志 trace 关联
- 缺少相应自动产生的请求及数据库 / 外部 HTTP span;业务是否继续运行还取决于应用自身逻辑
高级用法
手动追踪业务操作(withSpan)
withSpan() 是追踪自定义业务操作的推荐方式。它对 tracer.startActiveSpan() 做了 try/catch/finally 封装,自动处理 span.end()、span.recordException()、span.setStatus() 三件最容易遗漏的事。
VextJS 插件(通过 app.otel.withSpan)
以下片段可放在 src/routes/index.ts:先按本页安装并注册 OTel 插件,实现业务服务 src/services/payment.ts 的 process(id) 方法,再运行 typegen。测试时用支付服务替身;一次请求只执行一次支付操作。
行为说明:
底层 API(自定义 SpanKind / Processor 等高级场景)
以下进阶片段同样放在 src/routes/index.ts,需先实现 src/services/user.ts 的 findById(id) 并运行 typegen;直接导入 OTel API 时执行 npm install @opentelemetry/api。startSpan 不会自动把新 span 设为子调用的 active context,需传播上下文时优先使用 withSpan。
自定义业务指标
采样(降低开销)
方式一:package.json 代码级配置(推荐)
instrumentation 在 SDK 初始化时读取 vext.otel.sampling.ratio,
当有效 ratio 小于1时使用 ParentBasedSampler(TraceIdRatioBasedSampler(ratio));没有已采样父上下文的根 span 按该比例采样。修改后重启:
方式二:未指定 package 采样时使用环境变量
Cluster 多进程
自定义 instrumentation
项目 src/preload/ 与直接依赖包声明的 vext.preload 会合并执行。应用自己的 package.json.vext.preload 不是项目脚本入口,更不会替换依赖包入口。不要在默认接入旁再创建一个未协调的 NodeSDK。
确需自己拥有 SDK 时,先阅读预加载指南,明确排除/禁用内置启动入口、初始化顺序、导出器和关闭所有权,再按上游自定义 SDK 文档实施。本页默认例程使用一个受插件管理的 SDK,不把额外入口当作现成替换方案。
日志字段规划
VextJS + @devcodex/opentelemetry 支持两层日志输出,各有侧重:
- A. 落地日志(stdout / file JSON):业务字段清晰可读,便于人工排查和日志聚合(ELK/Loki)
- B. OTel Logs(LogRecord → Collector):轻量级,通过
trace_id关联完整链路
A. 落地日志字段(stdout / file JSON)
通过 config.logger.mixin 添加稳定业务字段;日志 mixin 不等同于 SDK Resource 配置。以下替换前面的 logger 配置即可,无需顶层 await,也不读取非公开 Span.name:
trace_id / span_id 由有活跃 recording span 的请求上下文提供;业务 span 名称可由业务日志显式记录。
输出字段片段示例:
requestId、以及写入requestContext的traceId/spanId会由框架内置 provider 自动注入为requestId、trace_id、span_id;不需要在用户 mixin 中重复配置。
字段对照表
默认请求日志消息形如 GET /users/123 200 8ms | IP。指标应按路由模板聚合,避免把每个用户 ID 变成独立标签;在应用自己的路由中间件中显式记录即可。以下文件不替代前面的 OTel 初始化:
将 route-metrics 加入 config.middlewares 白名单,并在目标路由的 middlewares 中引用;配置步骤见中间件。参数路由的 endpoint 应为 /users/:id,不是 /users/123。这里用 req.onClose() 记录响应结束或连接提前断开的耗时;关闭事件不保证客户端已完整接收,也不应自动计为一次成功请求。
B. OTel Logs(LogRecord → Collector)
Vext 默认 logger 不依赖第三方 logger,因此 logger-specific auto instrumentation 不会自动捕获 app.logger。如需输出 OTel Logs,可通过 @devcodex/opentelemetry 的 app.setLogger() 桥接,或自定义插件包装当前 logger:
trace_id/span_id:从requestContext或 active span 写入 LogRecordseverity_text:从 Vext logger level 映射body:日志消息内容service.name:来自 Resource(instrumentation.ts 已配置)attributes:结构化日志字段映射为 LogRecord attributes
当前桥接读取 logger 调用参数,然后调用原 logger;原 logger 后续生成的 mixin 字段不会自动进入 LogRecord。需要两边都有的字段,应在日志参数中显式传入,或给 OTel 设置 logs.globalAttributes。bridge 默认在 endpoint 非 none 时启用,只包装 info/warn/error/debug/fatal,child logger 与 trace 方法不自动桥接;嵌套对象字段也不会完整透传。
避免在 LogRecord attributes 中放入所有落地日志字段。OTel Logs 通过 trace_id 关联 Trace 即可看到 endpoint、latency_ms、user.id 等完整上下文。保持 LogRecord 轻量有助于控制 Collector 流量。
C. 深层字段(自动出现在子 Span 中)
以下是旧语义命名下的示意,实际字段取决于已安装 instrumentation、目标库、配置与采样;例如新版本可能使用 url.full 或 db.query.text,不能把此表当作所有请求必有的字段合同:
通过
trace_id在 Jaeger / Grafana Tempo 中查看完整调用链路即可关联这些深层字段。
生产最佳实践
- 配置上报地址 — 未配置时不会上报(安全默认值),但也意味着无可观测性数据
- 关闭预算 — 插件 onClose 调用 SDK shutdown;按实际批处理/网络延迟设置 shutdown.timeout(秒)并验证。增大期限不能保证 Collector 成功接收
- 限制
/_otel/status— 当前 VextJS 适配器会自动注册该路由,生产环境请在网关层限制为内网访问 - 不要在 Span 中记录敏感信息 — 密码、Token、身份证号等
- 采样 — 统一 package sampling 或已核实的环境配置,重启后观察实际输出量
- 部署 Collector — 应用 → Collector → 后端,解耦 + 缓冲
常见问题
Q: /_otel/status 返回 "sdk": "noop"
未配置 endpoint 时 noop 可能正是预期。需要输出时检查直接依赖、插件启用、package endpoint/preloadSdk 和 OTEL_SDK_DISABLED;完全禁用插件会使该接口404。
Q: endpoint 显示 localhost 但我配了其他地址
① 检查 package.json vext.otel.endpoint ② 确认插件里的 endpoint/protocol/headers 与 package.json 保持一致 ③ 确认用 vext start/dev 启动
Q: 日志没有 trace_id
检查 SDK、早期自动检测、插件接入及采样;还需 requestContext 已启用且该日志位于 recording span 请求上下文。状态 initialized 不足以证明当前请求有 active span。
Q: 后端收不到数据
先确认 exportMode/exportTarget,再用本地文件区分“未产生数据”和“发送失败”。检查后端实际接收记录、鉴权、协议与网络;等待配置的批处理/指标周期。当前包不保证逐批打印 SUCCESS,gRPC 失败/恢复日志也不等于所有信号均已接收。
Q: [otel] ... export FAILED: grpcSend timeout
服务器到采集器的 h2c gRPC 连接受阻。检查:① 采集器地址和端口可达 ② 采集器服务正常运行 ③ 网络防火墙/安全组规则 ④ 如在 Docker/K8s 内,使用 Service DNS 而非 localhost
Q: 我直接用 node dist/server.js 启动,为什么 SDK 没生效?
因为 VextJS 的“零配置接入”依赖 CLI 在启动前自动扫描依赖包里的 vext.preload 并注入 --import。
可选做法:
- 推荐:通过项目 npm scripts 使用 vext dev / vext start
- 自定义 Node 启动命令:仅在你确实编写了完整应用启动入口时,手动补上
--import @devcodex/opentelemetry/instrumentation;标准 Vext build 不会凭空生成 dist/server.js
Q: 测试环境如何彻底禁用
或启动前设置 OTEL_SDK_DISABLED=true。关闭后也要停用依赖 app.otel 的演示路由;只写 endpoint:none 表示不导出,不能替代整个集成的关闭。