自定义框架集成
当你使用的框架还没有 evlog/<framework> 包时,需要自行构建集成。evlog/toolkit 提供了驱动所有内置集成(Hono、Express、Fastify、Elysia、NestJS、SvelteKit)的相同构建模块,因此你只需编写框架相关的胶水代码。
其核心思路始终相同:请求生命周期 → 创建 logger → enrich → drain。工具包会处理请求上下文的传递。
| 接口 | 它的作用 | 何时使用 |
|---|---|---|
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
bun add evlog
yarn add evlog
npm install evlog
工具包里有什么
| 导出 | 用途 |
|---|---|
defineFrameworkIntegration(spec) | 清单工厂 — 提取请求、创建 logger、挂载 logger,并通过 ALS 运行 |
createMiddlewareLogger(opts) | 更底层的生命周期(自定义模式) |
waitUntil on middleware options | 在 Cloudflare Workers / Vercel Edge 上延迟 drain(参见无服务器:Workers 和 Edge) |
createRequestLogger(opts) | 将非 HTTP 工作单元包裹在 logger 生命周期中 |
BaseEvlogOptions | 面向用户的基础选项 — drain、enrich、keep、include、exclude、routes、plugins |
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) | 插件契约 — 可选择加入 setup、enrich、drain、keep、onRequestStart、onRequestFinish、onClientLog、extendLogger 中的任意子集 |
composeEnrichers / composeDrains / composeKeep / composePlugins | 将多个扩展组合成一个 |
像 RequestLogger、DrainContext、EnrichContext、WideEvent 和 TailSamplingContext 这样的类型都从主 evlog 包中导出。
清单模式(推荐)
大多数框架都符合 (ctx, next) 中间件形状。对于这些框架,编写一个清单来描述如何提取请求和挂载 logger。defineFrameworkIntegration 会完成其余工作。
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 的作用
基于上面的清单,这个辅助函数会:
- 规范化标头(自动检测
Headers和IncomingHttpHeaders)。 - 如果
extractRequest没有返回requestId,则生成一个requestId。 - 使用合并后的选项调用
createMiddlewareLogger。 - 调用
attachLogger(ctx, logger)。 - 在提供
storage时,将log.fork()挂载到 logger 上(这样用户就可以生成具有关联性的后台工作)。 - 暴露
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 | ~50 | manifest | hono/index.ts |
| Express | ~50 | manifest + ALS | express/index.ts |
| Fastify | ~70 | manifest + Fastify hooks | fastify/index.ts |
| Elysia | ~80 | manifest + custom ALS scoping | elysia/index.ts |
| NestJS | ~120 | custom (interceptor) | nestjs/ |
| SvelteKit | ~90 | custom (handle hook) | sveltekit/ |
下一步
- 自定义 Drains:用于 drain 目标的相同工具包形状
- 自定义 Enrichers:用于派生事件字段的相同工具包形状
- Plugins:多钩子扩展(在一个对象中组合 drain + enrich + keep)
- Wide Events:通过上下文分层设计全面的事件
- Sampling:通过头部采样和尾部采样控制日志量
- Adapters:将日志发送到 Axiom、Sentry、PostHog 等更多服务。