OpenTelemetry
本指南介绍 AdonisJS 应用程序中的 OpenTelemetry 集成。您将学习如何安装和配置 @adonisjs/otel 包,理解 OpenTelemetry 概念(如追踪和跨度),使用 HTTP 请求和数据库查询的自动检测,使用助手和装饰器创建自定义跨度,跨服务传播追踪上下文,以及为生产环境配置采样和导出器。
概述
OpenTelemetry 是一个开放标准,用于从应用程序收集遥测数据:追踪、指标和日志。@adonisjs/otel 包提供了 AdonisJS 和 OpenTelemetry 之间的无缝集成,为您提供分布式追踪和自动检测,具有合理的默认值和零配置设置。
可观测性对于理解应用程序内部发生的事情至关重要,特别是在生产环境中。当用户报告"结账页面很慢"时,追踪可以让您确切地看到时间花在哪里:是数据库查询?外部 API 调用?还是慢速服务?没有追踪,您只能猜测。
:::media
:::
此包为您处理 OpenTelemetry 设置的复杂性。运行一个命令,您的应用程序就会自动追踪 HTTP 请求、数据库查询、Redis 操作等。
OpenTelemetry 概念
在深入实现之前,您应该了解一些核心 OpenTelemetry 概念。有关全面介绍,请参阅官方 OpenTelemetry 文档。
追踪表示请求通过系统的完整旅程。当用户访问您的 API 时,追踪会捕获发生的一切:HTTP 请求、数据库查询、缓存查找、对外部服务的调用以及响应。
跨度是追踪中的单个工作单元。每个数据库查询、HTTP 请求或函数调用都可以是一个跨度。跨度具有开始时间、持续时间、名称和属性(键值元数据)。跨度按层次结构嵌套:HTTP 请求的父跨度包含该请求期间进行的每个数据库查询的子跨度。
属性是附加到跨度的键值对,提供上下文。例如,HTTP 跨度可能具有 http.method: GET、http.route: /users/:id 和 http.status_code: 200 等属性。
安装
使用以下命令安装和配置包:
node ace add @adonisjs/otel此命令在项目根目录创建 otel.ts 并进行 OpenTelemetry 初始化,在 bin/server.ts 顶部添加导入,注册提供者和中间件,并设置环境变量。
就是这样。您的应用程序现在具有 HTTP 请求、数据库查询等的自动追踪。
WARNING
导入顺序至关重要
OpenTelemetry 必须在任何其他代码加载之前初始化。SDK 需要在 http、pg 和 redis 等库被导入之前对其进行修补。这就是为什么 otel.ts 在 bin/server.ts 中作为第一行导入。
如果您移动或删除 import './otel.js' 行,自动检测将不起作用。您仍然可以创建手动跨度,但 HTTP 请求和数据库查询的自动追踪将不会被捕获。
手动设置
如果您不想使用 node ace add,以下是它配置的内容。
首先,在 otel.ts 创建一个文件进行 OpenTelemetry 初始化:
// title: otel.ts
import { init } from '@adonisjs/otel/init'
await init(import.meta.dirname)然后更新 bin/server.ts 将其作为第一行导入:
// title: bin/server.ts
/**
* OpenTelemetry 初始化 - 必须是第一个导入
*/
import './otel.js'
import { Ignitor } from '@adonisjs/core'
// ... 其余服务器设置在 adonisrc.ts 中添加提供者以挂钩到 AdonisJS 的异常处理程序并在跨度中记录错误:
// title: adonisrc.ts
{
providers: [
// ... 其他提供者
() => import('@adonisjs/otel/otel_provider'),
]
}最后,在 start/kernel.ts 中将中间件添加为第一个路由中间件,以使用路由信息丰富 HTTP 跨度:
// title: start/kernel.ts
router.use([
() => import('@adonisjs/otel/otel_middleware'),
// ... 其他中间件
])配置
配置文件位于 config/otel.ts:
// title: 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 |
导出器
默认情况下,该包使用 OTLP over gRPC 将追踪导出到 localhost:4317。这是标准的 OpenTelemetry Collector 端点。如果您在本地或基础设施中运行 OpenTelemetry Collector,追踪将自动发送到那里。
您可以使用环境变量配置导出器端点而无需更改任何代码:
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com:4317对于身份验证或自定义标头:
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=your-api-key有关所有可用选项,请参阅 OpenTelemetry 环境变量规范,并查看高级配置以获取更多自定义。
调试模式
启用调试模式以在开发期间将跨度打印到控制台:
// title: config/otel.ts
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
debug: true,
})这会添加一个 ConsoleSpanExporter,将跨度输出到您的终端,帮助您在不设置收集器的情况下可视化追踪。
启用和禁用
当 NODE_ENV === 'test' 时,OpenTelemetry 会自动禁用以避免测试期间的噪音。您可以覆盖此行为:
// title: config/otel.ts
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
/**
* 在测试中强制启用
*/
enabled: true,
/**
* 或在任何环境中强制禁用
*/
enabled: false,
})采样
在高流量生产环境中,追踪每个请求会生成大量数据。采样控制收集多少百分比的追踪:
// title: config/otel.ts
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
/**
* 采样 10% 的追踪(建议用于高流量生产)
*/
samplingRatio: 0.1,
/**
* 采样 100% 的追踪(默认,适合开发)
*/
samplingRatio: 1.0,
})采样器使用基于父级的采样,这意味着子跨度从其父级继承采样决策。这确保您始终获得完整的追踪而不是片段。
另请参阅:OpenTelemetry 采样文档
TIP
如果您提供自定义 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
自定义检测
您可以配置单个检测或添加自定义忽略的 URL:
// title: config/otel.ts
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
instrumentations: {
/**
* 添加自定义忽略的 URL(与默认值合并)
*/
'@opentelemetry/instrumentation-http': {
ignoredUrls: ['/internal/*', '/api/ping'],
mergeIgnoredUrls: true,
},
/**
* 禁用特定检测
*/
'@opentelemetry/instrumentation-pg': { enabled: false },
},
})创建自定义跨度
虽然自动检测涵盖了大多数常见操作,但您通常希望追踪自定义业务逻辑。该包为此提供了助手和装饰器。
使用 record 助手
record 助手在代码段周围创建跨度:
// title: app/services/order_service.ts
import { record } from '@adonisjs/otel/helpers'
export default class OrderService {
async processOrder(orderId: string) {
/**
* 在跨度中包装同步或异步代码
*/
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) {
/**
* 访问跨度以添加自定义属性
*/
await record('order.validate_inventory', async (span) => {
span.setAttributes({
'order.id': order.id,
'order.items_count': order.items.length,
})
// 验证逻辑...
})
}
}使用装饰器
对于类方法,装饰器提供了更简洁的语法:
// title: app/services/user_service.ts
import { span, spanAll } from '@adonisjs/otel/decorators'
export default class UserService {
/**
* 创建名为 "UserService.findById" 的跨度
*/
@span()
async findById(id: string) {
return User.find(id)
}
/**
* 自定义跨度名称和属性
*/
@span({ name: 'user.create', attributes: { operation: 'create' } })
async create(data: UserData) {
return User.create(data)
}
}要自动追踪类的所有方法,请使用 @spanAll 装饰器:
// title: app/services/payment_service.ts
import { spanAll } from '@adonisjs/otel/decorators'
/**
* 所有方法都获得跨度:"PaymentService.charge"、"PaymentService.refund" 等
*/
@spanAll()
export default class PaymentService {
async charge(amount: number) {
// ...
}
async refund(transactionId: string) {
// ...
}
}
/**
* 自定义前缀:"payment.charge"、"payment.refund" 等
*/
@spanAll({ prefix: 'payment' })
export default class PaymentService {
// ...
}在当前跨度上设置属性
使用 setAttributes 向当前活动跨度添加元数据而不创建新跨度:
// title: app/controllers/orders_controller.ts
import { setAttributes } from '@adonisjs/otel/helpers'
export default class OrdersController {
async store({ request }: HttpContext) {
const data = request.all()
/**
* 向 HTTP 跨度添加业务上下文
*/
setAttributes({
'order.type': data.type,
'order.total': data.total,
'customer.tier': data.customerTier,
})
// 处理订单...
}
}记录事件
事件是跨度内带有时间戳的注释。使用它们标记重要时刻:
// title: app/services/checkout_service.ts
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')
}
}上下文传播
当您的应用程序调用其他服务或处理后台作业时,您需要传播追踪上下文,以便所有操作都显示在同一个追踪中。
传播到队列作业
分派后台作业时,包含追踪上下文:
// title: app/controllers/orders_controller.ts
import { injectTraceContext } from '@adonisjs/otel/helpers'
export default class OrdersController {
async store({ request }: HttpContext) {
const order = await Order.create(request.all())
/**
* 在作业元数据中包含追踪上下文
*/
const traceHeaders: Record<string, string> = {}
injectTraceContext(traceHeaders)
await queue.dispatch('process-order', {
orderId: order.id,
traceContext: traceHeaders,
})
return order
}
}在您的队列 worker 中,提取上下文并继续追踪:
// title: app/jobs/process_order_job.ts
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> }) {
/**
* 从作业有效负载中提取追踪上下文
*/
const extractedContext = extractTraceContext(payload.traceContext)
/**
* 在提取的上下文中运行作业
*/
await context.with(extractedContext, async () => {
await record('job.process_order', async () => {
/**
* 此跨度将是原始 HTTP 请求跨度的子级
*/
const order = await Order.findOrFail(payload.orderId)
await this.fulfillOrder(order)
})
})
}
}用户上下文
安装 @adonisjs/auth 后,如果用户已通过身份验证,中间件会自动在跨度上设置用户属性。这包括 user.id、user.email(如果可用)和 user.roles(如果可用)。
您可以自定义此行为或添加其他用户属性:
// title: config/otel.ts
import { defineConfig } from '@adonisjs/otel'
export default defineConfig({
serviceName: 'my-app',
/**
* 禁用自动用户上下文
*/
userContext: false,
/**
* 或使用解析器自定义
*/
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,
}
},
},
})您也可以在代码的任何位置手动设置用户上下文:
// title: app/middleware/auth_middleware.ts
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,
})
return next()
}
}日志集成
该包自动将追踪上下文注入 Pino 日志,为每个日志条目添加 trace_id 和 span_id。这允许您在可观测性平台中将日志与追踪关联起来。
使用 pino-pretty 进行开发时,您可以隐藏这些字段以获得更简洁的输出:
// title: config/logger.ts
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 的所有选项,为高级用户提供完全控制:
// title: config/otel.ts
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',
/**
* 使用 HTTP 代替 gRPC
*/
traceExporter: new OTLPTraceExporter({
url: 'https://otel-collector.example.com/v1/traces',
headers: { 'x-api-key': process.env.OTEL_API_KEY },
}),
/**
* 具有批处理配置的自定义跨度处理器
*/
spanProcessors: [
new BatchSpanProcessor(new OTLPTraceExporter(), {
maxQueueSize: 2048,
scheduledDelayMillis: 5000,
}),
],
/**
* 自定义资源属性
*/
resourceAttributes: {
'deployment.region': 'eu-west-1',
'k8s.pod.name': process.env.POD_NAME,
},
})有关所有可用选项,请参阅 OpenTelemetry JS 文档。
性能考虑
OpenTelemetry 会为您的应用程序增加一些开销。SDK 需要创建跨度对象、记录时间信息并将数据导出到您的收集器。在大多数应用程序中,这种开销可以忽略不计,但您应该了解它。
对于高流量生产环境,请考虑以下建议:
使用采样来减少追踪量。
samplingRatio为0.1(10%)通常足以识别问题,同时大大减少开销和存储成本。使用批处理(默认)而不是立即发送跨度。
BatchSpanProcessor将跨度排队并批量发送,减少网络开销。选择性使用自定义跨度。自动检测涵盖了大多数需求。仅为需要额外可见性的关键业务操作添加自定义跨度。不要通过在每个类上使用
@spanAll装饰器来过度检测。
另请参阅:OpenTelemetry 采样文档
助手参考
所有助手都可从 @adonisjs/otel/helpers 获取:
| 助手 | 描述 |
|---|---|
getCurrentSpan() | 返回当前活动跨度,如果没有则返回 undefined |
setAttributes(attributes) | 在当前跨度上设置属性 |
record(name, fn) | 在函数周围创建跨度 |
recordEvent(name, attributes?) | 在当前跨度上记录事件 |
setUser(user) | 在当前跨度上设置用户属性 |
injectTraceContext(carrier) | 将追踪上下文注入载体对象(标头) |
extractTraceContext(carrier) | 从载体对象提取追踪上下文 |
otelLoggingPreset(options?) | 返回隐藏 OTEL 字段的 pino-pretty 选项 |