@hugorcd/create-framework-integration
Logging that makes sense. Wide events, structured errors, zero chaos.
| 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. |
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 callsintegration.start(ctx, options)and runs the framework'snext()insiderunWith. Reference implementations: all ofpackages/evlog/src/{hono,express,fastify,elysia,nestjs,orpc,react-router,sveltekit,workers}/index.tsuse it. - Custom mode: use
createMiddlewareLoggerdirectly 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 fullBaseEvlogOptions(drain,enrich,keep,include,exclude,routes,plugins)useLogger()(ALS-backed). Workers is the one sanctioned exception (ALS needs a compat flag there;defineWorkerFetchattaches the logger instead)log.fork()support (automatic whenstorageis 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 | 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).
| 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
Headersand NodeIncomingHttpHeaders(so you don't need to pick a header extractor) - generates a
requestIdwhen none is present - calls
createMiddlewareLoggerand surfaces its{ logger, finish, skipped, middlewareOptions } - attaches
log.fork()automatically whenstorageis provided - exposes
runWith(fn)to run downstream handlers inside the integration's ALS - forwards
waitUntilwhen 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 }
// 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)+ ALSuseLogger(), streaming deferral viashouldDeferEmitForResponse,waitUntildetection - 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-pluginwrapper - 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 ALSuseLogger()(compat-flag constraint)
Key Architecture Rules
- Prefer
defineFrameworkIntegration: it handles header normalization, request-id generation, ALS, fork attachment, andwaitUntil. - Status / error reporting stays framework-side: call
finish({ status })on success andfinish({ error })on failure.finishruns 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:
runEnrichAndDrainis internal tocreateMiddlewareLogger/finish. - Export type helpers for typed context access (e.g.,
EvlogVariablesfor 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 })fromtest/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,findEventViaDrainfromtest/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, fromdefineWorkerFetch/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):
createDrainPipelineexample - Tail Sampling:
keepcallback - 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 `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:
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:
cd examples/{framework}
pnpm exec tsc --noEmit
Loading...
Select a file to preview
Analyzing security...
Checking scan reports and verification data.
Bill of Materials
Everything this skill can do — files, network, commands, and more.