OpenTelemetry
本指南介绍 AdonisJS 应用中的 OpenTelemetry 集成。你将学习如何安装和配置 @adonisjs/otel 包、理解 trace(追踪)和 span(跨度)等 OpenTelemetry 概念、为 HTTP 请求和数据库查询使用自动埋点(automatic instrumentation)、使用 helper 和装饰器创建自定义 span、跨服务传播 trace 上下文,以及为生产环境配置采样和导出器(exporter)。
概述
OpenTelemetry 是一个用于从你的应用中收集遥测数据(traces、metrics 和 logs)的开放标准。@adonisjs/otel 包在 AdonisJS 与 OpenTelemetry 之间提供了无缝集成,为你提供分布式追踪和自动埋点,具备合理的默认值和零配置的设置。
可观测性对于理解应用内部发生了什么至关重要,尤其是在生产环境中。当用户报告"结账页面很慢"时,追踪能让你准确看到时间花在了哪里:是数据库查询?外部 API 调用?还是某个缓慢的服务?没有追踪,你只能靠猜测。
这个包为你处理了 OpenTelemetry 设置的复杂性。运行一条命令,你的应用就会自动追踪 HTTP 请求、数据库查询、Redis 操作等等。
OpenTelemetry 概念
在深入实现之前,你应该理解几个核心的 OpenTelemetry 概念。如需全面的介绍,请参阅 OpenTelemetry 官方文档。
一个 trace 表示一个请求在你系统中的完整旅程。当用户访问你的 API 时,trace 会捕获所发生的一切:HTTP 请求、数据库查询、缓存查找、对外部服务的调用以及响应。
一个 span 是 trace 中的单个工作单元。每次数据库查询、HTTP 请求或函数调用都可以是一个 span。span 具有开始时间、持续时间、名称和属性(键值元数据)。span 以层级方式嵌套:HTTP 请求的父 span 包含该请求期间所进行的每次数据库查询的子 span。
属性(Attributes) 是附加到 span 上、用于提供上下文的键值对。例如,一个 HTTP span 可能有 http.method: GET、http.route: /users/:id 和 http.status_code: 200 等属性。
安装
使用以下命令安装并配置该包:
node ace add @adonisjs/otel
该命令会在你项目的根目录创建 otel.ts(包含 OpenTelemetry 初始化代码),在 bin/server.ts 顶部添加导入语句,注册 provider 和 middleware,并设置环境变量。
就这样。你的应用现在已具备对 HTTP 请求、数据库查询等的自动追踪。
导入顺序至关重要
OpenTelemetry 必须在任何其他代码加载之前初始化。SDK 需要在 http、pg 和 redis 等库被导入之前对它们进行补丁(patch)。这就是为什么 otel.ts 被作为 bin/server.ts 中最前面的一行导入。
如果你移动或移除了 import './otel.js' 这一行,自动埋点将无法工作。你仍然可以创建手动 span,但 HTTP 请求和数据库查询的自动追踪将不会被捕获。
手动设置
如果你不想使用 node ace add,以下是它所配置的内容。
首先,创建一个包含 OpenTelemetry 初始化代码的文件 otel.ts:
import { init } from '@adonisjs/otel/init'
await init(import.meta.dirname)
然后更新 bin/server.ts,将其作为最前面的一行导入:
/**
* OpenTelemetry initialization - MUST be the first import
*/
import './otel.js'
import { Ignitor } from '@adonisjs/core'
// ... rest of your server setup
在 adonisrc.ts 中添加 provider,以挂钩到 AdonisJS 的异常处理器并在 span 中记录错误:
{
providers: [
// ... other providers
() => import('@adonisjs/otel/otel_provider'),
]
}
最后,在 start/kernel.ts 中将 middleware 添加为第一个路由中间件,以便用路由信息丰富 HTTP span:
router.use([
() => import('@adonisjs/otel/otel_middleware'),
// ... other middlewares
])
配置
配置文件位于 config/otel.ts:
import { defineConfig } from '@adonisjs/otel'
import env from '#start/env'
export default defineConfig({
serviceName: env.get('APP_NAME'),
serviceVersion: env.get('APP_VERSION'),
environment: env.get('APP_ENV'),
})
服务标识
该包从多个来源解析服务元数据:
| 选项 | 环境变量 | 默认值 |
|---|---|---|
serviceName | OTEL_SERVICE_NAME 或 APP_NAME | unknown_service |
serviceVersion | APP_VERSION | 0.0.0 |
environment | APP_ENV | development |
导出器(Exporters)
默认情况下,该包通过 OTLP over gRPC 将 traces 导出到 localhost:4317。这是标准的 OpenTelemetry Collector 端点。如果你在本地或基础设施中运行了 OpenTelemetry Collector,traces 将自动发送到那里。
你可以使用环境变量配置导出器端点,而无需更改任何代码:
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com:4317
对于认证或自定义请求头:
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=your-api-key
有关所有可用选项,请参阅 OpenTelemetry 环境变量规范,并查看高级配置以获得更多自定义方式。
调试模式
启用调试模式,以便在开发期间将 span 打印到控制台:
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
debug: true,
})
这会添加一个 ConsoleSpanExporter,将 span 输出到你的终端,帮助你在不设置 collector 的情况下可视化 traces。
启用与禁用
当 NODE_ENV === 'test' 时,OpenTelemetry 会自动禁用,以避免测试期间产生噪声。你可以覆盖此行为:
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
/**
* Force enable in tests
*/
enabled: true,
/**
* Or force disable in any environment
*/
enabled: false,
})
采样(Sampling)
在高流量的生产环境中,追踪每一个请求会生成海量数据。采样控制着收集多大比例的 traces:
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
/**
* Sample 10% of traces (recommended for high-traffic production)
*/
samplingRatio: 0.1,
/**
* Sample 100% of traces (default, good for development)
*/
samplingRatio: 1.0,
})
采样器使用基于父级的采样,这意味着子 span 继承其父级的采样决策。这确保你始终获得完整的 trace,而不是片段。
如果你提供了自定义的 sampler 选项,samplingRatio 将被忽略。
自动埋点
在底层,@adonisjs/otel 使用官方的 @opentelemetry/auto-instrumentations-node 包。这意味着你无需任何代码更改即可获得对大量库的自动追踪:HTTP 请求(传入和传出)、数据库查询(PostgreSQL、MySQL、MongoDB)、Redis 操作,以及更多。
我们预先配置了合理的默认值,因此一切开箱即用。不过,你可以通过配置中的 instrumentations 选项覆盖任何埋点设置。
默认忽略的 URL
为了减少噪声,以下端点默认被排除在追踪之外:
/health、/healthz、/ready、/readiness/metrics、/internal/metrics、/internal/healthz/favicon.ico- 所有
OPTIONS请求(CORS 预检)
自定义埋点
你可以配置各个埋点,或添加自定义的忽略 URL:
import { defineConfig } from '@adonisjs/otel'
import { MyCustomInstrumentation } from 'my-custom-instrumentation'
export default defineConfig({
serviceName: 'my-app',
instrumentations: {
/**
* Add custom ignored URLs (merged with defaults)
*/
'@opentelemetry/instrumentation-http': {
ignoredUrls: ['/internal/*', '/api/ping'],
mergeIgnoredUrls: true,
},
/**
* Disable a specific instrumentation
*/
'@opentelemetry/instrumentation-pg': { enabled: false },
/**
* Add a custom instrumentation
*/
'my-custom-instrumentation': new MyCustomInstrumentation(),
},
})
创建自定义 span
虽然自动埋点涵盖了大多数常见操作,但你常常会希望追踪自定义的业务逻辑。该包为此提供了 helper 和装饰器。
使用 record helper
record helper 会围绕一段代码创建一个 span:
import { record } from '@adonisjs/otel/helpers'
export default class OrderService {
async processOrder(orderId: string) {
/**
* Wrap synchronous or asynchronous code in a span
*/
const result = await record('order.process', async () => {
const order = await Order.findOrFail(orderId)
await this.validateInventory(order)
await this.chargePayment(order)
return order
})
return result
}
async validateInventory(order: Order) {
/**
* Access the span to add custom attributes
*/
await record('order.validate_inventory', async (span) => {
span.setAttributes({
'order.id': order.id,
'order.items_count': order.items.length,
})
// Validation logic...
})
}
}
使用装饰器
对于类方法,装饰器提供了更简洁的语法:
import { span, spanAll } from '@adonisjs/otel/decorators'
export default class UserService {
/**
* Creates a span named "UserService.findById"
*/
@span()
async findById(id: string) {
return User.find(id)
}
/**
* Custom span name and attributes
*/
@span({ name: 'user.create', attributes: { operation: 'create' } })
async create(data: UserData) {
return User.create(data)
}
}
要自动追踪某个类的所有方法,请使用 @spanAll 装饰器:
import { spanAll } from '@adonisjs/otel/decorators'
/**
* All methods get spans: "PaymentService.charge", "PaymentService.refund", etc.
*/
@spanAll()
export default class PaymentService {
async charge(amount: number) {
// ...
}
async refund(transactionId: string) {
// ...
}
}
/**
* Custom prefix: "payment.charge", "payment.refund", etc.
*/
@spanAll({ prefix: 'payment' })
export default class PaymentService {
// ...
}
在当前 span 上设置属性
使用 setAttributes 为当前活动的 span 添加元数据,而无需创建新的 span:
import { setAttributes } from '@adonisjs/otel/helpers'
export default class OrdersController {
async store({ request }: HttpContext) {
const data = request.all()
/**
* Add business context to the HTTP span
*/
setAttributes({
'order.type': data.type,
'order.total': data.total,
'customer.tier': data.customerTier,
})
// Process order...
}
}
记录事件
事件是 span 内带有时间戳的注解。用它们来标记重要的时刻:
import { recordEvent } from '@adonisjs/otel/helpers'
export default class CheckoutService {
async process(cart: Cart) {
recordEvent('checkout.started')
await this.validateCart(cart)
recordEvent('checkout.cart_validated')
const payment = await this.processPayment(cart)
recordEvent('checkout.payment_processed', {
'payment.method': payment.method,
'payment.amount': payment.amount,
})
await this.fulfillOrder(cart)
recordEvent('checkout.completed')
}
}
上下文传播
当你的应用调用其他服务或处理后台任务时,你需要传播 trace 上下文,以便所有操作都出现在同一个 trace 中。
传播到队列任务
在派发后台任务时,包含 trace 上下文:
import { injectTraceContext } from '@adonisjs/otel/helpers'
export default class OrdersController {
async store({ request }: HttpContext) {
const order = await Order.create(request.all())
/**
* Include trace context in job metadata
*/
const traceHeaders: Record<string, string> = {}
injectTraceContext(traceHeaders)
await queue.dispatch('process-order', {
orderId: order.id,
traceContext: traceHeaders,
})
return order
}
}
在你的队列 worker 中,提取上下文并继续该 trace:
import { extractTraceContext, record } from '@adonisjs/otel/helpers'
import { context } from '@adonisjs/otel'
export default class ProcessOrderJob {
async handle(payload: { orderId: string; traceContext: Record<string, string> }) {
/**
* Extract the trace context from the job payload
*/
const extractedContext = extractTraceContext(payload.traceContext)
/**
* Run the job within the extracted context
*/
await context.with(extractedContext, async () => {
await record('job.process_order', async () => {
/**
* This span will be a child of the original HTTP request span
*/
const order = await Order.findOrFail(payload.orderId)
await this.fulfillOrder(order)
})
})
}
}
用户上下文
当安装了 @adonisjs/auth 时,如果用户已通过身份认证,middleware 会自动在 span 上设置用户属性。这包括 user.id、user.email(如果可用)和 user.roles(如果可用)。
要使自动用户检测工作,用户必须在 OTEL middleware 运行之前完成身份认证。这意味着你需要在路由中间件栈中,在 OTEL middleware 之前注册一个 silent_auth middleware。如果这种设置不符合你的需求,你可以改为在自己的 auth middleware 中手动使用 setUser()。
你可以自定义此行为或添加额外的用户属性:
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
/**
* Disable automatic user context
*/
userContext: false,
/**
* Or customize with a resolver
*/
userContext: {
resolver: async (ctx) => {
if (!ctx.auth.user) return null
return {
id: ctx.auth.user.id,
tenantId: ctx.auth.user.tenantId,
plan: ctx.auth.user.plan,
}
},
},
})
你也可以在代码中的任何位置手动设置用户上下文。自定义属性会自动加上 user. 前缀:
import { setUser } from '@adonisjs/otel/helpers'
export default class AuthMiddleware {
async handle({ auth }: HttpContext, next: NextFn) {
await auth.authenticate()
setUser({
id: auth.user?.id,
email: auth.user?.email,
role: auth.user?.role,
// Custom attributes
tenantId: auth.user?.tenantId,
plan: auth.user?.plan,
})
return next()
}
}
请小心不要在用户属性中包含敏感数据(密码、token、API 密钥)。这些值会被发送到你的可观测性后端,并可能在 trace 查看器中可见。
日志集成
该包会自动将 trace 上下文注入到 Pino 日志中,为每条日志条目添加 trace_id 和 span_id。这让你可以在可观测性平台中将日志与 trace 关联起来。
在开发中使用 pino-pretty 时,你可以隐藏这些字段以获得更简洁的输出:
import { defineConfig, targets } from '@adonisjs/core/logger'
import app from '@adonisjs/core/services/app'
import { otelLoggingPreset } from '@adonisjs/otel/helpers'
export default defineConfig({
default: 'app',
loggers: {
app: {
transport: {
targets: targets()
.pushIf(!app.inProduction, targets.pretty({ ...otelLoggingPreset() }))
.toArray(),
},
},
},
})
要让特定字段保持可见:
otelLoggingPreset({ keep: ['trace_id', 'span_id'] })
高级配置
defineConfig 函数接受来自 OpenTelemetry Node SDK 的所有选项,为高级用户提供完全的控制权:
import { defineConfig } from '@adonisjs/otel'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
export default defineConfig({
serviceName: 'my-app',
/**
* Use HTTP instead of gRPC
*/
traceExporter: new OTLPTraceExporter({
url: 'https://otel-collector.example.com/v1/traces',
headers: { 'x-api-key': process.env.OTEL_API_KEY },
}),
/**
* Custom span processor with batching configuration
*/
spanProcessors: [
new BatchSpanProcessor(new OTLPTraceExporter(), {
maxQueueSize: 2048,
scheduledDelayMillis: 5000,
}),
],
/**
* Custom resource attributes
*/
resourceAttributes: {
'deployment.region': 'eu-west-1',
'k8s.pod.name': process.env.POD_NAME,
},
})
有关所有可用选项,请参阅 OpenTelemetry JS 文档。
性能考量
OpenTelemetry 会为你的应用增加一些开销。SDK 需要创建 span 对象、记录计时信息,并将数据导出到你的 collector。在大多数应用中,这种开销可以忽略不计,但你应该对此有所了解。
对于高流量的生产环境,请考虑以下建议:
-
使用采样 以减少 traces 的数量。
samplingRatio设为0.1(10%)通常足以识别问题,同时大幅降低开销和存储成本。 -
使用批处理(默认)而不是立即发送 span。
BatchSpanProcessor会将 span 排队并成批发送,从而降低网络开销。 -
对自定义 span 要有选择性。自动埋点已涵盖大多数需求。只为需要额外可见性的业务关键操作添加自定义 span。不要通过在每个类上使用
@spanAll装饰器来过度埋点。
Helpers 参考
所有 helper 都可从 @adonisjs/otel/helpers 中获得:
| Helper | 描述 |
|---|---|
getCurrentSpan() | 返回当前活动的 span,如果没有则返回 undefined |
setAttributes(attributes) | 在当前 span 上设置属性 |
record(name, fn) | 围绕一个函数创建一个 span |
recordEvent(name, attributes?) | 在当前 span 上记录一个事件 |
setUser(user) | 在当前 span 上设置用户属性 |
injectTraceContext(carrier) | 将 trace 上下文注入到载体对象(请求头)中 |
extractTraceContext(carrier) | 从载体对象中提取 trace 上下文 |
otelLoggingPreset(options?) | 返回隐藏 OTEL 字段的 pino-pretty 选项 |