New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@devcodex/opentelemetry

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@devcodex/opentelemetry

Framework-agnostic OpenTelemetry SDK wrapper — zero-config traces, metrics, Span API and multi-framework adapters

latest
Source
npmnpm
Version
2.1.17
Version published
Weekly downloads
7
-81.58%
Maintainers
1
Weekly downloads
 
Created
Source

@devcodex/opentelemetry

多框架 OpenTelemetry 集成 — 零配置追踪、指标与日志,支持 VextJS / Egg.js / Koa / Express / Hono / Fastify

npm version License: MIT

将原本需要手写的 ~200 行 OpenTelemetry 样板代码压缩为极简配置,开箱即得 Traces(链路追踪)、Metrics(指标监控)与 Logs(日志关联)。

目录导航

特性

  • 追踪 — 自动标注 HTTP Span 属性(路由、状态码、请求 ID)
  • 指标 — 内置 HTTP 请求时长直方图、请求总数、活跃请求数
  • 日志关联 — 自动将 trace_id 注入每条请求日志
  • gRPC h2c — 原生 node:http2 实现,兼容自建 Jaeger / K8s OTel Collector
  • 优雅降级 — SDK 未初始化时以 Noop 模式运行,零 overhead
  • 轻量预加载 — VextJS 自动 preload 默认延后 SDK/exporter 到 plugin setup,支持 enabled:false 和标准 OTEL_SDK_DISABLED=true 早退
  • 多框架 — VextJS / Egg.js / Koa / Express / Hono / Fastify

安装

Node.js 要求:@devcodex/opentelemetry 当前声明 engines.node ^18.19.0 || >=20.6.0。 若你的服务仍使用 Node 18.0–18.18 或 20.0–20.5,请先升级运行时;安装阶段可能只给 warning,但这些版本不属于当前 OpenTelemetry SDK 的受支持范围。

# 常规接入(VextJS / Koa / Express / Hono / Fastify)
npm install @devcodex/opentelemetry

# 仅当你的应用代码会直接 import 这些 instrumentation 时,再额外声明
# 典型场景:Egg/Koa 的 otel-init.cjs 自己 new Instrumentation()
npm install @opentelemetry/instrumentation-http \
            @opentelemetry/instrumentation-mongodb \
            @opentelemetry/instrumentation-ioredis \
            @opentelemetry/instrumentation-mysql2

@devcodex/opentelemetry 已内置 @opentelemetry/api、@opentelemetry/sdk-node 和常用 OTLP exporter。 对于正常接入,不需要再重复安装这些包。只有当你的业务代码要直接 import { NodeSDK } ... / import { SpanStatusCode } ... 时,才建议把对应包声明为应用自己的直接依赖。

先理解:4 个配置入口分别负责什么

这 4 个入口不是一回事。大多数“配了但不生效 / 不知道该写哪”的问题,都来自把它们混着理解。

配置入口负责什么生效阶段适用场景不适合放什么
package.json vext.otelVextJS 预加载阶段的 SDK 初始配置进程启动 / preloadVextJS 需要从一开始就确定 service.name、导出目标、采样、metric 上报周期请求级逻辑、Span 属性、access log
opentelemetryPlugin()VextJS 插件期的运行时配置与请求观测配置plugin setup + requestVextJS 中补充导出配置、桥接日志、配置 tracing/metrics/lifecycle不适合承担 preload 阶段才会生效的 Resource 语义
initOtel()Egg / Koa 的 SDK 初始化 helper进程启动 / --requireEgg / Koa 需要在任何模块加载前初始化 SDK请求级属性提取、access log、副作用逻辑
HttpOtelOptions各 HTTP 适配器的统一请求观测配置request start / request end配置 tracing、metrics、lifecycle、logsSDK 启动参数(如采样率、metric reader 周期)

一句话记忆

  • SDK 怎么启动 / 怎么导出 → 看 package.json vext.otel、opentelemetryPlugin()、initOtel()
  • 每个请求怎么观测 / 打什么属性 / 做什么副作用 → 看 HttpOtelOptions

我应该从哪里开始配置

