扩展

自定义框架集成

为没有内置集成的 HTTP 框架(或非 HTTP 运行时)构建 evlog 支持。对于 (ctx, next) 中间件形状使用 defineFrameworkIntegration,或为其他情况使用 createMiddlewareLogger / createRequestLogger。

当你使用的框架还没有 evlog/<framework> 包时,就需要自己来构建集成。evlog/toolkit 提供了支撑所有内置集成(Hono、Express、Fastify、Elysia、NestJS)的相同基础构件——你只需要编写框架相关的胶水代码。

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

工具包 API 被标记为 beta。其接口面已保持稳定(所有内置集成都在使用),但可能会根据社区反馈继续演进。
Surface它的作用何时使用
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、挂载、使用 ALS 运行
createMiddlewareLogger(opts)更底层的生命周期(自定义模式)
createRequestLogger(opts)将非 HTTP 工作单元包裹在 logger 生命周期中
BaseEvlogOptions面向用户的基础选项——drainenrichkeepincludeexcluderoutesplugins
MiddlewareLoggerResult返回类型:{ logger, finish, skipped }
extractSafeHeaders(headers)从 Web API Headers 对象中过滤敏感 headers
extractSafeNodeHeaders(headers)从 Node.js IncomingHttpHeaders 中过滤敏感 headers
createLoggerStorage(hint)返回 { storage, useLogger } 的工厂,基于 AsyncLocalStorage
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'
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. 规范化 headers(自动识别 HeadersIncomingHttpHeaders)。
  2. 如果 extractRequest 没有返回 requestId,则自动生成一个。
  3. 使用合并后的选项调用 createMiddlewareLogger
  4. 调用 attachLogger(ctx, logger)
  5. 当提供了 storage 时,为 logger 附加 log.fork()(这样用户就能发起相关联的后台工作)。
  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、插件)。

非 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 钩子、以及相同的身份 headers都会用于向外发出的 HTTP drain 请求——只是入口形状不同。

参考实现

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

框架行数模式源码
Hono~50manifesthono/index.ts
Express~50manifest + ALSexpress/index.ts
Fastify~70manifest + Fastify hooksfastify/index.ts
Elysia~80manifest + 自定义 ALS 作用域elysia/index.ts
NestJS~120自定义(拦截器)nestjs/
SvelteKit~90自定义(handle 钩子)sveltekit/
为我们不支持的框架构建了一个集成?提交 PR——社区会感谢你的。

下一步

  • 自定义 Drain — drain 目标的同一套工具包形状
  • 自定义 Enricher — 派生事件字段的同一套工具包形状
  • 插件 — 多钩子扩展(将 drain + enrich + keep 合并在一个对象中)
  • 宽事件 — 使用上下文分层设计完整事件
  • 采样 — 通过头部和尾部采样控制日志量
  • 适配器 — 将日志发送到 Axiom、Sentry、PostHog 等