关于 create-evlog-framework-integration
这款仓库 Skill 面向为 evlog 添加框架集成的贡献者,介绍共享 manifest 方案和自定义中间件的适用场景。它列出必要 API、测试要求以及包和文档改动。
主要功能
- 01Manifest 与自定义集成路径
- 02共享中间件和存储工具
- 03集成所需 API
- 04框架专用测试指导
- 05构建、包和文档清单
适用场景
- 01为框架添加 evlog 支持
- 02选择集成架构
- 03准备集成测试与包改动
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ evloghq/ evlog --skill create-evlog-framework-integration安装后在 Claude Code 中输入/create-evlog-framework-integration确认已加载。
详细介绍
创建 evlog 框架集成
为 evlog 添加新的框架集成。对于具有请求/响应中间件结构的框架,建议采用基于 evlog/toolkit 中 defineFrameworkIntegration 的清单模式。对于生命周期从根本上不同的框架,则回退到较低层级的 createMiddlewareLogger。
两种方式
- 清单模式(首选,约 30–80 行胶合代码)。在模块级别调用一次
defineFrameworkIntegration({ name, extractRequest, attachLogger, storage? }),然后编写一个简短的中间件,调用integration.start(ctx, options),并在runWith中运行框架的next()。参考实现:packages/evlog/src/{hono,express,fastify,elysia,nestjs,orpc,react-router,sveltekit,workers}/index.ts中的所有实现均采用此模式。 - 自定义模式:当框架生命周期不符合标准中间件模式时,直接使用
createMiddlewareLogger。当前采用自定义模式的集成包括:Next.js(src/next/)、Nitro v2/v3(src/nitro/、src/nitro-v3/)、Eve(src/eve/)。
清单模式现在已覆盖所有经典 HTTP 框架。仅当无法在生命周期开始时同步提取请求时(服务器操作、模块级钩子、Agent 轮次),才使用自定义模式。
必需的 API 接口(来自 AGENTS.md)
每个框架集成都必须提供:
- 接受完整
BaseEvlogOptions(drain、enrich、keep、include、exclude、routes、plugins)的evlog()中间件/插件 useLogger()(由 ALS 支持)。有两个获准的例外:Workers(ALS 在此需要兼容标志;defineWorkerFetch会改为附加日志记录器)和 Effect(日志记录器是 Effect 上下文中的EvlogLogger服务)- 支持
log.fork()(向清单提供storage时自动启用) - 框架原生访问器(
c.get('log')、req.log、event.locals.log,……)
PR 标题
feat({framework}): add {Framework} middleware integration
范围时机注意事项:语义 PR 检查会从基分支读取范围列表,因此新范围无法验证引入该范围的 PR。请先通过一个小型 PR 注册该范围,或者在引入 PR 中使用不带范围的标题(feat: add {Framework} middleware integration)。
检查点清单
| # | 文件 | 操作 |
|---|---|---|
| 1 | packages/evlog/src/{framework}/index.ts | 创建集成源码 |
| 2 | packages/evlog/tsdown.config.ts | 添加构建入口和 external |
| 3 | packages/evlog/package.json | 添加 exports、typesVersions、可选 peer dep 和关键词 |
| 4 | packages/evlog/test/frameworks/{framework}.test.ts | 创建测试(使用真实请求驱动器和 describeStandardHttpMatrix) |
| 5 | packages/evlog/test/toolkit/__snapshots__/api-surface.test.ts.snap | 由 pnpm run build 和 pnpm test 重新生成 |
| 6 | apps/docs/content/4.integrate/frameworks/{NN}.{framework}.md | 创建框架文档页面 |
| 7 | apps/docs/content/4.integrate/frameworks/00.overview.md | 添加表格行和卡片 |
| 8 | apps/docs/content/1.start/3.installation.md | 在“选择你的框架”中添加卡片 |
| 9 | apps/docs/content/0.landing.md | 添加框架代码片段位置 |
| 10 | apps/docs/app/components/features/FeatureFrameworks.vue | 添加框架标签页 |
| 11 | skills/review-logging-patterns/SKILL.md | 添加框架设置部分并更新 frontmatter 描述 |
| 12 | packages/evlog/README.md | 添加框架部分,并在框架支持表中添加一行 |
| 13 | examples/{framework}/ | 创建带测试 UI 的示例应用(由 pnpm example {framework} 自动发现,无需根脚本) |
| 14 | .changeset/{framework}-integration.md | 创建 changeset(minor) |
| 15 | .github/workflows/semantic-pull-request.yml + .github/pull_request_template.md | 在两个文件中将 {framework} 注册为 PR 范围 |
重要:完成所有 15 个检查点后,才能认为任务已完成。
命名约定
| 占位符 | 示例(Hono) | 用途 |
|---|---|---|
{framework} | hono | 目录名、导入路径、文件名、PR 范围 |
{Framework} | Hono | 类型/接口名称中的 PascalCase |
共享工具
所有集成都共用相同的核心工具。**绝不要重新实现 shared/ 中已有的逻辑。**这些工具也通过 evlog/toolkit 向社区构建的集成公开提供(参见自定义集成文档)。
| 工具 | 位置 | 用途 |
|---|---|---|
defineFrameworkIntegration | ../shared/integration | 清单工厂:提取请求、创建日志记录器、挂载,并使用 ALS 运行 |
createMiddlewareLogger | ../shared/middleware | 更底层的生命周期(自定义模式):创建日志记录器、路由筛选、尾部采样、发送、丰富和排空 |
BaseEvlogOptions | ../shared/middleware | 面向用户的基础选项类型,包含 drain、enrich、keep、include、exclude、routes、plugins |
createLoggerStorage | ../shared/storage(evlog/toolkit/storage) | 返回 { storage, useLogger } 的工厂,用于基于 AsyncLocalStorage 的 useLogger()。在 Workers / edge 上优先使用 evlog/toolkit/storage |
shouldDeferEmitForResponse | ../shared/streamResponse | 延迟发送宽事件,直到流式响应体关闭(参见 Hono/Elysia) |
defineFrameworkIntegration 会自动:
- 规范化 Web
Headers和 NodeIncomingHttpHeaders(因此无需选择头部提取器) - 在不存在
requestId时生成一个 - 调用
createMiddlewareLogger,并提供其{ logger, finish, skipped, middlewareOptions } - 在提供
storage时自动挂载log.fork() - 提供
runWith(fn),以便在集成的 ALS 中运行下游处理器 - 在运行时提供
waitUntil时转发它(Workers、Workers 上的 Hono)
第 1 步:集成源码
创建 packages/evlog/src/{framework}/index.ts。
模板结构(清单模式)
import type { AuditableLogger } from '../audit'
import { defineFrameworkIntegration } from '../shared/integration'
import type { BaseEvlogOptions } from '../shared/middleware'
import { createLoggerStorage } from '../shared/storage'
const { storage, useLogger } = createLoggerStorage(
'middleware context. Make sure the evlog middleware is registered before your routes.',
'evlog:{framework}',
)
export type Evlog{Framework}Options = BaseEvlogOptions
export { useLogger }
// Type augmentation for typed logger access (framework-specific):
// - Express: declare module 'express-serve-static-core' { interface Request { log: AuditableLogger } }
// - Hono: export type EvlogVariables = { Variables: { log: AuditableLogger } }
const integration = defineFrameworkIntegration<{Framework}Context>({
name: '{framework}',
extractRequest: (ctx) => ({
method: /* ctx.method */,
path: /* ctx.path */,
headers: /* Web Headers OR Node headers OR plain object */,
requestId: /* x-request-id header or undefined → auto-generated */,
}),
attachLogger: (ctx, logger) => {
// Store in framework-idiomatic location:
// - Hono: c.set('log', logger)
// - Express: req.log = logger
},
storage,
})
export function evlog(options: Evlog{Framework}Options = {}): FrameworkMiddleware {
return async (ctx, next) => {
const { skipped, finish, runWith } = integration.start(ctx, options)
if (skipped) {
await next()
return
}
try {
await runWith(() => next())
await finish({ status: /* extract status from ctx */ })
} catch (error) {
await finish({ error: error as Error })
throw error
}
}
}
参考实现
- Hono:
src/hono/index.ts。c.set('log', logger)+ ALSuseLogger(),通过shouldDeferEmitForResponse延迟流式响应,检测waitUntil - Express:
src/express/index.ts。req.log、ALS 存储、使用res.on('finish')获取终止状态 - Fastify:
src/fastify/index.ts。Fastify 钩子(onRequest/onResponse/onError)、fastify-plugin包装器 - Elysia:
src/elysia/index.ts。使用.derive({ as: 'global' })的插件、storage.enterWith风格的 ALS、延迟流式响应 - NestJS:
src/nestjs/index.ts。基于清单实现EvlogModule.forRoot()/forRootAsync()。在@nestjs/platform-fastify上,中间件只能看到原始请求,因此通过HttpAdapterHost注册的preHandler钩子会将日志记录器复制到FastifyRequest.log - oRPC:
src/orpc/index.ts。evlog()过程级中间件 +withEvlog(handler)包装器 - React Router:
src/react-router/index.ts。loggerContext = createContext<AuditableLogger>() - SvelteKit:
src/sveltekit/index.ts。evlog()handle +evlogHandleError()+createEvlogHooks() - Workers:
src/workers/index.ts。defineWorkerFetch/withEvlog,不使用 ALSuseLogger()(兼容性标志限制)
对于使用 enterWith() 绑定的集成,请使用 src/shared/asyncStorageScope.ts 中的 createSharedEnterWithStorage。它的能力探测会在临时作用域和独立存储中运行,从而在 Bun 和 Node 上保留导入方的异步上下文。
核心架构规则
- 优先使用
defineFrameworkIntegration:它会处理请求头规范化、请求 ID 生成、ALS、分叉附加和waitUntil。 - 状态和错误报告由框架侧负责:成功时调用
finish({ status }),失败时调用finish({ error })。finish会执行 emit、enrich、drain 和插件钩子。 - 调用
finish({ error })后,重新抛出错误,以便框架自身的错误处理程序仍能运行。 - 流式响应:如果框架可以返回流式响应体,则将 emit 延迟到流关闭时(
shouldDeferEmitForResponse;参见 Hono 和 Elysia)。 - 框架 SDK 是可选的 peer dependency:切勿将其打包。
- 切勿重复实现流水线逻辑:
runEnrichAndDrain是createMiddlewareLogger/finish的内部实现。 - 导出类型辅助工具,以便进行带类型的上下文访问(例如,Hono 的
EvlogVariables)。
何时回退到自定义模式
在以下情况下,直接使用 createMiddlewareLogger(跳过 defineFrameworkIntegration):
- 中间件没有明确的“请求进入/响应退出”配对(Next.js App Router server actions、Eve Agent 回合)。
- 日志记录器的创建跨越由模块系统管理的多个生命周期阶段(Nitro 插件和钩子)。
- 只有在响应流完成后才能确定状态,而框架又没有提供相应的钩子。
第 2 步:构建配置
在 packages/evlog/tsdown.config.ts 中添加构建入口:
'{framework}/index': 'src/{framework}/index.ts',
同时将框架 SDK 添加到 external 数组中(例如,'elysia'、'fastify')。
第 3 步:包导出
在 packages/evlog/package.json 中:
在 exports 中(放在最后一个框架条目之后):
"./{framework}": {
"types": "./dist/{framework}/index.d.mts",
"import": "./dist/{framework}/index.mjs",
"default": "./dist/{framework}/index.mjs"
}
在 typesVersions["*"] 中:"{framework}": ["./dist/{framework}/index.d.mts"]
在 peerDependencies 中(版本范围)和 peerDependenciesMeta 中("optional": true),并将框架名称添加到 keywords。
如果某个导出没有匹配的 typesVersions["*"] 或 tsdown.config.ts 条目,或者其 import 和 default 未指向与 types 同目录的 .mjs 文件,test/toolkit/entrypoint-registry.test.ts 就会失败。该测试无需构建即可运行。
第 4 步:测试
创建 packages/evlog/test/frameworks/{framework}.test.ts。请先阅读 packages/evlog/test/README.md,特别是 Framework runtime fidelity 表格。
以下两点不可妥协:
- 使用真实请求驱动程序。 使用框架自带的驱动程序:supertest(Express/NestJS)、
app.request()(Hono)、app.inject()(Fastify)、app.handle(new Request(...))(Elysia)。如果没有适用于 Node.js 的驱动程序,则直接使用真实输入形态调用面向用户的接口(参见 SvelteKit 和 React Router 测试)。切勿提取内部实现来测试替代品。 - 接入共享测试矩阵。 从
test/helpers/frameworkMatrix.ts调用describeStandardHttpMatrix({ name, mount })。它会为所有 HTTP 框架覆盖标准测试组(事件发出、x-request-id、路由服务)。
在测试矩阵之外,还要覆盖框架特有的部分:
- 框架原生访问器返回日志记录器(
c.get('log')、req.log等) - 错误处理。捕获错误、事件包含错误级别和详情、重新抛出错误
- 路由过滤。跳过的路由不会创建日志记录器,也不会执行 drain/enrich
- 上下文累积。
logger.set()中的数据会出现在发出的事件中 - Drain/enrich/keep 回调(使用
test/helpers/framework.ts中的createPipelineSpies()、assertHttpEventEmitted、waitForDrainCalls、findEventViaDrain) - Drain/enrich 错误恢复能力。这些环节中的错误绝不会中断请求
useLogger():与原生访问器返回同一个日志记录器,可跨异步边界使用,并且在上下文之外调用时会抛出错误。对于不带 ALS 的集成,跳过此项,并测试它提供的访问器:在 Workers 上,该访问器是处理程序的第四个参数,由defineWorkerFetch/withEvlog提供- 流式响应(如适用)。事件延迟到响应体关闭后再发出
任何涉及时间的测试都使用 fake timers;使用 defined() 代替 !。
第 5 步:框架文档页面
创建 apps/docs/content/4.integrate/frameworks/{NN}.{framework}.md,编写一份全面且独立完整的指南。检查现有文件,确定下一个补零编号。
Frontmatter:
---
title: {Framework}
description: Using evlog with {Framework}: automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in {Framework} applications.
navigation:
title: {Framework}
icon: i-simple-icons-{framework}
links:
- label: Source Code
icon: i-simple-icons-github
to: https://github.com/evloghq/evlog/tree/main/examples/{framework}
color: neutral
variant: subtle
---
章节(参照 Express/Hono/Elysia 页面):
- 快速开始:安装 + 注册中间件(可复制粘贴的最简设置)
- 宽事件:逐步使用
log.set() - useLogger():无需传入请求即可从服务中访问日志记录器;对于不支持 ALS 的集成,则介绍其提供的替代访问器
- 错误处理:
createError()+parseError()+ 框架错误处理器 - Drain 和 Enricher:带内联示例的中间件选项
- Pipeline(批处理与重试):
createDrainPipeline示例 - 尾部采样:
keep回调 - 路由筛选:
include/exclude/routes - 客户端日志记录:HTTP drain(
evlog/http)(仅当该框架支持客户端日志记录时) - 本地运行:克隆 +
pnpm example {framework} - 卡片组:链接到 GitHub 源码
第 6 步:概览和安装卡片
在 apps/docs/content/4.integrate/frameworks/00.overview.md 中:
- 在概览表中新增一行:框架名称、导入、类型、日志记录器访问方式、状态
- 在相应章节中添加卡片;如果引导设置或访问模式是新的,也在 API 速查表中新增一行
在 apps/docs/content/1.start/3.installation.md 中:在 “Choose Your Framework” ::card-group 中添加一张卡片,顺序与框架概览一致。
第 7 步:着陆页
在 apps/docs/content/0.landing.md 中,找到 FeatureFrameworks MDC 区块(插槽 #nuxt、#nextjs、#hono 等),并添加一个新插槽:
#{framework}
```ts [src/index.ts]
// Framework-specific code example showing evlog usage
## Step 8: FeatureFrameworks Component
Update `apps/docs/app/components/features/FeatureFrameworks.vue`:
1. Add the framework to the `frameworks` array with its icon and the next available `tab` index
2. Add a `<div v-if="activeTab === {N}">` with `<slot name="{framework}" />` in the template
Icons use Simple Icons format: `i-simple-icons-{name}`.
## Step 9: Update the Public Skill
In `skills/review-logging-patterns/SKILL.md` (published on evlog.dev):
1. Add `### {Framework}` in the **"Framework Setup"** section, in the same order as the docs
2. Include: import + `initLogger` + middleware setup; native logger access; a `useLogger()` snippet, or the accessor that replaces it when the integration has no ALS; full pipeline example (`drain`, `enrich`, `keep`)
3. Update the `description:` line in the YAML frontmatter to mention the new framework name
## Step 10: Update README
In `packages/evlog/README.md` (root `README.md` is a symlink):
1. Add a `## {Framework}` section near the other framework sections with a minimal setup snippet and a link to the example app
2. Add a row to the **Framework Support** table
Keep the snippet short: init, register middleware, one route handler showing logger access.
## Step 11: Example App
Create `examples/{framework}/` with a runnable app demonstrating all evlog features. It is auto-discovered by the root runner: `pnpm example {framework}` (which loads the root `.env` via dotenv, so no root `package.json` change is needed).
The app must include:
1. **`evlog()` middleware** with `drain` (PostHog) and `enrich` callbacks
2. **Health route**: basic `log.set()` usage
3. **Data route**: context accumulation with user/business data, using `useLogger()` in a service function, or the integration's own accessor when it has no ALS
4. **Error route**: `createError()` with status/why/fix/link
5. **Error handler**: framework's error handler with `parseError()` + manual `log.error()`
6. **Test UI**: served at `/`, a self-contained HTML page with buttons to hit each route and display JSON responses
**Drain must use PostHog** (`createPostHogDrain()` from `evlog/posthog`). `POSTHOG_API_KEY` is set in the root `.env` (maintainer's key, not committed), so every example exercises a real external drain. Without the env var the drain resolves to `null` and skips, so someone cloning the repo sends nothing anywhere unless they opt in with their own key. Enable pretty printing for readable local output.
**Type the `enrich` callback parameter explicitly**, as `(ctx: EnrichContext) => ...` with `type EnrichContext` imported from `evlog`.
### Test UI
Reference: `examples/hono/src/ui.ts`, a single `src/ui.ts` exporting `testUI()` returning a self-contained dark-theme HTML string (route list with method badges, click-to-fetch, JSON display, status colors, response time). Register the `/` route **before** the evlog middleware so it isn't logged.
### Required files
| File | Purpose |
|------|---------|
| `src/index.ts` | App with all features demonstrated |
| `src/ui.ts` | Test UI |
| `package.json` | `dev` and `start` scripts (`bun --watch src/index.ts` / `bun src/index.ts`) |
| `tsconfig.json` | TypeScript config (if needed) |
| `README.md` | How to run + link to the UI |
There is also `examples/community-framework-skeleton/` showing the community-facing (toolkit-only) variant, so keep it in mind if the new integration changes the toolkit contract.
## Step 12: Changeset
Create `.changeset/{framework}-integration.md`:
```markdown
---
"evlog": minor
---
feat({framework}): add {Framework} middleware integration (`evlog/{framework}`) with automatic wide-event logging, drain, enrich, and tail sampling support
第 13 步:PR 作用域
按字母顺序将 {framework} 添加到 .github/workflows/semantic-pull-request.yml 的 scopes 列表中,并且添加到 .github/pull_request_template.md 的 Scopes 章节中。请谨记 PR 标题章节提到的时机注意事项:此项注册仅对基分支中已包含该注册的 PR 生效。
验证
全新安装后,先在仓库根目录运行 pnpm run dev:prepare,然后:
pnpm run dev:prepare
cd packages/evlog
pnpm run build # required before test — api-surface snapshot is gated on dist/
pnpm run test
pnpm run lint
接着对示例进行类型检查:
cd examples/{framework}
pnpm exec tsc --noEmit