---
title: "自定义框架集成"
description: "使用 defineFrameworkIntegration 或更底层的辅助函数，为尚无集成的框架或运行时构建 evlog 支持。"
canonical_url: "https://evlog.zhcndoc.com/extend/custom-framework"
---
# 自定义框架集成

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

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

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

<callout color="warning" icon="i-lucide-flask-conical">

工具包 API 被标记为 **beta**。其接口面已保持稳定（所有内置集成都在使用），但可能会根据社区反馈继续演进。

</callout>

<table>
<thead>
  <tr>
    <th>
      接口
    </th>
    
    <th>
      它的作用
    </th>
    
    <th>
      何时使用
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <a href="#manifest-mode-recommended">
        <code>
          defineFrameworkIntegration()
        </code>
      </a>
    </td>
    
    <td>
      声明式地连接请求提取 + logger 挂载
    </td>
    
    <td>
      具有 <code>
        (ctx, next)
      </code>
      
       中间件形状的 HTTP 框架（Hono、Express、Fastify、Elysia、NestJS 这类形状）
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="#custom-mode">
        <code>
          createMiddlewareLogger()
        </code>
      </a>
    </td>
    
    <td>
      命令式路径：在请求开始时创建 logger，在响应结束时发出
    </td>
    
    <td>
      生命周期不适合 <code>
        (ctx, next)
      </code>
      
       的框架（NestJS 拦截器、Next.js App Router、SvelteKit <code>
        handle
      </code>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="#non-http-runtimes">
        <code>
          createRequestLogger()
        </code>
      </a>
    </td>
    
    <td>
      将任意工作单元包裹在 logger 生命周期中
    </td>
    
    <td>
      非 HTTP 运行时（队列 worker、CLI、cron、durable workflows）
    </td>
  </tr>
</tbody>
</table>

<prompt :actions="["copy","cursor","claude"]" description="为自定义框架构建 evlog 集成" icon="i-lucide-puzzle">

将 evlog 接入一个没有内置集成的 HTTP 框架（或非 HTTP 运行时）。

- 对于具有 `(ctx, next)` 的 HTTP 框架，从 `evlog/toolkit` 使用 `defineFrameworkIntegration`，声明返回 `{ method, path, headers, requestId? }` 的 `extractRequest(ctx)`、`attachLogger(ctx, logger)`，以及来自 `createLoggerStorage()` 的可选存储（在 Workers / edge 上优先使用 `evlog/toolkit/storage`）
- Headers 可以是 Web `Headers` 或 Node `IncomingHttpHeaders`。`defineFrameworkIntegration` 会将两者统一处理
- 在中间件中调用 `integration.start(ctx, options)`，它会返回 `{ skipped, finish, runWith, logger, middlewareOptions }`
- 如果 `skipped` 为 `true`，直接跳转到 `next`
- 在 `runWith(() => next())` 中运行下游处理程序，以便 `AsyncLocalStorage` 和 `log.fork()` 正常工作
- 成功时：`await finish({ status })`；出错时：`await finish({ error })`，然后重新抛出错误
- 暴露 `drain`、`enrich`、`keep`、`include`、`exclude`、`routes` 和 `plugins` 选项
- 在 Cloudflare Workers / Vercel Edge 上，传入 `waitUntil`（或在清单上配置 `extractWaitUntil`），以便异步 drain 在响应之后完成
- 对于非 HTTP 运行时（队列 worker、CLI、cron），直接使用 `evlog/toolkit` 中的 `createRequestLogger`。将每个工作单元包裹在 logger 生命周期中