你的目标推荐入口原因
想改 service.name、导出地址、采样率package.json vext.otel(VextJS)/ initOtel()(Egg/Koa)/ preload bootstrap(Express/Hono/Fastify)这些都属于 SDK 初始化层
想给请求 Span 增加属性tracing.startAttributes / tracing.endAttributes这是观测数据本身
想改 Span 名称tracing.spanNameResolver这是追踪主语义
想打 access log、trace 关联日志lifecycle.onEnd这是请求结束副作用,不是 Span 属性
想回写 ctx / req 字段lifecycle.onStart / lifecycle.onEnd这是运行时副作用
想加指标标签metrics.labels这是指标维度配置,但必须低基数
想桥接 VextJS 的 app.logger 到 OTel Logslogs.bridgeAppLogger这是日志桥接,不属于 tracing

导出目标与 endpoint / protocol 规则

endpoint 在不同初始化入口里的语义不完全一样,尤其要分清 VextJS 与 Egg/Koa。

场景配置入口host:port 含义其他说明
VextJS 预加载 / pluginpackage.json vext.otel.endpoint、opentelemetryPlugin({ endpoint, protocol })由 protocol 决定:protocol: "grpc" 时走 gRPC h2c;protocol: "http" 时走 OTLP HTTP当前默认 protocol 为 "http",因此只写 host:port 且不显式改协议时,会按 HTTP 处理
Egg / Koa 手动初始化initOtel({ endpoint })默认走 gRPC h2cinitOtel() 不暴露单独 protocol 字段;只有写成 http://... 才会切到 OTLP HTTP
统一关闭导出endpoint: "none" 或不传不上报适合本地开发、测试或只想保留 Noop SDK

关闭上报速查

如果目标是不再向 Collector 上报 traces / metrics / logs,请关闭 exporter:

{
  "vext": {
    "otel": {
      "endpoint": "none"
    }
  }
}

同时确认插件阶段没有再补一个导出目标:

export default opentelemetryPlugin({
  endpoint: "none",
});

VextJS 自动 preload 场景下,默认不会抢先根据 package.json vext.otel.endpoint 启动完整 SDK/exporter;真正的 SDK 启动与 exporter 配置会延后到 opentelemetryPlugin() setup 阶段。因此:

  • app.config.otel.enabled=false 或 opentelemetryPlugin({ enabled: false }) 会阻止插件阶段启动 SDK、挂载请求观测、注册 /_otel/status 和 logger bridge。
  • package.json vext.otel.enabled=false、VEXT_OTEL_DISABLED=1、VEXT_OTEL_PRELOAD=0 或 OTEL_SDK_DISABLED=true 会在 preload 阶段直接早退,不加载完整 OTel SDK。
  • 如果你确实需要进程最早期 SDK / auto-instrumentation,请显式设置 package.json vext.otel.preloadSdk=true 或 VEXT_OTEL_FORCE_SDK=1。启用后,这段初始化早于 src/config/default.ts,因此后者无法再回滚已经启动的 SDK/exporter;在支持 node:module.register() 的 Node 运行时中,preload 也会先注册 OpenTelemetry 的 ESM hook,以覆盖随后载入的 ESM 模块。
  • 非 Vext 手动 node --import @devcodex/opentelemetry/instrumentation ... 场景保持独立心智模型:有有效 endpoint 时会在 preload 阶段启动 SDK;endpoint:"none" 且无强制 SDK/auto-instrumentation 信号时保持 noop。

为什么 Egg / Koa 默认偏向 gRPC h2c

initOtel() 在 gRPC 模式下直接使用 node:http2,可以绕开 @grpc/grpc-js 在部分自建 Collector 场景下的 h2c 握手兼容问题。

启动日志策略

默认只保留:

  • 真正已配置导出目标时的启动摘要
  • 首次失败告警
  • 失败后的首次恢复提示

不会持续打印每一批成功导出日志;当处于 deferred export(等待插件 setup 接管)时,也不会默认打印启动摘要,避免在 VextJS 场景里把阶段性状态误读为最终状态。

tracing 与 lifecycle 到底有什么区别

这是最容易被误解的地方。

一句话区别

  • tracing = “这次请求在观测系统里长什么样”
  • lifecycle = “这次请求前后我要额外做什么动作”

对比表

维度tracinglifecycle
本质Span / 观测数据配置请求生命周期副作用
典型能力ignorePaths、spanNameResolver、startAttributes、endAttributesonStart、onEnd
适合做什么改 Span 名、补 Span 属性、控制哪些请求被追踪打 access log、回写 ctx/req、收尾动作
不适合做什么打日志、改业务上下文、写副作用充当主要 Span 属性配置入口

正确的心智模型

tracing

