create-evlog-framework-integration
Create a new evlog framework integration to add automatic wide-event logging to an HTTP framework. Use when adding middleware/plugin support for a framework (e.g., Koa, H3 standalone, Deno Fresh, etc.) to the evlog package. Covers source code, build config, package exports, tests, example app, and all documentation.
Works with
---
name: create-evlog-framework-integration
description: Create a new evlog framework integration to add automatic wide-event logging to an HTTP framework. Use when adding middleware/plugin support for a framework (e.g., Koa, H3 standalone, Deno Fresh, etc.) to the evlog package. Covers source code, build config, package exports, tests, example app, and all documentation.
license: MIT
---
# Create evlog Framework Integration
Add a new framework integration to evlog. The recommended path is the **manifest mode** built on `defineFrameworkIntegration` from `evlog/toolkit`, for any framework with a request/response middleware shape. For frameworks with a fundamentally different lifecycle you'll fall back to the lower-level `createMiddlewareLogger`.
## Two paths
- **Manifest mode** (preferred, ~30–80 lines of glue). Call `defineFrameworkIntegration({ name, extractRequest, attachLogger, storage? })` once at module level, then write a tiny middleware that calls `integration.start(ctx, options)` and runs the framework's `next()` inside `runWith`. Reference implementations: all of `packages/evlog/src/{hono,express,fastify,elysia,nestjs,orpc,react-router,sveltekit,workers}/index.ts` use it.
- **Custom mode**: use `createMiddlewareLogger` directly when the framework's lifecycle doesn't fit a standard middleware. Current custom-mode integrations: Next.js (`src/next/`), Nitro v2/v3 (`src/nitro/`, `src/nitro-v3/`), Eve (`src/eve/`).
Manifest mode now covers all classic HTTP frameworks. Use custom mode only when you can't extract a request synchronously at the start of the lifecycle (server actions, module-level hooks, agent turns).
## Required API surface (from AGENTS.md)
Every framework integration must expose:
1. `evlog()` middleware/plugin accepting the full `BaseEvlogOptions` (`drain`, `enrich`, `keep`, `include`, `exclude`, `routes`, `plugins`)
2. `useLogger()` (ALS-backed). Workers is the one sanctioned exception (ALS needs a compat flag there; `defineWorkerFetch` attaches the logger instead)
3. `log.fork()` support (automatic when `storage` is provided to the manifest)
4. The framework-native accessor (`c.get('log')`, `req.log`, `event.locals.log`, …)
## PR Title
```
feat({framework}): add {Framework} middleware integration
```
**Scope timing caveat**: the semantic PR check reads its scope list from the **base branch**, so a brand-new scope can't validate the very PR that introduces it. Either register the scope in a small preceding PR, or use an unscoped title (`feat: add {Framework} middleware integration`) on the introducing PR.
## Touchpoints Checklist
| # | File | Action |
|---|------|--------|
| 1 | `packages/evlog/src/{framework}/index.ts` | Create integration source |
| 2 | `packages/evlog/tsdown.config.ts` | Add build entry + external |
| 3 | `packages/evlog/package.json` | Add `exports` + `typesVersions` + optional peer dep + keyword |
| 4 | `packages/evlog/test/frameworks/{framework}.test.ts` | Create tests (real request driver + `describeStandardHttpMatrix`) |
| 5 | `packages/evlog/test/toolkit/__snapshots__/api-surface.test.ts.snap` | Regenerated by `pnpm run build` + `pnpm test` |
| 6 | `apps/docs/content/4.integrate/frameworks/{NN}.{framework}.md` | Create framework docs page |
| 7 | `apps/docs/content/4.integrate/frameworks/00.overview.md` | Add table row + card |
| 8 | `apps/docs/content/1.start/3.installation.md` | Add card in "Choose Your Framework" |
| 9 | `apps/docs/content/0.landing.md` | Add framework code snippet slot |
| 10 | `apps/docs/app/components/features/FeatureFrameworks.vue` | Add framework tab |
| 11 | `apps/docs/skills/review-logging-patterns/SKILL.md` | Add framework setup section + update frontmatter description |
| 12 | `packages/evlog/README.md` | Add framework section + row in the Framework Support table |
| 13 | `examples/{framework}/` | Create example app with test UI (auto-discovered by `pnpm example {framework}` — no root script needed) |
| 14 | `.changeset/{framework}-integration.md` | Create changeset (`minor`) |
| 15 | `.github/workflows/semantic-pull-request.yml` + `.github/pull_request_template.md` | Register `{framework}` as a PR scope in both files |
**Important**: Do NOT consider the task complete until all 15 touchpoints have been addressed.
## Naming Conventions
| Placeholder | Example (Hono) | Usage |
|-------------|----------------|-------|
| `{framework}` | `hono` | Directory names, import paths, file names, PR scope |
| `{Framework}` | `Hono` | PascalCase in type/interface names |
## Shared Utilities
All integrations share the same core utilities. **Never reimplement logic that exists in shared/**. These are also publicly available as `evlog/toolkit` for community-built integrations (see [Custom Integration docs](https://evlog.dev/extend/custom-framework)).
| Utility | Location | Purpose |
|---------|----------|---------|
| `defineFrameworkIntegration` | `../shared/integration` | Manifest factory — extract request, create logger, attach, run with ALS |
| `createMiddlewareLogger` | `../shared/middleware` | Lower-level lifecycle (custom mode): logger creation, route filtering, tail sampling, emit, enrich, drain |
| `BaseEvlogOptions` | `../shared/middleware` | Base user-facing options type with `drain`, `enrich`, `keep`, `include`, `exclude`, `routes`, `plugins` |
| `createLoggerStorage` | `../shared/storage` (`evlog/toolkit/storage`) | Factory returning `{ storage, useLogger }` for `AsyncLocalStorage`-backed `useLogger()`. Prefer `evlog/toolkit/storage` on Workers / edge |
| `shouldDeferEmitForResponse` | `../shared/streamResponse` | Defer the wide event until a streaming body closes (see Hono/Elysia) |
`defineFrameworkIntegration` automatically:
- normalizes both Web `Headers` and Node `IncomingHttpHeaders` (so you don't need to pick a header extractor)
- generates a `requestId` when none is present
- calls `createMiddlewareLogger` and surfaces its `{ logger, finish, skipped, middlewareOptions }`
- attaches `log.fork()` automatically when `storage` is provided
- exposes `runWith(fn)` to run downstream handlers inside the integration's ALS
- forwards `waitUntil` when the runtime provides one (Workers, Hono on Workers)
## Step 1: Integration Source
Create `packages/evlog/src/{framework}/index.ts`.
### Template Structure (manifest mode)
```typescript
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
}
}
}
```
### Reference Implementations
- **Hono**: `src/hono/index.ts`. `c.set('log', logger)` + ALS `useLogger()`, streaming deferral via `shouldDeferEmitForResponse`, `waitUntil` detection
- **Express**: `src/express/index.ts`. `req.log`, ALS storage, `res.on('finish')` for terminal status
- **Fastify**: `src/fastify/index.ts`. Fastify hooks (`onRequest` / `onResponse` / `onError`), `fastify-plugin` wrapper
- **Elysia**: `src/elysia/index.ts`. Plugin with `.derive({ as: 'global' })`, `storage.enterWith`-style ALS, streaming deferral
- **NestJS**: `src/nestjs/index.ts`. `EvlogModule.forRoot()` / `forRootAsync()` on top of the manifest
- **oRPC**: `src/orpc/index.ts`. `evlog()` procedure middleware + `withEvlog(handler)` wrapper
- **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`, no ALS `useLogger()` (compat-flag constraint)
### Key Architecture Rules
1. **Prefer `defineFrameworkIntegration`**: it handles header normalization, request-id generation, ALS, fork attachment, and `waitUntil`.
2. **Status / error reporting stays framework-side**: call `finish({ status })` on success and `finish({ error })` on failure. `finish` runs emit + enrich + drain + plugin hooks.
3. **Re-throw errors** after `finish({ error })` so the framework's own error handler still runs.
4. **Streaming responses**: if the framework can return streaming bodies, defer the emit until the stream closes (`shouldDeferEmitForResponse`; see Hono and Elysia).
5. **Framework SDK is an optional peer dependency**: never bundle it.
6. **Never duplicate pipeline logic**: `runEnrichAndDrain` is internal to `createMiddlewareLogger`/`finish`.
7. **Export type helpers** for typed context access (e.g., `EvlogVariables` for Hono).
### When to fall back to custom mode
Use `createMiddlewareLogger` directly (skipping `defineFrameworkIntegration`) when:
- The middleware doesn't have a clear "request entry / response exit" pair (Next.js App Router server actions, Eve agent turns).
- Logger creation spans multiple lifecycle phases owned by a module system (Nitro plugins + hooks).
- The status is not knowable until after the response stream completes and the framework gives you no hook for it.
## Step 2: Build Config
Add a build entry in `packages/evlog/tsdown.config.ts`:
```typescript
'{framework}/index': 'src/{framework}/index.ts',
```
Also add the framework SDK to the `external` array (e.g., `'elysia'`, `'fastify'`).
## Step 3: Package Exports
In `packages/evlog/package.json`:
**In `exports`** (after the last framework entry):
```json
"./{framework}": {
"types": "./dist/{framework}/index.d.mts",
"import": "./dist/{framework}/index.mjs"
}
```
**In `typesVersions["*"]`**: `"{framework}": ["./dist/{framework}/index.d.mts"]`
**In `peerDependencies`** (version range) + **`peerDependenciesMeta`** (`"optional": true`), and add the framework name to `keywords`.
Exports without matching `tsdown.config.ts` entries fail `test/toolkit/api-surface.test.ts`.
## Step 4: Tests
Create `packages/evlog/test/frameworks/{framework}.test.ts`. Read `packages/evlog/test/README.md` first, especially the **Framework runtime fidelity** table.
Two non-negotiables:
1. **Real request driver.** Use the framework's own driver: supertest (Express/NestJS), `app.request()` (Hono), `app.inject()` (Fastify), `app.handle(new Request(...))` (Elysia). If no Node-friendly driver exists, call the user-facing contract directly with realistic input shapes (see the SvelteKit and React Router tests). Never extract internals to test a substitute.
2. **Wire the shared matrix.** Call `describeStandardHttpMatrix({ name, mount })` from `test/helpers/frameworkMatrix.ts`. It covers the standard sweep (event emission, `x-request-id`, route service) for every HTTP framework.
On top of the matrix, cover the framework-specific surface:
1. Framework-native accessor returns the logger (`c.get('log')`, `req.log`, …)
2. Error handling. Errors captured, event has error level + details, error re-thrown
3. Route filtering. Skipped routes don't create a logger, skip drain/enrich
4. Context accumulation. `logger.set()` data appears in the emitted event
5. Drain / enrich / keep callbacks (use `createPipelineSpies()`, `assertHttpEventEmitted`, `waitForDrainCalls`, `findEventViaDrain` from `test/helpers/framework.ts`)
6. Drain/enrich error resilience. Errors there never break the request
7. `useLogger()`: same logger as the native accessor, works across async boundaries, throws outside context. Skip it for an integration without ALS, and test the accessor it ships instead: on Workers that is the handler's fourth argument, from `defineWorkerFetch` / `withEvlog`
8. Streaming (if applicable). Event deferred until the body closes
Use fake timers for anything time-based; `defined()` instead of `!`.
## Step 5: Framework Docs Page
Create `apps/docs/content/4.integrate/frameworks/{NN}.{framework}.md` with a comprehensive, self-contained guide. Check existing files for the next zero-padded number.
**Frontmatter**:
```yaml
---
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/HugoRCD/evlog/tree/main/examples/{framework}
color: neutral
variant: subtle
---
```
**Sections** (follow the Express/Hono/Elysia pages as reference):
1. **Quick Start**: install + register middleware (copy-paste minimum setup)
2. **Wide Events**: progressive `log.set()` usage
3. **useLogger()**: accessing the logger from services without passing the request, or, for an integration without ALS, the accessor it ships in its place
4. **Error Handling**: `createError()` + `parseError()` + framework error handler
5. **Drain & Enrichers**: middleware options with inline example
6. **Pipeline (Batching & Retry)**: `createDrainPipeline` example
7. **Tail Sampling**: `keep` callback
8. **Route Filtering**: `include` / `exclude` / `routes`
9. **Client-Side Logging**: HTTP drain (`evlog/http`) (only if the framework has a client-side story)
10. **Run Locally**: clone + `pnpm example {framework}`
11. **Card group** linking to GitHub source
## Step 6: Overview & Installation Cards
**In `apps/docs/content/4.integrate/frameworks/00.overview.md`**:
1. Add a row to the **Overview table**: framework name, import, type, logger access, status
2. Add a card in the appropriate section, and a row in the **API cheat sheet** if the bootstrap/access pattern is new
**In `apps/docs/content/1.start/3.installation.md`**: add a card in the "Choose Your Framework" `::card-group`, in the same order as the frameworks overview.
## Step 7: Landing Page
In `apps/docs/content/0.landing.md`, find the `FeatureFrameworks` MDC section (slots `#nuxt`, `#nextjs`, `#hono`, …) and add a new slot:
```markdown
#{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 `apps/docs/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
```
## Step 13: PR Scopes
Add `{framework}` to the `scopes` list in `.github/workflows/semantic-pull-request.yml` **and** to the Scopes section of `.github/pull_request_template.md`, in alphabetical order. Remember the timing caveat from the PR Title section: this registration only takes effect for PRs whose base branch already contains it.
## Verification
After a clean install, run `pnpm run dev:prepare` from the repo root first, then:
```bash
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
```
Then type-check the example:
```bash
cd examples/{framework}
pnpm exec tsc --noEmit
```More Writing & Documentation skills
paper-context-resolver
lllllllama/rigorpilot-skills
Rigor Paper Context helper for README-first deep learning repo reproduction. Use only when the README and repository files leave a narrow reproduction-critical gap and the task is to resolve a specific paper detail such as dataset split, preprocessing, evaluation protocol, checkpoint mapping, or runtime assumption from primary paper sources while recording conflicts. Do not use for general paper summary, repo scanning, environment setup, command execution, title-only paper lookup, or replacing README guidance by default.
repo-intake-and-plan
lllllllama/rigorpilot-skills
Rigor Intake helper for README-first deep learning repo reproduction. Use when the task is specifically to scan a repository, read the README and common project files, extract documented commands, classify inference, evaluation, and training candidates, and return the smallest trustworthy reproduction plan to the main orchestrator. Do not use for environment setup, asset download, command execution, final reporting, paper lookup, or end-to-end orchestration.
minimal-run-and-audit
lllllllama/rigorpilot-skills
Rigor Run skill for README-first deep learning repo reproduction. Use when the task is specifically to capture or normalize evidence from the selected smoke test or documented inference or evaluation command and write standardized `repro_outputs/` files, including patch notes when repository files changed. Do not use for training execution, initial repo intake, generic environment setup, paper lookup, target selection, hidden scientific-meaning changes, or end-to-end orchestration by itself.

