OpenTelemetry
把日志跟活动 trace 和 span 关联起来
Elogs 提供子路径 @eastgold15/elogs/otel 把 OpenTelemetry 活动 span ID 合并到请求 context 包,access log 自动带上 trace_id / span_id。
安装
@opentelemetry/api 是可选 peer dep —— 装了就有 trace context,没装函数静默返回 undefined。
bun add @opentelemetry/api
基本用法
从 @eastgold15/elogs/otel 子路径导入 injectTraceContext,在 Elysia 的 .request() 钩子里调用一次,后续所有 hook 都能看到 trace_id / span_id:
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
import { injectTraceContext } from '@eastgold15/elogs/otel'
const app = new Elysia()
.use(createElogs())
.request(({ request, store }) => {
injectTraceContext(store.logger, request)
})
.get('/', () => 'ok')
当存在 active span 时,trace_id 和 span_id 合并到请求 context 包,出现在 access log:
🦊 2025-12-21 10:00:00.225 INFO 12.34ms GET / 200
└─ trace: { trace_id: '8c2f…', span_id: '4a1d…' }
Elogs 不启动 OpenTelemetry SDK —— 你需要自己装 tracer provider 和 instrumentation,injectTraceContext 才找得到活动 span。
API
function injectTraceContext(
logger: Pick<Logger, 'mergeContext'>,
request: Request
): Promise<TraceContextFields | undefined>
interface TraceContextFields {
trace_id: string
span_id: string
}
injectTraceContext是 async 的,因为它要动态import('@opentelemetry/api')来保持@opentelemetry/api的可选 peer dep 地位 —— 只有调用这个函数时才会发生 import。- 模块没装时,函数返回
undefined,什么都不输出。 - 没有 active span 时,函数同样返回
undefined,logger.mergeContext不会被调用。
工作机制
- 函数从 OpenTelemetry context API 查当前 active span(
trace.getSpan(context.active()))。 - 如果有 span,读
spanContext().traceId和spanContext().spanId。 - 这一对字段合并到请求 context 的
trace: { trace_id, span_id }(通过logger.mergeContext(request, { trace: { trace_id, span_id } }))。 - 合并后的 context 出现在最终 access log,跟用户合并的 context(比如
context.userId)并列。
完整 setup
最小的 OpenTelemetry setup 长这样:
// tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(),
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
然后在 Elysia app 里:
// app.ts
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
import { injectTraceContext } from '@eastgold15/elogs/otel'
import './tracing' // 先启动 OTel SDK
const app = new Elysia()
.use(createElogs())
.request(({ request, store }) => {
injectTraceContext(store.logger, request)
})
.get('/', () => 'ok')
.listen(3000)
跟 request ID 配合
request ID 和 trace ID 是独立的关联键:
requestId是你的应用层关联 ID(X-Request-Idheader)。在纯文本日志里好 grep。trace_id是 OpenTelemetry 的 trace ID。在分布式追踪 UI(Jaeger、Honeycomb 等)里用。
两个都放进 customLogFormat:
import { createElogs } from '@eastgold15/elogs'
createElogs({
config: {
requestId: true,
customLogFormat:
'{now} {level} {method} {pathname} {status} {duration}ms req={requestId} trace={context.trace.trace_id}',
},
})
或者不设 customLogFormat,让 showContextTree 把整个 context 对象作为树形行打印。
相关 API
injectTraceContext— 主入口函数TraceContextFields— 返回类型