适合:

  • 让 Span 名称从 GET /users/123 收敛成 GET /users/:id
  • 给 Span 增加 tenant.id、request.has_body 这类观测属性
  • 忽略健康检查路径

不适合:

  • 在回调里直接打 access log
  • 在回调里修改 ctx.state / req.user
  • 把业务副作用塞到 attribute resolver 里

lifecycle

适合:

  • 在请求开始时回写上下文字段
  • 在请求结束时打 access log
  • 在请求结束时用 traceId 做日志关联

不适合:

  • 作为主要 Span 属性入口
  • 替代 spanNameResolver
  • 承担“告诉 OTel 这次请求长什么样”的职责

错误示例 vs 正确示例

错误示例 1:把 access log 写进 endAttributes

  • endAttributes 里一边 return { "tenant.id": "t-1" },一边又直接 raw.logger.info(...) 打日志。

问题:endAttributes 的职责是返回 Span 属性,不是做副作用。把日志塞进这里,读者会误以为 tracing hook 就是“请求结束万能回调”。

正确示例 1:属性放 endAttributes,日志放 lifecycle.onEnd

  • tracing.endAttributes 只返回诸如 request.body.present 这类 Span 属性。
  • lifecycle.onEnd 单独负责 raw.logger.info(...) 这类 access log / 收尾动作。

错误示例 2:把上下文回写放进 startAttributes

  • startAttributes 里先执行 raw.user_id = ... 回写上下文,再返回 tenant.id 等属性。

问题:startAttributes 的职责是返回属性,不是修改运行时上下文。

正确示例 2:回写放 lifecycle.onStart

  • tracing.startAttributes 只返回 tenant.id 这类请求开始阶段就能确定的稳定属性。
  • lifecycle.onStart 再单独负责 raw.user_id = ... 这类上下文回写。

配置项完整说明

package.json vext.otel

适用:VextJS preload / instrumentation 阶段。

{
  "name": "admin",
  "vext": {
    "otel": {
      "serviceName": "admin",
      "enabled": true,
      "preloadSdk": false,
      "endpoint": "47.89.182.109:32767",
      "protocol": "grpc",
      "headers": {
        "x-tenant": "prod"
      },
      "sampling": { "ratio": 1 },
      "metricIntervalMs": 15000
    }
  }
}
字段作用生效阶段推荐场景不推荐场景常见误用
serviceName写入 SDK Resource 的 service.namepreload / setup fallback希望从进程启动时就确定服务名请求级逻辑误以为只配 plugin 里的 serviceName 就能回写已启动的 Resource
enabledpackage/preload 层总开关preload / setup fallback希望禁用当前 Vext 应用的 OTel SDK 与 plugin fallback请求级开关误以为只关 plugin 但仍想使用 package endpoint
preloadSdkVext 自动 preload 是否抢先启动完整 SDKpreload需要进程最早期 SDK / auto-instrumentation普通请求观测忽略它早于 src/config/default.ts 加载
endpoint导出目标;Vext 自动 preload 默认延后到 plugin setuppreload / setup fallback配置统一导出目标请求级 hook误把它当成 tracing 里的请求属性配置
protocolVextJS 导出协议preload / setup fallbackVextJS 明确需要 gRPC h2c 或 HTTPEgg/Koa initOtel()误以为所有入口都支持单独 protocol 字段
headersOTLP 导出请求头preload需要为 Collector 注入固定头请求内动态 headers误以为这是要采集业务请求头
sampling.ratioTrace 采样率preload需要在启动阶段控制采样请求级 attribute误把它当“某个请求要不要采”的业务判断
metricIntervalMsmetric reader 导出周期preload需要统一 metric 推送节奏请求级逻辑误以为越小越好,忽略上报频率成本

opentelemetryPlugin()

适用:VextJS plugin setup + request 阶段。

import { opentelemetryPlugin } from "@devcodex/opentelemetry/vextjs";

