| 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. |
| metadata | {"internal":true} |
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:
evlog() middleware/plugin accepting the full BaseEvlogOptions (drain, enrich, keep, include, exclude, routes, plugins)
useLogger() (ALS-backed). Workers is the one sanctioned exception (ALS needs a compat flag there; defineWorkerFetch attaches the logger instead)
log.fork() support (automatic when storage is provided to the manifest)
- 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 | 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 + |
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).
| 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)
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 }
const integration = defineFrameworkIntegration<{Framework}Context>({
name: '{framework}',
extractRequest: (ctx) => ({
method: ,
path: ,
headers: ,
requestId: ,
}),
: {
},
storage,
})
(): {
(ctx, next) => {
{ skipped, finish, runWith } = integration.(ctx, options)
(skipped) {
()
}
{
( ())
({ : })
} (error) {
({ : error })
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
- Prefer
defineFrameworkIntegration: it handles header normalization, request-id generation, ALS, fork attachment, and waitUntil.
- Status / error reporting stays framework-side: call
finish({ status }) on success and finish({ error }) on failure. finish runs emit + enrich + drain + plugin hooks.
- Re-throw errors after
finish({ error }) so the framework's own error handler still runs.
- Streaming responses: if the framework can return streaming bodies, defer the emit until the stream closes (
shouldDeferEmitForResponse; see Hono and Elysia).
- Framework SDK is an optional peer dependency: never bundle it.
- Never duplicate pipeline logic:
runEnrichAndDrain is internal to createMiddlewareLogger/finish.
- 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:
'{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):
"./{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:
- 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.
- 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:
- Framework-native accessor returns the logger (
c.get('log'), req.log, …)
- Error handling. Errors captured, event has error level + details, error re-thrown
- Route filtering. Skipped routes don't create a logger, skip drain/enrich
- Context accumulation.
logger.set() data appears in the emitted event
- Drain / enrich / keep callbacks (use
createPipelineSpies(), assertHttpEventEmitted, waitForDrainCalls, findEventViaDrain from test/helpers/framework.ts)
- Drain/enrich error resilience. Errors there never break the request
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
- 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:
---
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):
- Quick Start: install + register middleware (copy-paste minimum setup)
- Wide Events: progressive
log.set() usage
- useLogger(): accessing the logger from services without passing the request, or, for an integration without ALS, the accessor it ships in its place
- Error Handling:
createError() + parseError() + framework error handler
- Drain & Enrichers: middleware options with inline example
- Pipeline (Batching & Retry):
createDrainPipeline example
- Tail Sampling:
keep callback
- Route Filtering:
include / exclude / routes
- Client-Side Logging: HTTP drain (
evlog/http) (only if the framework has a client-side story)
- Run Locally: clone +
pnpm example {framework}
- Card group linking to GitHub source
Step 6: Overview & Installation Cards
In apps/docs/content/4.integrate/frameworks/00.overview.md:
- Add a row to the Overview table: framework name, import, type, logger access, status
- 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:
#{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
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:
pnpm run dev:prepare
cd packages/evlog
pnpm run build
pnpm run test
pnpm run lint
Then type-check the example:
cd examples/{framework}
pnpm exec tsc --noEmit