框架

Fastify

源代码
Fastify 应用中的自动宽事件、结构化错误、排水适配器、增强器和尾部采样。

evlog/fastify 插件会自动创建一个请求范围的日志记录器,可通过 request.loguseLogger() 访问,并在响应完成时触发宽事件。

提示
在我的 Fastify 应用中设置 evlog。

- 安装 evlog:pnpm add evlog
- 启动时调用 initLogger({ env: { service: 'my-api' } })
- 或者在 vite.config.ts 中使用 evlog/vite 插件进行自动初始化(会替换 initLogger)
- 在应用中注册 evlog:import evlog from 'evlog/fastify' 并使用 app.register(evlog)
- 在路由处理程序中通过 request.log 访问日志记录器,或在任何地方使用 useLogger()
- 使用 log.set() 在请求过程中累积上下文
- 注册时可选择传入 drain、enrich、include 和 keep 选项

文档:https://www.evlog.dev/frameworks/fastify
适配器:https://www.evlog.dev/adapters

快速开始

1. 安装

终端
bun add evlog fastify

2. 初始化并注册插件

src/index.ts
import Fastify from 'fastify'
import { initLogger } from 'evlog'
import { evlog } from 'evlog/fastify'

initLogger({
  env: { service: 'my-api' },
})

const app = Fastify({ logger: false })

await app.register(evlog)

app.get('/health', async (request) => {
  request.log.set({ route: 'health' })
  return { ok: true }
})

await app.listen({ port: 3000 })
使用 Vite?evlog/vite 插件 会用编译时自动初始化替换 initLogger() 调用,剥离生产构建中的 log.debug(),并注入源代码位置。

request.log 是 evlog 的宽事件日志记录器,会覆盖请求对象上的 Fastify 内置 pino 日志记录器。pino 日志记录器仍可通过 fastify.log 在服务器级别访问,用于结构化日志记录。

宽事件

在处理函数中逐步构建上下文。一次请求 = 一个宽事件:

src/index.ts
app.get('/users/:id', async (request) => {
  const { id } = request.params as { id: string }

  request.log.set({ user: { id } })

  const user = await db.findUser(id)
  request.log.set({ user: { name: user.name, plan: user.plan } })

  const orders = await db.findOrders(id)
  request.log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })

  return { user, orders }
})

所有字段会在请求完成时合并为一个宽事件并发出:

终端输出
14:58:15 INFO [my-api] GET /users/usr_123 200 in 12ms
  ├─ orders: count=2 totalRevenue=6298
  ├─ user: id=usr_123 name=Alice plan=pro
  └─ requestId: 4a8ff3a8-...

useLogger()

使用 useLogger() 可以从调用栈中的任何位置访问请求范围的日志记录器,而无需将请求对象传递到服务层:

src/services/user.ts
import { useLogger } from 'evlog/fastify'

export async function findUser(id: string) {
  const log = useLogger()
  log.set({ user: { id } })

  const user = await db.findUser(id)
  log.set({ user: { name: user.name, plan: user.plan } })

  return user
}
src/index.ts
import { findUser } from './services/user'

app.get('/users/:id', async (request) => {
  const { id } = request.params as { id: string }
  const user = await findUser(id)
  return user
})

request.loguseLogger() 返回相同的日志记录器实例。useLogger() 使用 AsyncLocalStorage 在异步边界之间传播日志记录器。

错误处理

使用 createError 创建包含 whyfixlink 字段的结构化错误。Fastify 通过 onError 捕获抛出的错误:

src/index.ts
import { createError, parseError } from 'evlog'

app.get('/checkout', async (_request, reply) => {
  throw createError({
    message: 'Payment failed',
    status: 402,
    why: 'Card declined by issuer',
    fix: 'Try a different payment method',
    link: 'https://docs.example.com/payments/declined',
  })
})

app.setErrorHandler((error, _request, reply) => {
  const parsed = parseError(error)
  reply.status(parsed.status).send({
    message: parsed.message,
    why: parsed.why,
    fix: parsed.fix,
    link: parsed.link,
  })
})

错误会被捕获并记录,同时包含自定义上下文和结构化错误字段:

终端输出
14:58:20 ERROR [my-api] GET /checkout 402 in 3ms
  ├─ error: name=EvlogError message=Payment failed status=402
  └─ requestId: 880a50ac-...

配置

请参阅 配置参考文档 以了解所有可用选项(initLogger、中间件选项、采样、静默模式等)。

排水与增强器

在插件选项中直接配置排水适配器和增强器:

src/index.ts
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'

const userAgent = createUserAgentEnricher()

await app.register(evlog, {
  drain: createAxiomDrain(),
  enrich: (ctx) => {
    userAgent(ctx)
    ctx.event.region = process.env.FLY_REGION
  },
})

管道(批处理与重试)

对于生产环境,使用 createDrainPipeline 包装适配器以实现事件批处理和失败重试:

src/index.ts
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())

await app.register(evlog, { drain })
在服务器关闭时调用 drain.flush() 以确保所有缓冲事件已被发送。请参阅 管道文档 获取所有选项。

尾部采样

使用 keep 强制保留特定事件,无论头部采样如何:

src/index.ts
await app.register(evlog, {
  drain: createAxiomDrain(),
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})

路由过滤

使用 includeexclude 控制记录哪些路由:

src/index.ts
await app.register(evlog, {
  include: ['/api/**'],
  exclude: ['/_internal/**', '/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})

本地运行

终端
git clone https://github.com/hugorcd/evlog.git
cd evlog
bun install
bun run example:fastify

打开 http://localhost:3000 探索交互式测试界面。

源代码

在 GitHub 上浏览完整的 Fastify 示例源代码。

下一步

  • 宽事件:设计带有上下文层级的综合事件
  • 适配器:将日志发送到 Axiom、Sentry、PostHog 等
  • 采样:使用头部和尾部采样控制日志量
  • 结构化错误:抛出带有 whyfixlink 字段的错误