export default opentelemetryPlugin({
  serviceName: "my-app",
  endpoint: "47.89.182.109:32767",
  protocol: "grpc",
  insecure: true,
  tracing: {
    ignorePaths: ["/health", "/_otel/status"],
  },
  logs: {
    bridgeAppLogger: true,
    globalAttributes: { "app.version": "1.0.0" },
  },
});
字段作用生效阶段推荐场景不推荐场景常见误用
enabled关闭 VextJS plugin setupsetup临时禁用当前应用 OTel plugin代替 SDK 级开关语义误以为它能回滚 preload 已启动的 SDK
serviceName影响运行期 tracer / meter / logger 命名setupVextJS plugin 级命名期望它回写 preload Resource误解为“它和 package.json 的 serviceName 完全等价”
endpointsetup 阶段追加/覆盖 exporter 目标setup运行期补充导出目标请求级属性误把它当成请求数据采集配置
protocolsetup 阶段 exporter 协议(VextJS 专属)setup需要显式切 gRPC/HTTPEgg/Koa误以为所有框架都支持此字段
headerssetup 阶段 OTLP 请求头setupVextJS 运行时补 header请求级业务 header 采集混淆“导出请求头”和“业务请求头”
insecuregRPC 连接方式(h2c/TLS)setup区分内网 collector 与公网 TLS collectorHTTP 模式忽略它只对 protocol: "grpc" 有意义
resourceAttributes兼容占位字段;当前 plugin 阶段不会重新写入 SDK Resourcesetup仅用于说明兼容语义依赖它在 plugin 阶段动态改 Resource误以为写了就会改变已启动 SDK 的 Resource
statusEndpoint兼容占位;当前不支持自定义路径setup历史配置过渡说明新项目自定义路径误以为可以改默认 /_otel/status 路径
tracing / metrics / lifecycle / logs请求观测配置requestVextJS 请求期观测与日志桥接SDK 启动参数把它们和 preload 配置混在一起理解

app.config.otel.enabled / serviceName / endpoint / protocol / headers 会作为 VextJS plugin setup 的运行期 fallback 读取。 在默认 Vext 自动 preload 模式下,app.config.otel.enabled=false 能阻止实际 SDK/exporter 启动;只有显式 preloadSdk=true / VEXT_OTEL_FORCE_SDK=1 的进程最早期 SDK 场景早于 default.ts。 如果要关闭 Collector 上报,请使用 endpoint: "none" 或 enabled:false;如果要在 preload 阶段完全早退,可使用 package.json vext.otel.enabled=false、VEXT_OTEL_DISABLED=1 或 OTEL_SDK_DISABLED=true。

initOtel()

适用:Egg / Koa 的 --require 预加载场景。

"use strict";
const { initOtel } = require("@devcodex/opentelemetry/koa");
const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");

initOtel({
  serviceName: "my-service",
  endpoint: process.env.OTEL_COLLECTOR_ENDPOINT || "47.89.182.109:32767",
  headers: { "x-tenant": "prod" },
  instrumentations: [new HttpInstrumentation()],
  metricIntervalMs: 15000,
});
字段作用生效阶段推荐场景不推荐场景常见误用
serviceNameResource 的服务名preloadEgg/Koa 启动时统一服务名请求级 hook误把它当中间件配置的一部分
endpoint导出目标preloadEgg/Koa collector 导出请求采集逻辑误以为 host:port 默认是 HTTP
headersOTLP 导出请求头preload需要为 exporter 注入固定 headers业务 header 采集把 exporter header 和 request header 混为一谈
instrumentations额外自动埋点列表preload需要补 HTTP / DB / Redis instrumentation请求生命周期副作用误以为这是 HttpOtelOptions 的一部分
metricIntervalMsmetrics 导出周期preload统一 metric 推送节奏请求逻辑忽略采集频率成本

HttpOtelOptions.tracing

适用:所有 HTTP 适配器共享的请求追踪配置。

字段作用生效阶段推荐场景不推荐场景常见误用
enabled开关 tracingrequest局部禁用某组中间件追踪代替 SDK 是否启动误解为能停掉整个 SDK
ignorePaths忽略指定路径request start健康检查、状态页、噪音路径动态业务判断把它当权限控制
spanNameResolver自定义 Span 名称request end需要使用 route 模板收敛 Span 名access log把日志格式化职责塞进这里
startAttributes请求开始阶段额外 Span 属性request start稳定请求头、租户、requestId、客户端来源需要完整 raw ctx 的字段在回调里做副作用、回写上下文
endAttributes请求结束阶段额外 Span 属性request endroute/statusCode/latency/query/params/body 等结束后更完整的信息access log / 业务副作用在回调里打日志或修改 ctx/req

HttpOtelOptions.metrics

字段作用生效阶段推荐场景不推荐场景常见误用
enabled开关 HTTP 指标request某些场景只保留 tracing 不保留 metrics代替 SDK metrics reader 配置误解为它能改 metric 导出周期
durationBuckets时长直方图分桶request end希望根据接口 SLA 调整桶边界不清楚实际延迟分布时盲调分桶设得过细导致观测噪音
labels附加指标标签request end少量、低基数业务维度user.id、query、full path、body把高基数字段塞进 metrics,导致成本膨胀

