核心概念

生命周期

了解 evlog 事件从创建到排干的完整生命周期。涵盖三种模式(简单日志、宽事件、请求日志)、采样、丰富化和传输。

evlog 事件遵循从创建到传输的流水线。流水线会根据你使用的日志模式略有不同,但核心阶段(发送、采样、丰富化、传输)是共享的。

按模式概述

阶段log(简单)createLogger / createRequestLogger框架中间件
创建每次调用隐式创建createLogger({...})createRequestLogger({...})请求开始时自动创建
积累不适用(单次调用)多次调用 log.set()通过 useLogger(event) 调用 log.set()
发送立即发送手动调用 log.emit()响应结束时自动发送
采样仅头部采样头部 + 尾部采样头部 + 尾部采样
丰富化通过全局传输钩子通过全局传输钩子通过钩子或回调
传输通过全局传输钩子通过全局传输钩子通过钩子或回调

请求日志流水线

对于框架管理的请求日志,每个请求都会遵循以下流水线。中间件创建日志记录器,useLogger(event) 用于获取它:

   请求进入
  ┌──────────┐     路由被排除?
  │  过滤器  │────── 是 ──▶ 跳过(不记录)
  └──────────┘
       │ 否
  ┌──────────────────┐
  │  创建日志记录器  │  requestId、方法、路径、开始时间
  └──────────────────┘
  ┌──────────────────┐
  │  处理器运行      │  log.set() 积累上下文
  │                  │  log.error() 记录错误
  └──────────────────┘
  ┌──────────────────┐
  │  请求结束        │  计算状态码和持续时间
  └──────────────────┘
  ┌──────────────────┐
  │  尾部采样        │  evlog:emit:keep 钩子
  │  (保留?)        │  基于结果强制保留
  └──────────────────┘
  ┌──────────────────┐
  │  头部采样        │  每个级别的随机抽样
  │  (抽样?)        │  如果尾部已说“保留”则跳过
  └──────────────────┘
       │  被抽样排除? ──▶ 丢弃(无输出)
  ┌──────────────────┐
  │  发送            │  构建宽事件 + 控制台输出
  └──────────────────┘
  ┌──────────────────┐
  │  丰富化          │  evlog:enrich 钩子
  │                  │  用户代理、地理位置、追踪、自定义
  └──────────────────┘
  ┌──────────────────┐
  │  传输            │  evlog:drain 钩子
  │                  │  Axiom、OTLP、Sentry、自定义
  └──────────────────┘
   完成

逐步说明

1. 路由过滤

当请求到达时,evlog 会检查路径是否匹配配置的 include / exclude 模式。如果路由被排除,不会创建日志记录器,请求将继续执行且不产生任何日志开销。

默认情况下记录所有路由。使用 include 限制仅记录特定模式:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    include: ['/api/**'],
  },
})

2. 日志记录器创建

对于匹配的路由,evlog 会创建一个 RequestLogger 并将其附加到请求上下文。日志记录器预先填充了以下字段:

字段来源
methodHTTP 方法(GETPOST、……)
path请求路径
requestId自动生成的 UUID(在 Cloudflare 上使用 cf-ray
startTime用于计算持续时间的 Date.now()

日志记录器存储在事件上下文上。useLogger(event) 是获取它的快捷方式,不会创建新的日志记录器。

3. 上下文积累

在处理器中,你调用 log.set() 来附加上下文。每次调用都会深度合并到现有上下文中,因此可以根据需要多次调用:

server/api/checkout.post.ts
import { useLogger } from 'evlog'

const log = useLogger(event)

const user = await getUser(event)
log.set({ user: { id: user.id, plan: user.plan } })

const cart = await getCart(user.id)
log.set({ cart: { items: cart.items.length, total: cart.total } })

如果抛出错误,evlog 的 error 钩子会自动捕获并记录错误及其状态码。

4. 请求结束

当响应发送(或抛出错误)时,evlog 会计算:

  • 状态码:来自响应(或错误的 status / statusCode
  • 持续时间:从 Date.now() - startTime
  • 级别:如果记录了错误则为 error,如果状态码 >= 400 则为 warn,否则为 info

如果错误触发了发送请求,会标记该请求已发送,以防止在响应钩子中重复发送。

5. 尾部采样(evlog:emit:keep

在事件被采样之前,evlog 会评估尾部采样规则。这些规则在请求完成后运行,因此可以检查结果:

nuxt.config.ts
evlog: {
  sampling: {
    keep: [
      { duration: 1000 },          // 慢请求
      { status: 400 },             // 客户端/服务器错误
      { path: '/api/critical/**' }, // 关键路径
    ],
  },
}

evlog:emit:keep 钩子也会触发,允许你基于自定义业务逻辑强制保留:

server/plugins/evlog-custom.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    if (ctx.context.user?.premium) {
      ctx.shouldKeep = true
    }
  })
})

