扩展

自定义框架集成

使用 defineFrameworkIntegration 或更底层的辅助函数,为尚无集成的框架或运行时构建 evlog 支持。

当你使用的框架还没有 evlog/<framework> 包时,需要自行构建集成。evlog/toolkit 提供了驱动所有内置集成(Hono、Express、Fastify、Elysia、NestJS、SvelteKit)的相同构建模块,因此你只需编写框架相关的胶水代码。

其核心思路始终相同:请求生命周期 → 创建 logger → enrich → drain。工具包会处理请求上下文的传递。

工具包 API 被标记为 beta。其接口面已保持稳定(所有内置集成都在使用),但可能会根据社区反馈继续演进。
接口它的作用何时使用
defineFrameworkIntegration()声明式地连接请求提取 + logger 挂载具有 (ctx, next) 中间件形状的 HTTP 框架(Hono、Express、Fastify、Elysia、NestJS 这类形状)
createMiddlewareLogger()命令式路径:在请求开始时创建 logger,在响应结束时发出生命周期不适合 (ctx, next) 的框架(NestJS 拦截器、Next.js App Router、SvelteKit handle
createRequestLogger()将任意工作单元包裹在 logger 生命周期中非 HTTP 运行时(队列 worker、CLI、cron、durable workflows)

为自定义框架构建 evlog 集成

安装

pnpm add evlog

工具包里有什么

导出用途
defineFrameworkIntegration(spec)清单工厂 — 提取请求、创建 logger、挂载 logger,并通过 ALS 运行
createMiddlewareLogger(opts)更底层的生命周期(自定义模式)
waitUntil on middleware options在 Cloudflare Workers / Vercel Edge 上延迟 drain(参见无服务器:Workers 和 Edge
createRequestLogger(opts)将非 HTTP 工作单元包裹在 logger 生命周期中
BaseEvlogOptions面向用户的基础选项 — drainenrichkeepincludeexcluderoutesplugins
MiddlewareLoggerResult返回类型:{ logger, finish, skipped }
extractSafeHeaders(headers)从 Web API Headers 对象中过滤敏感标头
extractSafeNodeHeaders(headers)从 Node.js IncomingHttpHeaders 中过滤敏感标头
createLoggerStorage(hint)返回由 AsyncLocalStorage 支持的 { storage, useLogger } 的工厂 — evlog/toolkit/storage 中也提供该导出(在 Workers / edge 上优先使用该入口,以隔离 node:async_hooks
attachForkToLogger(storage, parent, opts)log.fork(label, fn) 连接到请求 logger,使消费者可以生成具有关联性的后台工作 — 清单模式会自动使用;在自定义模式下,在 createMiddlewareLogger 返回 logger 后、生命周期结束前手动调用
defineEvlog(config)标准配置对象 — 适用于 initLogger 和中间件选项
definePlugin(plugin)插件契约 — 可选择加入 setupenrichdrainkeeponRequestStartonRequestFinishonClientLogextendLogger 中的任意子集
composeEnrichers / composeDrains / composeKeep / composePlugins将多个扩展组合成一个

RequestLoggerDrainContextEnrichContextWideEventTailSamplingContext 这样的类型都从主 evlog 包中导出。

清单模式(推荐)

大多数框架都符合 (ctx, next) 中间件形状。对于这些框架,编写一个清单来描述如何提取请求和挂载 logger。defineFrameworkIntegration 会完成其余工作。

my-framework-evlog.ts
import type { IncomingMessage, ServerResponse } from 'node:http'
import {
  createLoggerStorage,
  defineFrameworkIntegration,
  type BaseEvlogOptions,
} from 'evlog/toolkit'
// On Workers / edge, prefer: import { createLoggerStorage } from 'evlog/toolkit/storage'
import type { RequestLogger } from 'evlog'

export type MyFrameworkEvlogOptions = BaseEvlogOptions

const { storage, useLogger } = createLoggerStorage(
  '无法在中间件上下文之外访问 logger。请确保在路由之前注册了 evlog 中间件。',
)

export { useLogger }

const integration = defineFrameworkIntegration<IncomingMessage>({
  name: 'my-framework',
  extractRequest: (req) => ({
    method: req.method || 'GET',
    path: req.url || '/',
    headers: req.headers,
    requestId: typeof req.headers['x-request-id'] === 'string'
      ? req.headers['x-request-id']
      : undefined,
  }),
  attachLogger: (req, logger) => {
    (req as IncomingMessage & { log: RequestLogger }).log = logger
  },
  storage,
})

export function evlog(options: MyFrameworkEvlogOptions = {}) {
  return async (req: IncomingMessage, res: ServerResponse, next: () => Promise<void>) => {
    const { skipped, finish, runWith } = integration.start(req, options)
    if (skipped) {
      await next()
      return
    }
    try {
      await runWith(() => next())
      await finish({ status: res.statusCode })
    } catch (error) {
      await finish({ error: error as Error })
      throw error
    }
  }
}

就是这样。这个中间件可以免费获得所有功能:路由过滤、drain 适配器、enricher、尾部采样、错误捕获、插件生命周期钩子、log.fork() 和持续时间跟踪。

defineFrameworkIntegration 的作用

基于上面的清单,这个辅助函数会:

  1. 规范化标头(自动检测 HeadersIncomingHttpHeaders)。
  2. 如果 extractRequest 没有返回 requestId,则生成一个 requestId
  3. 使用合并后的选项调用 createMiddlewareLogger
  4. 调用 attachLogger(ctx, logger)
  5. 在提供 storage 时,将 log.fork() 挂载到 logger 上(这样用户就可以生成具有关联性的后台工作)。
  6. 暴露 runWith(fn);如果配置了 storage,则在 storage.run(logger, …) 内运行 fn(),否则直接调用 fn()

你最终只需要处理框架相关的胶水代码:从哪里读取请求、在哪里附加 logger,以及如何计算响应状态。

自定义模式

如果你的框架生命周期不适合清晰的 (ctx, next) 结构(NestJS 拦截器、Next.js App Router、SvelteKit handle),那就再往下一层,直接调用 createMiddlewareLogger

import { createMiddlewareLogger, extractSafeNodeHeaders } from 'evlog/toolkit'

const { logger, finish, skipped } = createMiddlewareLogger({
  method,
  path,
  requestId,
  headers: extractSafeNodeHeaders(rawHeaders),
  ...options,
})

你需要负责 ALS 包装(storage.run)、log.fork() 挂载(通过 attachForkToLogger)以及结束生命周期,但仍可免费获得完整的处理管线(路由过滤、采样、emit、enrich、drain、插件)。

无服务器:Workers 和 Edge

在 Cloudflare Workers 和 Vercel Edge 上,运行时可能会在返回响应后立即终止。如果你的 drain 通过 HTTP 向可观测性后端发送数据,请传入 waitUntil,这样 enrich 仍会在线运行,但 drain 工作会在响应之后继续执行,其行为与 evlog/workers 和 Nitro 插件相同。

自定义模式。 为每个请求传入 waitUntil

import { waitUntil } from '@vercel/functions'
// import { waitUntil } from 'cloudflare:workers' // Vercel-style global on some runtimes

const { logger, finish, skipped } = createMiddlewareLogger({
  method,
  path,
  requestId,
  headers: extractSafeNodeHeaders(rawHeaders),
  waitUntil, // or ctx.waitUntil.bind(ctx) on Cloudflare
  ...options,
})

清单模式。 可以在 integration.start(ctx, options) 中传入 waitUntil,也可以在钩子位于框架上下文中时,在清单上声明 extractWaitUntil

const integration = defineFrameworkIntegration<WorkerContext>({
  name: 'my-framework',
  extractRequest: (ctx) => ({ /* … */ }),
  attachLogger: (ctx, logger) => { /* … */ },
  extractWaitUntil: ctx => ctx.executionCtx.waitUntil.bind(ctx.executionCtx),
})

export function evlog(options: BaseEvlogOptions = {}) {
  return async (ctx, next) => {
    const { skipped, finish, runWith } = integration.start(ctx, options)
    // Per-request override still works:
    // integration.start(ctx, { ...options, waitUntil: ctx.executionCtx.waitUntil.bind(ctx.executionCtx) })
    // …
  }
}

每个请求的 options.waitUntil 优先于 extractWaitUntil。如果两者都没有,drain 会被等待完成(这对于传统 Node.js 服务器是正确的行为)。

非 HTTP 运行时

对于队列 worker、CLI 驱动、cron 作业或持久化执行引擎,跳过 HTTP 形状的辅助函数,直接使用 evlog/toolkit 中的 createRequestLogger

import { createRequestLogger } from 'evlog/toolkit'

async function processJob(job: Job) {
  const logger = createRequestLogger({
    service: 'jobs',
    context: { jobId: job.id, queue: job.queue },
  })

  try {
    await runJob(job)
    logger.set({ status: 'success' })
  } catch (err) {
    logger.error(err)
    throw err
  } finally {
    await logger.emit()
  }
}

相同的 enrichers、相同的 drain 钩子、相同的出站 HTTP drain 请求上的身份标头。变化的只有入口形状。

参考实现

研究这些内置集成,了解不同框架的模式:

框架行数模式源码
Hono~50manifesthono/index.ts
Express~50manifest + ALSexpress/index.ts
Fastify~70manifest + Fastify hooksfastify/index.ts
Elysia~80manifest + custom ALS scopingelysia/index.ts
NestJS~120custom (interceptor)nestjs/
SvelteKit~90custom (handle hook)sveltekit/
为我们尚未支持的框架构建了集成?提交 PR。社区会感谢你。

下一步

  • 自定义 Drains:用于 drain 目标的相同工具包形状
  • 自定义 Enrichers:用于派生事件字段的相同工具包形状
  • Plugins:多钩子扩展(在一个对象中组合 drain + enrich + keep)
  • Wide Events:通过上下文分层设计全面的事件
  • Sampling:通过头部采样和尾部采样控制日志量
  • Adapters:将日志发送到 Axiom、Sentry、PostHog 等更多服务。