HttpOtelOptions.lifecycle

字段作用生效阶段推荐场景不推荐场景常见误用
onStart请求开始阶段副作用request start回写 ctx/req、准备上下文状态Span 属性返回用它做主要 tracing 配置
onEnd请求结束阶段副作用request endaccess log、trace 关联日志、收尾动作Span 名称 / 主属性配置把它当 endAttributes 替代品

HttpOtelOptions.logs

字段作用生效阶段推荐场景不推荐场景常见误用
globalAttributes附加到通过适配器 log bridge 发出的全局日志属性request / emit应用级稳定字段,如 app.version、deployment.environment高变字段把每次请求都变化的字段塞进 globalAttributes
bridgeAppLoggerVextJS 中把 app.logger 桥接到 OTel Logssetup / request希望框架日志自动进入 OTel Logs非 VextJS 适配器误以为所有框架都有同名自动桥接

常见采集诉求速查

我想做什么推荐入口原因注意事项
采请求头优先 capture.headers;复杂场景再用 tracing.startAttributes / tracing.endAttributes高频场景可以声明式配置,特殊场景再回到 resolverheaders 仍只建议白名单;不要采 authorization / cookie
采 query / params / body优先 capture.query / capture.params / capture.body常见场景可声明式配置;请求结束阶段上下文更完整query/params 支持显式全量模式;body 仍只建议白名单
打 access loglifecycle.onEnd这是副作用,不是 Span 属性不要塞进 endAttributes
回写 ctx / req 字段lifecycle.onStart / lifecycle.onEnd这是运行时副作用不要用 attribute resolver 代替
改 Span 名tracing.spanNameResolver直接作用于追踪主语义不要用 lifecycle 模拟
给指标补业务标签metrics.labels这是指标维度入口只能放低基数字段

不建议默认全量采集的原因

  • 高基数:query/body/user.id/full path 很容易导致存储和查询成本暴涨。
  • 敏感信息风险:token、cookie、手机号、邮箱、密码等字段极易误采。
  • 跨框架一致性问题:不同框架对 raw request/context 的可读字段并不完全等价。

推荐策略

  • headers → 默认白名单;需要复现请求时,也可显式开启全量模式,但应同时配置排除 / 脱敏规则
  • query → 默认白名单;当你明确知道业务需要时,可显式开启全量模式
  • params → 默认白名单;路由参数天然更结构化,也可显式开启全量模式
  • body → 默认白名单;需要复现请求时,也可显式开启全量模式,但应同时配置深度 / 数量 / 脱敏规则
  • metrics labels → 只保留低基数字段

声明式 capture

capture 用来覆盖“必要字段不想每次都手写 resolver”的高频场景:

const options = {
  capture: {
    headers: ["x-tenant-id", "x-request-id"],
    query: ["page", "limit"],
    params: ["id"],
    body: ["orderNo", "customer.id"],
  },
};

生成的属性前缀固定为:

  • http.request.header.*
  • http.request.query.*
  • http.request.param.*
  • http.request.body.*

当配置 snapshot: true 或 output: "snapshot" | "both" 时,还会额外生成:

  • request.headers.raw
  • request.query.raw
  • request.params.raw
  • request.body.raw

四类输入的显式全量模式

如果你的业务场景里,确实需要把当前请求的完整输入打到观测数据里,可以显式开启:

const options = {
  capture: {
    headers: true,
    query: true,
    params: true,
    body: true,
  },
};

等价于:

const options = {
  capture: {
    headers: "*",
    query: "*",
    params: "*",
    body: "*",
  },
};

注意:

  • 这不是默认行为,而是显式 opt-in;
  • 全量模式对 headers / query / params / body 都开放;
  • 进入全量模式后,仍然会经过:
    • 敏感字段脱敏
    • 字符串长度截断
    • body 的深度/数组数量限制
    • 非标量值的安全处理
  • capture 不会自动进入 metrics.labels。

规则对象:筛选 / 排除 / 脱敏 / 输出控制

如果你不仅要“开全量”,还需要控制输出范围,可以用规则对象:

