框架

Hono

Source Code
Automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in Hono applications.

evlog/hono 中间件会自动创建一个请求作用域的日志记录器,可通过 c.get('log') 访问,并在响应完成时触发一个广泛事件。

Prompt
在 Hono 应用中设置 evlog。

- 安装 evlog:pnpm add evlog
- 启动时调用 initLogger({ env: { service: 'my-api' } })
- 或者在 vite.config.ts 中使用 evlog/vite 插件进行自动初始化(替换 initLogger)
- 从 'evlog/hono' 导入 evlog 中间件和 EvlogVariables 类型
- 添加 app.use(evlog()) 并使用 Hono<EvlogVariables> 类型化应用
- 在路由处理程序中通过 c.get('log') 访问日志记录器
- 使用 log.set() 在请求过程中累积上下文
- 可选地将 drain、enrich、include 和 keep 选项传递给 evlog()

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

快速开始

1. 安装

Terminal
bun add evlog hono @hono/node-server

2. 初始化并注册中间件

src/index.ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { initLogger } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'

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

const app = new Hono<EvlogVariables>()

app.use(evlog())

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

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

EvlogVariables 类型让你在所有路由处理程序中都能对 c.get('log') 进行类型访问。

广泛事件

在处理程序中逐步构建上下文。一个请求 = 一个广泛事件:

src/index.ts
app.get('/users/:id', async (c) => {
  const log = c.get('log')
  const userId = c.req.param('id')

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

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

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

  return c.json({ 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-...

错误处理

使用 createError 创建带有 whyfixlink 字段的结构化错误:

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

app.get('/checkout', (c) => {
  const log = c.get('log')
  log.set({ cart: { items: 3, total: 9999 } })

  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.onError 全局处理错误,以返回结构化的 JSON 响应:

src/index.ts
import type { ContentfulStatusCode } from 'hono/utils/http-status'

app.onError((error, c) => {
  c.get('log').error(error)
  const parsed = parseError(error)

  return c.json(
    {
      message: parsed.message,
      why: parsed.why,
      fix: parsed.fix,
      link: parsed.link,
    },
    parsed.status as ContentfulStatusCode,
  )
})

parseError()status 类型定义为 number,而 Hono 的 c.json() 第二个参数期望 ContentfulStatusCode。此类型转换与运行时实际返回的类型匹配,并满足 TypeScript 的类型检查。

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

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

配置

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

排水与增强器

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

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

const userAgent = createUserAgentEnricher()

app.use(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())

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

抽样

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

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

路由过滤

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

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

客户端日志记录

使用 evlog/http 从任何前端向 Hono 服务器发送结构化日志。这适用于任何客户端框架(React、Vue、Svelte、原生 JS)。

浏览器设置

client.ts
import { initLogger, log } from 'evlog'
import { createHttpLogDrain } from 'evlog/http'

const drain = createHttpLogDrain({
  drain: { endpoint: '/v1/ingest' },
})
initLogger({ drain })

log.info({ action: 'page_view', path: location.pathname })

摄入端点

添加一个 POST 路由以接收来自浏览器的批量 DrainContext[]

src/index.ts
import type { DrainContext } from 'evlog'

app.post('/v1/ingest', async (c) => {
  const batch = await c.req.json<DrainContext[]>()
  for (const ctx of batch) {
    console.log('[BROWSER]', JSON.stringify(ctx.event))
  }
  return c.body(null, 204)
})
请参阅完整的 HTTP 排水适配器文档,了解批量处理、重试、sendBeacon 回退和身份验证选项。

本地运行

Terminal
git clone https://github.com/hugorcd/evlog.git
cd evlog
bun install
bun run example:hono

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

源代码

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

后续步骤

深入学习 Hono 集成:

  • 广泛事件:设计包含上下文层次结构的全面事件
  • 适配器:将日志发送到 Axiom、Sentry、PostHog 等
  • 抽样:使用头部和尾部抽样控制日志量
  • 结构化错误:抛出带有 whyfixlink 字段的错误