Bun 与 ElysiaJS 的 OpenTelemetry 监控指南
本指南将介绍如何为 Bun 应用接入 OpenTelemetry,并把链路和指标数据发送到 SigNoz。主线方案使用专为 Bun 设计的 Web 框架 ElysiaJS。如果你的应用直接使用 Bun.serve,请参考不使用 Elysia 的 Bun 接入方案。
使用自托管 SigNoz?
大部分步骤都一样。只需按照从 Cloud 迁移到 Self-Hosted的说明,修改 endpoint 地址并移除 ingestion key 请求头即可。
前提条件
- 已安装 Bun
- 一个 SigNoz 实例(Cloud 或 Self-Hosted 均可)
- 一个 Elysia 应用。如果想从零开始,运行
bun create elysia my-app,该命令会在src/index.ts创建应用入口文件。
已在 Bun v1.4.2、Elysia 1.4.30 和 @elysia/opentelemetry 1.4.12 上测试通过。
从 Elysia 应用发送遥测数据
第 1 步:安装依赖包
在项目根目录运行以下命令:
复制bun add @elysia/opentelemetry \
@opentelemetry/sdk-trace-node \
@opentelemetry/sdk-metrics \
@opentelemetry/exporter-trace-otlp-proto \
@opentelemetry/exporter-metrics-otlp-proto \
@opentelemetry/instrumentation-runtime-node \
@opentelemetry/instrumentation-host-metrics
请使用 @elysia/opentelemetry,而不是 @elysiajs/opentelemetry
旧包 @elysiajs/opentelemetry(最高到 1.4.11 版本)无法记录 HTTP 请求指标。请使用 @elysia/opentelemetry。
第 2 步:在应用中接入 OpenTelemetry
将插件添加到你的 Elysia 应用中。exporter 会通过 OTLP/HTTP 发送数据,并从环境变量中读取 endpoint 和 ingestion key——环境变量将在下一步设置。
src/index.tsimport { Elysia } from 'elysia'
import { opentelemetry } from '@elysia/opentelemetry'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'
import { RuntimeNodeInstrumentation } from '@opentelemetry/instrumentation-runtime-node'
import { HostMetricsInstrumentation } from '@opentelemetry/instrumentation-host-metrics'
const app = new Elysia()
.use(
opentelemetry({
serviceName: process.env.OTEL_SERVICE_NAME ?? 'bun-elysia-app',
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())],
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
exportIntervalMillis: 15000,
}),
instrumentations: [
new RuntimeNodeInstrumentation(),
new HostMetricsInstrumentation(),
],
})
)
.get('/', () => 'Hello from Elysia')
.listen(3000)
在添加路由之前先调用 .use(opentelemetry(...))。Elysia 不会追踪在插件之前添加的路由。
第三步:设置环境变量并运行
VMKubernetesDockerWindows在启动应用所在环境的 shell 中设置以下变量,然后运行应用:
Copyexport OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
export OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
export OTEL_SERVICE_NAME="<service-name>"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=<environment>"
bun run src/index.ts
在部署清单的容器配置中加入这些环境变量:
Copyenv:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "https://ingest.<region>.signoz.cloud:443"
- name: OTEL_EXPORTER_OTLP_HEADERS
value: "signoz-ingestion-key=<your-ingestion-key>"
- name: OTEL_SERVICE_NAME
value: "<service-name>"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "deployment.environment.name=<environment>"
如需从本地机器发送测试请求,请将本地端口转发到该部署。把 <deployment-name> 替换为你的部署名称:
kubectl port-forward deploy/<deployment-name> 3000:3000
然后在另一个终端中运行 curl http://localhost:3000/。
在项目根目录创建一个 Dockerfile:
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
CMD ["bun", "run", "src/index.ts"]构建镜像,并在运行容器时传入环境变量:
Copydocker build -t bun-elysia-app .
docker run \
-e OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443" \
-e OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>" \
-e OTEL_SERVICE_NAME="<service-name>" \
-e OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=<environment>" \
-p 3000:3000 \
bun-elysia-app在 PowerShell 中设置变量,然后运行应用:
Copy$env:OTEL_EXPORTER_OTLP_ENDPOINT = "https://ingest.<region>.signoz.cloud:443"
$env:OTEL_EXPORTER_OTLP_HEADERS = "signoz-ingestion-key=<your-ingestion-key>"
$env:OTEL_SERVICE_NAME = "<service-name>"
$env:OTEL_RESOURCE_ATTRIBUTES = "deployment.environment.name=<environment>"
bun run src/index.ts
请核对以下参数值:
<region>:您的 SigNoz Cloud 区域。<your-ingestion-key>:您的 SigNoz 摄入密钥(ingestion key)。<service-name>:SigNoz 中显示的服务名称,例如bun-elysia-app。<environment>:部署环境,例如production。SigNoz 仪表盘会根据此字段进行过滤。
向应用发送几次请求,例如执行 curl http://localhost:3000/。
不依赖 Elysia 为 Bun 插桩
Bun.serve 不支持自动插桩,因此需要在 fetch 处理程序中为每个请求手动创建一个 span。您可以获取调用链和运行时指标,但无法获得由 Elysia 插件生成的 http.server.request.duration 指标。
安装以下包:
Copybun add @opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/sdk-metrics \
@opentelemetry/exporter-trace-otlp-proto \
@opentelemetry/exporter-metrics-otlp-proto \
@opentelemetry/instrumentation-runtime-node \
@opentelemetry/instrumentation-host-metrics在项目根目录创建 index.ts。它会启动 SDK,并为每个请求包一层 span。设置与第 3 步相同的环境变量,然后运行 bun run index.ts。
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { RuntimeNodeInstrumentation } from '@opentelemetry/instrumentation-runtime-node'
import { HostMetricsInstrumentation } from '@opentelemetry/instrumentation-host-metrics'
import { SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
exportIntervalMillis: 15000,
}),
instrumentations: [
new RuntimeNodeInstrumentation(),
new HostMetricsInstrumentation(),
],
})
sdk.start()
const tracer = trace.getTracer('bun-serve-app')
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url)
const parentContext = propagation.extract(context.active(), req.headers, {
get: (headers, key) => headers.get(key) ?? undefined,
keys: (headers) => [...headers.keys()],
})
return tracer.startActiveSpan(`${req.method} ${url.pathname}`, { kind: SpanKind.SERVER }, parentContext, (span) => {
span.setAttribute('http.request.method', req.method)
span.setAttribute('url.path', url.pathname)
try {
const res = new Response('Hello from Bun')
span.setAttribute('http.response.status_code', res.status)
return res
} catch (err) {
span.recordException(err as Error)
span.setStatus({ code: SpanStatusCode.ERROR })
span.setAttribute('http.response.status_code', 500)
return new Response('Internal Server Error', { status: 500 })
} finally {
span.end()
}
})
},
})
调用 propagation.extract 会读取传入的 traceparent 头部信息,使当前 span 加入调用方服务的 trace。若处理函数为 async,请将回调设为 async,并在 await 响应完成后调用 span.end()。由于 span 名称直接使用原始 URL 路径,带 ID 的路由会产生大量 span 名称。建议改用路由器中的路由模式。
验证
运行插桩后的应用,确认 traces 和 metrics 已到达 SigNoz:
- 向应用发送若干请求。
- 打开 Services 选项卡,查找在
OTEL_SERVICE_NAME中设置的服务名称。 - 打开 Metrics Explorer,查看
http.server.request.duration(仅限 Elysia)和v8js.memory.heap.used。


局限性
以下限制适用于 @elysia/opentelemetry 版本 1.4.12。
http.server.request.duration缺少http.route属性。需查询 traces 以按路由细分流量。http.server.request.duration记录的持续时间远小于实际请求时间,因为该插件在路由处理函数完成前就进行了记录。请以 traces 中的 span 持续时间作为延迟参考。- Bun 始终将
nodejs.eventloop.utilization报告为0。nodejs.eventloop.delay.*指标正常可用。 - Bun 不报告
process.memory.usage。内存监控请使用v8js.memory.heap.used。
故障排除
还看不到数据?请参见排查缺失的链路、日志与指标。链路可见但缺少http.server.request.duration
- 可能的原因:项目使用了
@elysiajs/opentelemetry,该包本身不包含 HTTP 指标,或者插件中未配置metricReader。若没有<