const options = {
  capture: {
    headers: {
      mode: "all",
      exclude: ["cookie"],
      sensitiveKeys: ["authorization"],
      output: "both",
    },
    body: {
      mode: "all",
      exclude: ["password"],
      sensitiveKeys: [/token/i],
      maxDepth: 6,
      maxItems: 50,
      maxValueLength: 4096,
      output: "both",
    },
  },
};

支持的关键字段:

  • mode: "allowlist" | "all"
  • fields:显式白名单字段
  • exclude:排除字段 / 路径
  • sensitiveKeys:当前 source 的敏感键规则
  • maxValueLength:字符串截断长度
  • maxDepth:对象递归展开深度(主要用于 body)
  • maxItems:数组展开数量上限(主要用于 body)
  • snapshot: true:兼容快捷开关,等价于开启快照输出
  • output: "attributes" | "snapshot" | "both"

为什么默认仍不建议 headers / body 全量?

  • headers 很容易包含 authorization、cookie 这类高风险字段;
  • body 更容易包含密码、手机号、邮箱、Token、复杂对象、超长文本;
  • 因此虽然库支持显式全量,默认策略仍应优先白名单或规则控制,避免“一开就脏”。

普通观测模式 vs 复现模式

普通观测模式(默认推荐)

const options = {
  capture: {
    headers: ["x-tenant-id", "x-request-id"],
    query: true,
    params: true,
    body: ["orderNo", "customer.id"],
  },
};
  • 适合排障、聚合分析
  • 不保留完整原始请求快照

复现模式(显式开启)

const options = {
  capture: {
    headers: {
      mode: "all",
      exclude: ["cookie"],
      sensitiveKeys: ["authorization"],
      output: "both",
    },
    query: { mode: "all", output: "attributes" },
    params: { mode: "all", output: "attributes" },
    body: {
      mode: "all",
      exclude: ["password"],
      sensitiveKeys: [/token/i],
      maxDepth: 6,
      maxItems: 50,
      output: "both",
    },
  },
};
  • 适合请求复现 / replay 增强
  • 建议和排除 / 脱敏 / 截断规则一起使用

Hono 的 body 特别说明

Hono 适配器不会在遥测中间件里主动调用 req.json() / req.parseBody() / req.formData() 去消费请求流。

capture.body 在 Hono 下只会读取 bodyCache 中已经存在的解析结果:

  • 如果前面的 handler / middleware 已经解析过 body,则可采集
  • 如果还没有解析,则跳过 capture.body

这保证了遥测能力不会为了采集 body 而破坏请求处理链路。

框架快速接入

VextJS

VextJS 是唯一同时具备 preload 初始化 与 plugin setup 两阶段配置入口的场景。

{
  "name": "admin",
  "vext": {
    "otel": {
      "serviceName": "admin",
      "endpoint": "47.89.182.109:32767",
      "protocol": "grpc"
    }
  }
}
// src/plugins/otel.ts
import { opentelemetryPlugin } from "@devcodex/opentelemetry/vextjs";

export default opentelemetryPlugin({
  serviceName: "admin",
  endpoint: "47.89.182.109:32767",
  protocol: "grpc",
  tracing: {
    ignorePaths: ["/health", "/_otel/status"],
    spanNameResolver: (ctx) => `${ctx.method} ${String(ctx.route ?? ctx.path)}`,
    startAttributes: (_ctx, req) => ({
      "tenant.id": (req.headers?.["x-tenant-id"] as string) ?? "",
    }),
    endAttributes: (_ctx, req) => ({
      "request.has_query": Boolean((req as Record<string, unknown>).query),
    }),
  },
  logs: {
    bridgeAppLogger: true,
    globalAttributes: { "app.version": "1.0.0" },
  },
  lifecycle: {
    onEnd: (ctx, _req, info) => {
      if (info.statusCode >= 500) {
        console.warn(
          `[otel] ${ctx.method} ${ctx.route ?? ctx.path} -> ${info.statusCode} trace=${info.traceId}`,
        );
      }
    },
  },
});

opentelemetryPlugin({ serviceName }) 会影响运行期 tracer / meter / logger 命名。默认 Vext 自动 preload 会延后 SDK/exporter 到 plugin setup,因此 package.json vext.otel 可作为 plugin fallback;如果显式 preloadSdk=true,SDK Resource 会在 preload 阶段固定,建议同时在 package 配置中写 serviceName。 本地开发或临时停报时,可设置 app.config.otel.enabled=false、opentelemetryPlugin({ enabled:false })、package.json vext.otel.enabled=false 或 OTEL_SDK_DISABLED=true;需要完全跳过 Vext package preload 时可加 VEXT_OTEL_PRELOAD=0。