文档：[https://www.evlog.dev/extend/custom-framework](https://www.evlog.dev/extend/custom-framework)

</prompt>

## 安装

<code-group>

```bash [pnpm]
pnpm add evlog
```

```bash [bun]
bun add evlog
```

```bash [yarn]
yarn add evlog
```

```bash [npm]
npm install evlog
```

</code-group>

## 工具包里有什么

<table>
<thead>
  <tr>
    <th>
      导出
    </th>
    
    <th>
      用途
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        defineFrameworkIntegration(spec)
      </code>
    </td>
    
    <td>
      清单工厂 — 提取请求、创建 logger、挂载 logger，并通过 ALS 运行
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        createMiddlewareLogger(opts)
      </code>
    </td>
    
    <td>
      更底层的生命周期（自定义模式）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        waitUntil
      </code>
      
       on middleware options
    </td>
    
    <td>
      在 Cloudflare Workers / Vercel Edge 上延迟 drain（参见<a href="#serverless-workers-and-edge">
        无服务器：Workers 和 Edge
      </a>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        createRequestLogger(opts)
      </code>
    </td>
    
    <td>
      将非 HTTP 工作单元包裹在 logger 生命周期中
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        BaseEvlogOptions
      </code>
    </td>
    
    <td>
      面向用户的基础选项 — <code>
        drain
      </code>
      
      、<code>
        enrich
      </code>
      
      、<code>
        keep
      </code>
      
      、<code>
        include
      </code>
      
      、<code>
        exclude
      </code>
      
      、<code>
        routes
      </code>
      
      、<code>
        plugins
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        MiddlewareLoggerResult
      </code>
    </td>
    
    <td>
      返回类型：<code>
        { logger, finish, skipped }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        extractSafeHeaders(headers)
      </code>
    </td>
    
    <td>
      从 Web API <code>
        Headers
      </code>
      
       对象中过滤敏感标头
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        extractSafeNodeHeaders(headers)
      </code>
    </td>
    
    <td>
      从 Node.js <code>
        IncomingHttpHeaders
      </code>
      
       中过滤敏感标头
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        createLoggerStorage(hint)
      </code>
    </td>
    
    <td>
      返回由 <code>
        AsyncLocalStorage
      </code>
      
       支持的 <code>
        { storage, useLogger }
      </code>
      
       的工厂 — <code>
        evlog/toolkit/storage
      </code>
      
       中也提供该导出（在 Workers / edge 上优先使用该入口，以隔离 <code>
        node:async_hooks
      </code>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        attachForkToLogger(storage, parent, opts, lifecycle?)
      </code>
    </td>
    
    <td>
      将 <code>
        log.fork(label, fn)
      </code>
      
       连接到请求 logger，使消费者可以生成具有关联性的后台工作 — 清单模式会自动使用；在自定义模式下，在 <code>
        createMiddlewareLogger
      </code>
      
       返回 logger 后、生命周期结束前手动调用。<code>
        log.fork()
      </code>
      
       会返回一个 promise，在子事件完成排空后解析。可选的 <code>
        ForkLifecycle
      </code>
      
      （<code>
        onChildEnter
      </code>
      
      、<code>
        onChildExit
      </code>
      
      ）会在每个子事件周围运行，<code>
        onChildExit
      </code>
      
       会在其排空后触发
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        defineEvlog(config)
      </code>
    </td>
    
    <td>
      标准配置对象 — 适用于 <code>
        initLogger
      </code>
      
       和中间件选项
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        definePlugin(plugin)
      </code>
    </td>
    
    <td>
      插件契约 — 可选择加入 <code>
        setup
      </code>
      
      、<code>
        enrich
      </code>
      
      、<code>
        drain
      </code>
      
      、<code>
        keep
      </code>
      
      、<code>
        onRequestStart
      </code>
      
      、<code>
        onRequestFinish
      </code>
      
      、<code>
        onClientLog
      </code>
      
      、<code>
        extendLogger
      </code>
      
       中的任意子集
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        composeEnrichers / composeDrains / composeKeep / composePlugins
      </code>
    </td>
    
    <td>
      将多个扩展组合成一个
    </td>
  </tr>
</tbody>
</table>

像 `RequestLogger`、`DrainContext`、`EnrichContext`、`WideEvent` 和 `TailSamplingContext` 这样的类型都从主 `evlog` 包中导出。

## 清单模式（推荐）

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

<code-collapse>

```typescript [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
    }
  }
}
```

</code-collapse>

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

### `defineFrameworkIntegration` 的作用

基于上面的清单，这个辅助函数会：

1. 规范化标头（自动检测 `Headers` 和 `IncomingHttpHeaders`）。
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`：

```typescript
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`](https://evlog.zhcndoc.com/integrate/frameworks/cloudflare-workers) 和 Nitro 插件相同。

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

```typescript
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`：

```typescript
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 服务器是正确的行为）。

`log.fork()` 使用相同的钩子。它会在调用时、处理程序仍处于作用域内时，将子事件生命周期注册到 `waitUntil`，因此即使没有人等待分叉，它也会在运行时停止前完成排空。

## 非 HTTP 运行时

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

```ts
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 请求上的[身份标头](https://evlog.zhcndoc.com/extend/identity-headers)。变化的只有入口形状。

## 参考实现

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

<table>
<thead>
  <tr>
    <th>
      框架
    </th>
    
    <th>
      行数
    </th>
    
    <th>
      模式
    </th>
    
    <th>
      源码
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Hono
    </td>
    
    <td>
      ~50
    </td>
    
    <td>
      manifest
    </td>
    
    <td>
      <a href="https://github.com/evloghq/evlog/blob/main/packages/evlog/src/hono/index.ts" rel="nofollow">
        hono/index.ts
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Express
    </td>
    
    <td>
      ~50
    </td>
    
    <td>
      manifest + ALS
    </td>
    
    <td>
      <a href="https://github.com/evloghq/evlog/blob/main/packages/evlog/src/express/index.ts" rel="nofollow">
        express/index.ts
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Fastify
    </td>
    
    <td>
      ~70
    </td>
    
    <td>
      manifest + Fastify hooks
    </td>
    
    <td>
      <a href="https://github.com/evloghq/evlog/blob/main/packages/evlog/src/fastify/index.ts" rel="nofollow">
        fastify/index.ts
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Elysia
    </td>
    
    <td>
      ~80
    </td>
    
    <td>
      manifest + custom ALS scoping
    </td>
    
    <td>
      <a href="https://github.com/evloghq/evlog/blob/main/packages/evlog/src/elysia/index.ts" rel="nofollow">
        elysia/index.ts
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      NestJS
    </td>
    
    <td>
      ~120
    </td>
    
    <td>
      custom (interceptor)
    </td>
    
    <td>
      <a href="https://github.com/evloghq/evlog/blob/main/packages/evlog/src/nestjs/" rel="nofollow">
        nestjs/
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      SvelteKit
    </td>
    
    <td>
      ~90
    </td>
    
    <td>
      custom (<code>
        handle
      </code>
      
       hook)
    </td>
    
    <td>
      <a href="https://github.com/evloghq/evlog/blob/main/packages/evlog/src/sveltekit/" rel="nofollow">
        sveltekit/
      </a>
    </td>
  </tr>
</tbody>
</table>

<callout color="neutral" icon="i-lucide-heart">

为我们尚未支持的框架构建了集成？[提交 PR](https://github.com/evloghq/evlog/pulls)。社区会感谢你。

</callout>

## 下一步

- [自定义 Drains](https://evlog.zhcndoc.com/extend/custom-drains)：用于 drain 目标的相同工具包形状
- [自定义 Enrichers](https://evlog.zhcndoc.com/extend/custom-enrichers)：用于派生事件字段的相同工具包形状
- [Plugins](https://evlog.zhcndoc.com/extend/plugins)：多钩子扩展（在一个对象中组合 drain + enrich + keep）
- [Wide Events](https://evlog.zhcndoc.com/learn/wide-events)：通过上下文分层设计全面的事件
- [Sampling](https://evlog.zhcndoc.com/learn/sampling)：通过头部采样和尾部采样控制日志量
- [Adapters](https://evlog.zhcndoc.com/integrate/adapters/overview)：将日志发送到 Axiom、Sentry、PostHog 等更多服务。

---

- [自定义 Drain](https://evlog.zhcndoc.com/extend/custom-drains)
- [自定义 Enricher](https://evlog.zhcndoc.com/extend/custom-enrichers)


## Sitemap

See the full [sitemap](https://evlog.zhcndoc.com/sitemap.md) for all pages.