如果任何规则或钩子设置了 shouldKeep = true,该事件将完全跳过头部采样

6. 头部采样

如果事件未被尾部采样强制保留,则应用头部采样。这是基于日志级别的随机投掷硬币。

默认情况下所有级别都以 100% 保留(无采样)。配置 sampling.rates 可在生产环境中减少流量:

nuxt.config.ts
evlog: {
  sampling: {
    rates: { info: 10, warn: 50, debug: 0 },
  },
}
  • info: 10 - 保留 10% 的 info 级别事件
  • warn: 50 - 保留 50% 的警告
  • error 默认始终保留(即使设置了比率也不会被采样排除)

如果事件被采样排除,处理将完全停止:不会输出控制台内容,不会丰富化,也不会传输。

7. 发送

构建 WideEvent 对象,包含积累的上下文:

WideEvent
{
  "timestamp": "2026-01-15T10:30:00.000Z",
  "level": "info",
  "service": "my-app",
  "method": "POST",
  "path": "/api/checkout",
  "requestId": "abc-123",
  "duration": 234,
  "status": 200,
  "user": { "id": 1, "plan": "pro" },
  "cart": { "items": 3, "total": 9999 }
}

事件会被打印到控制台,开发环境下格式美观,生产环境下输出为 JSON。这是默认行为,无需配置。

8. 丰富化(evlog:enrich

发送后,丰富化器会向事件添加派生上下文。内置丰富化器从请求头中提取数据:

丰富化器添加内容来源
用户代理userAgent(浏览器、操作系统、设备)User-Agent
地理位置geo(国家、地区、城市)平台头(Vercel、Cloudflare)
请求大小requestSize(请求/响应字节数)Content-Length
追踪上下文traceContext(traceId、spanId)traceparent
server/plugins/evlog-enrich.ts
import { createUserAgentEnricher, createGeoEnricher } from 'evlog/enrichers'

export default defineNitroPlugin((nitroApp) => {
  const enrichers = [createUserAgentEnricher(), createGeoEnricher()]

  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  })
})

丰富化器接收完整的 EnrichContext,其中包含可变事件、请求元数据、安全头和响应信息。

9. 传输(evlog:drain

最后一步将丰富化后的事件发送到你的可观测性平台。evlog:drain 钩子接收包含完整事件的 DrainContext

server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})

在支持 waitUntil 的平台(Cloudflare Workers、Vercel Edge)上,传输在响应发送后执行以避免增加延迟。在传统服务器上,传输会被等待以防止在服务器无服务器冷启动时丢失事件。

钩子执行顺序

顺序钩子时机目的
1evlog:emit:keep请求结束后,采样前基于结果强制保留事件
2evlog:enrich发送后,传输前向事件添加派生上下文
3evlog:drain丰富化后将事件发送到外部服务

错误路径与成功路径

两条路径在相同的发送/丰富化/传输流水线中汇聚。唯一区别在于何时触发发送

成功错误
触发时机afterResponse / response 钩子error 钩子
级别info(或状态码 >= 400 时为 warnerror
状态码来自响应来自错误的 status 字段(默认为 500)
错误上下文error 字段,包含消息、堆栈、whyfix
重复发送保护检查 _evlogEmitted 标志设置 _evlogEmitted = true

简单日志流水线

使用 log 单例时,流水线更短:

  1. 调用log.info({ action: 'deploy' })log.info('tag', 'message')
  2. 发送:事件立即构建并输出
  3. 传输:如果通过 initLogger() 配置了全局 drain,事件会被发送到外部服务

标记日志(log.info('tag', 'message'))在纯文本模式下仅输出到控制台。对象形式日志(log.info({ ... }))始终通过传输流水线。

独立宽事件流水线

使用 createLogger() 在框架外使用时:

  1. 创建createLogger({ jobId: 'sync-001' })
  2. 积累log.set()log.info()log.warn()log.error() 在整个操作过程中
  3. 发送:手动调用 log.emit()
  4. 采样:头部采样基于计算出的级别应用。尾部采样通过 initLogger({ sampling: { keep: [...] } }) 配置
  5. 传输:如果配置了全局 drain,事件会被发送
scripts/migrate.ts
import { initLogger, createLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

initLogger({
  env: { service: 'worker' },
  drain: createAxiomDrain(),
  sampling: { rates: { info: 10 } },
})

const log = createLogger({ task: 'migrate' })
log.set({ records: 500, status: 'complete' })
log.emit()

下一步