VextJS 使用 vext start / vext dev 时,instrumentation 会通过 vext.preload 自动注入;自定义启动脚本时需手动加 --import。支持 node:module.register() 的运行时会自动注册 OpenTelemetry ESM hook,无需额外添加实验性 loader 参数:

{
  "scripts": {
    "start": "node --import @devcodex/opentelemetry/instrumentation dist/server.js"
  }
}

Egg.js

Egg.js 采用 CJS --require 预加载模式,SDK 必须在任何模块加载前完成初始化。

// app/otel-init.cjs
"use strict";
const { initOtel } = require("@devcodex/opentelemetry/koa");
const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");

initOtel({
  serviceName: "my-service",
  endpoint: process.env.OTEL_COLLECTOR_ENDPOINT || "47.89.182.109:32767",
  instrumentations: [new HttpInstrumentation()],
});
{
  "scripts": {
    "dev": "egg-bin dev --require ./app/otel-init.cjs",
    "start": "egg-scripts start --require ./app/otel-init.cjs"
  }
}
// app/middleware/otel.ts
import {
  createEggMiddleware,
  type EggContextLike,
} from "@devcodex/opentelemetry/egg";

type AppEggContext = EggContextLike & {
  user_id?: string;
  feature_flag?: string;
  state?: Record<string, unknown> & { userId?: string; user?: { id?: string } };
};

export default createEggMiddleware<AppEggContext>({
  serviceName: "my-service",
  tracing: {
    ignorePaths: [/^\/favicon/, /^\/_/, "/health"],
    spanNameResolver: (ctx) => `${ctx.method} ${ctx.route ?? ctx.path}`,
    startAttributes: (_ctx, rawCtx) => ({
      "tenant.id": rawCtx.get("x-tenant-id") || "",
    }),
    endAttributes: (_ctx, rawCtx) => ({
      "request.body.present": Boolean(rawCtx.request?.body),
    }),
  },
  lifecycle: {
    onStart: (_ctx, rawCtx) => {
      rawCtx.user_id = rawCtx.state?.userId ?? rawCtx.state?.user?.id ?? "";
      rawCtx.feature_flag = rawCtx.get("x-feature-flag") || "";
    },
    onEnd: (ctx, rawCtx, info) => {
      rawCtx.logger?.info?.(
        `${ctx.method} ${rawCtx.status} ${ctx.route ?? ctx.path} ${info.latencyMs}ms`,
      );
    },
  },
});

更完整的 Egg 示例可参考:examples/egg-middleware.ts

Koa

import Koa from "koa";
import { createKoaMiddleware } from "@devcodex/opentelemetry/koa";
import { getOtelStatus } from "@devcodex/opentelemetry";

const app = new Koa();

app.use(
  createKoaMiddleware({
    serviceName: "my-koa-app",
    tracing: { ignorePaths: ["/health", "/_otel/status"] },
  }),
);

app.use(async (ctx, next) => {
  if (ctx.path === "/_otel/status") {
    ctx.body = getOtelStatus();
    return;
  }
  await next();
});

Express

Express / Hono / Fastify 只提供 HTTP 中间件 / 插件适配层,不提供 initOtel() 子路径。 这三类框架请先通过 node --import @devcodex/opentelemetry/instrumentation ... 或自建 bootstrap 完成 SDK 初始化,再注册中间件 / 插件。

import express from "express";
import { createExpressMiddleware } from "@devcodex/opentelemetry/express";
import { getOtelStatus } from "@devcodex/opentelemetry";

const app = express();
app.use(createExpressMiddleware({ serviceName: "my-express-app" }));
app.get("/_otel/status", (_req, res) => res.json(getOtelStatus()));

Hono

import { Hono } from "hono";
import { createHonoMiddleware } from "@devcodex/opentelemetry/hono";
import { getOtelStatus } from "@devcodex/opentelemetry";

const app = new Hono();
app.use(createHonoMiddleware({ serviceName: "my-hono-app" }));
app.get("/_otel/status", (c) => c.json(getOtelStatus()));

Fastify

import Fastify from "fastify";
import { createFastifyPlugin } from "@devcodex/opentelemetry/fastify";
import { getOtelStatus } from "@devcodex/opentelemetry";

const fastify = Fastify();
await fastify.register(createFastifyPlugin({ serviceName: "my-fastify-app" }));
fastify.get("/_otel/status", () => getOtelStatus());

