create-evlog-framework-integration について
evlogにフレームワーク連携を追加する貢献者向けのリポジトリSkillです。共有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 を使います。
2 つの方法
- マニフェストモード(推奨、接続コードは約 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 フレームワークすべてに対応しています。ライフサイクルの開始時にリクエストを同期的に取得できない場合(サーバーアクション、モジュールレベルのフック、エージェントのターン)に限り、カスタムモードを使用してください。
必須 API サーフェス(AGENTS.md より)
すべてのフレームワーク統合で、次のものを公開する必要があります。
evlog()ミドルウェア/プラグイン。完全なBaseEvlogOptions(drain、enrich、keep、include、exclude、routes、plugins)を受け付けますuseLogger()(ALS ベース)。例外として認められているのは 2 つです。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 | 「Choose Your Framework」にカードを追加 |
| 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 | フレームワークのセクションと Framework Support 表の行を追加 |
| 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 | 低レベルなライフサイクル(カスタムモード): ロガーの作成、ルートのフィルタリング、テールサンプリング、emit、enrich、drain |
BaseEvlogOptions | ../shared/middleware | drain、enrich、keep、include、exclude、routes、plugins を備えた、ユーザー向けの基本オプション型 |
createLoggerStorage | ../shared/storage(evlog/toolkit/storage) | AsyncLocalStorage に支えられた useLogger() 用に { storage, useLogger } を返すファクトリー。Workers / エッジでは evlog/toolkit/storage を推奨 |
shouldDeferEmitForResponse | ../shared/streamResponse | ストリーミングボディが閉じるまでワイドイベントを遅延させる(Hono/Elysia を参照) |
defineFrameworkIntegration は自動的に以下を行います。
- Web の
Headersと Node のIncomingHttpHeadersの両方を正規化します(そのためヘッダー抽出器を選ぶ必要はありません) - 存在しない場合は
requestIdを生成します createMiddlewareLoggerを呼び出し、その{ logger, finish, skipped, middlewareOptions }を返しますstorageが指定された場合、log.fork()を自動的にアタッチします- 統合の ALS 内で下流のハンドラーを実行するための
runWith(fn)を公開します - ランタイムが提供する場合に
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)+ ALS のuseLogger()、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ではミドルウェアが生のリクエストしか参照できないため、preHandlerフック(HttpAdapterHost経由で登録)がロガーを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()ハンドル +evlogHandleError()+createEvlogHooks() - Workers:
src/workers/index.ts。defineWorkerFetch/withEvlog、ALS のuseLogger()なし(compat-flag の制約)
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 はオプションのピア依存関係: 決してバンドルしないでください。
- パイプラインロジックを重複させない:
runEnrichAndDrainはcreateMiddlewareLogger/finishの内部専用です。 - 型付きコンテキストアクセス用の型ヘルパーをエクスポートする(例: Hono 用の
EvlogVariables)。
カスタムモードにフォールバックすべき場合
次の場合は createMiddlewareLogger を直接使用します(defineFrameworkIntegration をスキップします):
- ミドルウェアに明確な「リクエスト入口 / レスポンス出口」のペアがない場合(Next.js App Router のサーバーアクション、Eve エージェントのターン)。
- ロガーの生成が、モジュールシステムが所有する複数のライフサイクルフェーズにまたがる場合(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 に追加します。
test/toolkit/entrypoint-registry.test.ts は、エクスポートに対応する typesVersions["*"] または tsdown.config.ts のエントリがない場合、あるいはその import と default が types の隣にある .mjs を指していない場合に失敗します。これはビルドなしで実行されます。
ステップ 4: テスト
packages/evlog/test/frameworks/{framework}.test.ts を作成します。まず packages/evlog/test/README.md を読んでください。特に Framework runtime fidelity の表を確認してください。
2 つの絶対条件:
- 実際のリクエストドライバー。 フレームワーク自身のドライバーを使用します: supertest(Express/NestJS)、
app.request()(Hono)、app.inject()(Fastify)、app.handle(new Request(...))(Elysia)。Node に適したドライバーが存在しない場合は、現実的な入力形状でユーザー向けのコントラクトを直接呼び出します(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 ではそれはハンドラーの第 4 引数で、defineWorkerFetch/withEvlogから渡されます- ストリーミング(該当する場合)。ボディが閉じるまでイベントが遅延される
時間ベースのものにはフェイクタイマーを使用し、! の代わりに defined() を使用します。
ステップ 5: フレームワークドキュメントページ
包括的で自己完結したガイドを apps/docs/content/4.integrate/frameworks/{NN}.{framework}.md として作成します。既存のファイルを確認して、次に使用するゼロ埋めされた番号を調べてください。
フロントマター:
---
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 とエンリッチャー: インライン例を伴うミドルウェアオプション
- パイプライン (バッチ処理とリトライ):
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 内で: 「フレームワークを選択」の ::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 スコープ
.github/workflows/semantic-pull-request.yml の scopes リストに、および .github/pull_request_template.md の Scopes セクションに、アルファベット順で {framework} を追加します。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