框架差异速查

特性VextJSEgg.js / KoaExpress / Hono / Fastify
SDK 初始化--import(自动/手动)--require otel-init.cjs--import 或应用自建 bootstrap/init 文件
exporter 配置位置package.json vext.otel / plugin optionsinitOtel()应用侧自行初始化 SDK
请求观测配置opentelemetryPlugin({ tracing/metrics/lifecycle/logs })createEggMiddleware() / createKoaMiddleware()createXxxMiddleware()
raw 参数形态reqctxExpress { req, res } / Hono c / Fastify { request, reply }
logger bridgelogs.bridgeAppLogger应用层手动应用层手动

/_otel/status 说明

  • VextJS 适配器当前会自动注册 GET /_otel/status,直接返回 app.otel.getStatus()。
  • statusEndpoint 选项仅保留作兼容占位,不支持自定义路径。
  • 其他框架如需状态接口,请在应用里手动路由到 getOtelStatus()。

内置指标

⚠️ 命名说明:OTLP 中所有字段名遵循 OTel 语义约定,使用点分隔(.)。表中同时给出 OTLP 原始名称和 Prometheus 端转换后的查询名称,详见下方字段命名约定。

OTLP 指标名Prometheus 查询名类型OTLP 属性键Prometheus 标签键
http.server.durationhttp_server_durationHistogram (ms)http.method / http.status_code / http.routehttp_method / http_status_code / http_route
http.server.request.totalhttp_server_request_totalCounterhttp.method / http.status_code / http.routehttp_method / http_status_code / http_route
http.server.active_requestshttp_server_active_requestsUpDownCounterhttp.methodhttp_method
http.server.request.sizehttp_server_request_sizeHistogram (bytes)http.methodhttp_method
http.server.response.sizehttp_server_response_sizeHistogram (bytes)http.method / http.status_codehttp_method / http_status_code

额外自定义标签请通过 metrics.labels 提供,并保持低基数。

字段命名约定

OTLP 协议本身保留点分隔命名(http.status_code),但导出到不同后端时会发生自动转换:

后端转换规则示例
Prometheus / Grafana. → _http.status_code → http_status_code;http.server.duration → http_server_duration
Jaeger / Zipkin保持原样(. 分隔)http.status_code 不变
ClickHouse / Tempo通常保持原样,视采集器配置而定—

此转换由 OTLP Exporter / Collector / 后端 在写入时自动完成,SDK 侧不做转换。

对 metrics.labels 自定义标签的影响

通过 metrics.labels 返回的自定义标签键,若包含 .,在 Prometheus 中会被自动替换为 _:

// 配置侧:使用 OTel 语义约定的点分隔命名
const options = {
  metrics: {
    labels: (_ctx, raw) => ({
      "tenant.id": raw.get?.("x-tenant-id") ?? "", // 点分隔
      "app.version": "1.2.0", // 点分隔
    }),
  },
};

// Prometheus 查询侧:自动转换为下划线
// tenant_id="t-001", app_version="1.2.0"

💡 建议:在同一项目中统一选择点分隔(OTel 语义约定)或下划线(Prometheus 惯用),避免混用造成查询混乱。如果你的主要后端是 Prometheus / Grafana,可以直接在 metrics.labels 键名里用 _,OTLP 侧也能正常传递,这样不需要在脑中做转换。

在代码中访问

import {
  createWithSpan,
  getActiveTraceId,
  getOtelStatus,
} from "@devcodex/opentelemetry";

const withSpan = createWithSpan("my-service");

const userResult = await withSpan("db.user.find", () => UserModel.findById(id));

const paymentResult = await withSpan("payment.process", async (span) => {
  const res = await processPayment(body);
  span.setAttribute("payment.result", res.status);
  return res;
});

const paymentResultWithAttrs = await withSpan(
  "payment.process",
  () => processPayment(body),
  {
    attributes: { "payment.provider": "stripe" },
  },
);

const traceId = getActiveTraceId(); // 无 active span 时返回 ''

console.log(getOtelStatus());
// {
//   sdk: "initialized",
//   serviceName: "my-service",
//   exportMode: "otlp-grpc",
//   exportTarget: "47.89.182.109:32767",
//   protocol: "grpc",
//   autoInstrumentation: true,
//   samplingRatio: 1
// }

许可证

MIT © VextJS Contributors

FAQs

Package last updated on 20 Aug 2026

Related posts