Create evlog Framework Integration
Add a new framework integration to evlog. Every integration follows the same architecture built on the shared createMiddlewareLogger utility. This skill walks through all touchpoints. Every single touchpoint is mandatory -- do not skip any.
PR Title
Recommended format for the pull request title:
feat({framework}): add {Framework} middleware integration
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
- peer dep + keyword
4 packages/evlog/test/{framework}.test.ts
Create tests
5 apps/docs/content/2.frameworks/{NN}.{framework}.md
Create framework docs page
6 apps/docs/content/2.frameworks/00.overview.md
Add card + table row
7 apps/docs/content/1.getting-started/2.installation.md
Add card in "Choose Your Framework"
8 apps/docs/content/0.landing.md
Add framework code snippet
9 apps/docs/app/components/features/FeatureFrameworks.vue
Add framework tab
10 skills/review-logging-patterns/SKILL.md
Add framework setup section + update frontmatter description
11 packages/evlog/README.md
Add framework section + add row to Framework Support table
12 examples/{framework}/
Create example app with test UI
13 package.json (root) Add example:{framework} script
14 .changeset/{framework}-integration.md
Create changeset (minor )
15 .github/workflows/semantic-pull-request.yml
Add {framework} scope
16 .github/pull_request_template.md
Add {framework} scope
Important: Do NOT consider the task complete until all 16 touchpoints have been addressed.
Naming Conventions
Use these placeholders consistently:
Placeholder Example (Hono) Usage
{framework}
hono
Directory names, import paths, file names
{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
createMiddlewareLogger
../shared/middleware
Full lifecycle: logger creation, route filtering, tail sampling, emit, enrich, drain
extractSafeHeaders
../shared/headers
Convert Web API Headers → filtered Record<string, string> (Hono, Elysia, etc.)
extractSafeNodeHeaders
../shared/headers
Convert Node.js IncomingHttpHeaders → filtered Record<string, string> (Express, Fastify, NestJS)
BaseEvlogOptions
../shared/middleware
Base user-facing options type with drain , enrich , keep , include , exclude , routes
MiddlewareLoggerOptions
../shared/middleware
Internal options type extending BaseEvlogOptions with method , path , requestId , headers
createLoggerStorage
../shared/storage
Factory returning { storage, useLogger } for AsyncLocalStorage -backed useLogger()
Test Helpers
Utility Location Purpose
createPipelineSpies()
test/helpers/framework
Creates mock drain/enrich/keep callbacks
assertDrainCalledWith()
test/helpers/framework
Validates drain was called with expected event shape
assertEnrichBeforeDrain()
test/helpers/framework
Validates enrich runs before drain
assertSensitiveHeadersFiltered()
test/helpers/framework
Validates sensitive headers are excluded
assertWideEventShape()
test/helpers/framework
Validates standard wide event fields
Step 1: Integration Source
Create packages/evlog/src/{framework}/index.ts .
The integration file should be minimal — typically 50-80 lines of framework-specific glue. All pipeline logic (enrich, drain, keep, header filtering) is handled by createMiddlewareLogger .
Template Structure
import type { RequestLogger } from '../types' import { createMiddlewareLogger, type BaseEvlogOptions } from '../shared/middleware' import { extractSafeHeaders } from '../shared/headers' // for Web API Headers (Hono, Elysia) // OR import { extractSafeNodeHeaders } from '../shared/headers' // for Node.js headers (Express, Fastify) import { createLoggerStorage } from '../shared/storage'
const { storage, useLogger } = createLoggerStorage( 'middleware context. Make sure the evlog middleware is registered before your routes.', )
export interface Evlog{Framework}Options extends BaseEvlogOptions {}
export { useLogger }
// Type augmentation for typed logger access (framework-specific) // For Express: declare module 'express-serve-static-core' { interface Request { log: RequestLogger } } // For Hono: export type EvlogVariables = { Variables: { log: RequestLogger } }
export function evlog(options: Evlog{Framework}Options = {}): FrameworkMiddleware { return async (frameworkContext, next) => { const { logger, finish, skipped } = createMiddlewareLogger({ method: /* extract from framework context /, path: / extract from framework context /, requestId: / extract x-request-id or crypto.randomUUID() /, headers: extractSafeHeaders(/ framework request Headers object */), ...options, })
if (skipped) {
await next()
return
}
// Store logger in framework-specific context
// e.g., c.set('log', logger) for Hono
// e.g., req.log = logger for Express
// Wrap next() in AsyncLocalStorage.run() for useLogger() support
// Express: storage.run(logger, () => next())
// Hono: await storage.run(logger, () => next())
} }
Reference Implementations
-
Hono (~40 lines): packages/evlog/src/hono/index.ts — Web API Headers, c.set('log', logger) , wraps next() in try/catch
-
Express (~80 lines): packages/evlog/src/express/index.ts — Node.js headers, req.log , res.on('finish') , AsyncLocalStorage for useLogger()
-
Elysia (~70 lines): packages/evlog/src/elysia/index.ts — Web API Headers, derive() plugin, onAfterHandle /onError , AsyncLocalStorage for useLogger()
Key Architecture Rules
-
Use createMiddlewareLogger — never call createRequestLogger directly
-
Use the right header extractor — extractSafeHeaders for Web API Headers , extractSafeNodeHeaders for Node.js IncomingHttpHeaders
-
Spread user options into createMiddlewareLogger — drain , enrich , keep are handled automatically by finish()
-
Store logger in the framework's idiomatic context (e.g., c.set() for Hono, req.log for Express, .derive() for Elysia)
-
Export useLogger() — backed by AsyncLocalStorage so the logger is accessible from anywhere in the call stack
-
Call finish() in both success and error paths — it handles emit + enrich + drain
-
Re-throw errors after finish() so framework error handlers still work
-
Export options interface with drain/enrich/keep for feature parity across all frameworks
-
Export type helpers for typed context access (e.g., EvlogVariables for Hono)
-
Framework SDK is a peer dependency — never bundle it
-
Never duplicate pipeline logic — callEnrichAndDrain is internal to createMiddlewareLogger
Framework-Specific Patterns
Hono: Use MiddlewareHandler return type, c.set('log', logger) , c.res.status for status, c.req.raw.headers for headers.
Express: Standard (req, res, next) middleware, res.on('finish') for response end, storage.run(logger, () => next()) for useLogger() . Type augmentation targets express-serve-static-core (NOT express ). Error handler uses ErrorRequestHandler type.
Elysia: Return new Elysia({ name: 'evlog' }) plugin, use .derive({ as: 'global' }) to create logger and attach log to context, onAfterHandle for success path, onError for error path. Use storage.enterWith(logger) in derive for useLogger() support. Note: onAfterResponse is fire-and-forget and may not complete before app.handle() returns in tests — use onAfterHandle instead.
Fastify: Use fastify-plugin wrapper, fastify.decorateRequest('log', null) , onRequest /onResponse hooks.
NestJS: NestInterceptor with intercept() , tap() /catchError() on observable, forRoot() dynamic module.
Step 2: Build Config
Add a build entry in packages/evlog/tsdown.config.ts :
'{framework}/index': 'src/{framework}/index.ts',
Place it after the existing framework entries (workers, next, hono, express).
Also add the framework SDK to the external array:
external: [ // ... existing externals '{framework-package}', // e.g., 'elysia', 'fastify', 'express' ],
Step 3: Package Exports
In packages/evlog/package.json , add four entries:
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 (with version range):
"{framework-package}": "^{latest-major}.0.0"
In peerDependenciesMeta (mark as optional):
"{framework-package}": { "optional": true }
In keywords — add the framework name to the keywords array.
Step 4: Tests
Create packages/evlog/test/{framework}.test.ts .
Import shared test helpers from ./helpers/framework :
import { assertDrainCalledWith, assertEnrichBeforeDrain, assertSensitiveHeadersFiltered, createPipelineSpies, } from './helpers/framework'
Required test categories:
-
Middleware creates logger — verify c.get('log') or req.log returns a RequestLogger
-
Auto-emit on response — verify event includes status, method, path, duration
-
Error handling — verify errors are captured and event has error level + error details
-
Route filtering — verify skipped routes don't create a logger
-
Request ID forwarding — verify x-request-id header is used when present
-
Context accumulation — verify logger.set() data appears in emitted event
-
Drain callback — use assertDrainCalledWith() helper
-
Enrich callback — use assertEnrichBeforeDrain() helper
-
Keep callback — verify tail sampling callback receives context and can force-keep logs
-
Sensitive header filtering — use assertSensitiveHeadersFiltered() helper
-
Drain/enrich error resilience — verify errors in drain/enrich do not break the request
-
Skipped routes skip drain/enrich — verify drain/enrich are not called for excluded routes
-
useLogger() returns same logger — verify useLogger() === req.log (or framework equivalent)
-
useLogger() throws outside context — verify error thrown when called without middleware
-
useLogger() works across async — verify logger accessible in async service functions
Use the framework's test utilities when available (e.g., Hono's app.request() , Express's supertest , Fastify's inject() ).
Step 5: Framework Docs Page
Create apps/docs/content/2.frameworks/{NN}.{framework}.md with a comprehensive, self-contained guide.
Use zero-padded numbering ({NN} ) to maintain correct sidebar ordering. Check existing files to determine the next 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 logger from services without passing req
-
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 — browser drain (only if framework has a client-side story)
-
Run Locally — clone + bun run example:{framework}
-
Card group linking to GitHub source
Step 6: Overview & Installation Cards
In apps/docs/content/2.frameworks/00.overview.md :
-
Add a row to the Overview table with framework name, import, type, logger access, and status
-
Add a :::card in the appropriate section (Full-Stack or Server Frameworks) with color: neutral
In apps/docs/content/1.getting-started/2.installation.md :
-
Add a :::card in the "Choose Your Framework" ::card-group with color: neutral
-
Place it in the correct order relative to existing frameworks (Nuxt, Next.js, SvelteKit, Nitro, TanStack Start, NestJS, Express, Hono, Fastify, Elysia, CF Workers)
Step 7: Landing Page (unchanged)
Add a code snippet in apps/docs/content/0.landing.md for the framework.
Find the FeatureFrameworks MDC component usage (the section with #nuxt , #nextjs , #hono , #express , etc.) and add a new slot:
#{framework}
// Framework-specific code example showing evlog usage
Place the snippet in the correct order relative to existing frameworks.
## 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-show="activeTab === {N}">` with `<slot name="{framework}" />` in the template
3. **Increment tab indices** for any frameworks that come after the new one
Icons use Simple Icons format: `i-simple-icons-{name}` (e.g., `i-simple-icons-express`, `i-simple-icons-hono`).
## Step 9: Update `skills/review-logging-patterns/SKILL.md`
In `skills/review-logging-patterns/SKILL.md` (the public skill distributed to users):
1. Add `### {Framework}` in the **"Framework Setup"** section, after the last existing framework entry and before "Cloudflare Workers"
2. Include:
- Import + `initLogger` + middleware/plugin setup
- Logger access in route handlers (`req.log`, `c.get('log')`, or `{ log }` destructuring)
- `useLogger()` snippet with a short service function example
- Full pipeline example showing `drain`, `enrich`, and `keep` options
3. Update the `description:` line in the YAML frontmatter to mention the new framework name
## Step 10: Update `packages/evlog/README.md`
In the root `packages/evlog/README.md`:
1. Add a `## {Framework}` section after the Elysia section (before `## Browser`), with a minimal setup snippet and a link to the example app
2. Add a row to the **"Framework Support"** table:
```markdown
| **{Framework}** | `{registration pattern}` with `import { evlog } from 'evlog/{framework}'` ([example](./examples/{framework})) |
Keep the snippet short — just init, register/use middleware, and one route handler showing logger access. No need to repeat drain/enrich/keep here.
Step 11: Example App
Create examples/{framework}/
with a runnable app that demonstrates all evlog features.
The app must include:
- evlog()
middleware with drain
(PostHog) and enrich
callbacks
- Health route — basic log.set()
usage
- Data route — context accumulation with user/business data, using useLogger()
in a service function
- Error route — createError()
with status/why/fix/link
- Error handler — framework's error handler with parseError()
+ manual log.error()
- 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
). The POSTHOG_API_KEY
env var is already set in the root .env
. This ensures every example tests a real external drain adapter.
Pretty printing should be enabled so the output is readable when testing locally.
Type the enrich
callback parameter explicitly — use type EnrichContext
from evlog
to avoid implicit any
:
import { type EnrichContext } from 'evlog'
app.use(evlog({
enrich: (ctx: EnrichContext) => {
ctx.event.runtime = 'node'
},
}))
Test UI
Every example must serve a test UI at GET /
— a self-contained HTML page (no external deps) that lets the user click routes and see responses without curl.
The UI must:
- List all available routes with method badge + path + description
- Send the request on click and display the JSON response with syntax highlighting
- Show status code (color-coded 2xx/4xx/5xx) and response time
- Use a dark theme with monospace font
- Be a single .ts
file (src/ui.ts
) exporting a testUI()
function returning an HTML string
- The root /
route must be registered before the evlog middleware so it doesn't get logged
Reference: examples/hono/src/ui.ts
for the canonical pattern. Copy and adapt for each framework.
Required files
File
Purpose
src/index.ts
App with all features demonstrated
src/ui.ts
Test UI — testUI()
returning self-contained HTML
package.json
dev
and start
scripts
tsconfig.json
TypeScript config (if needed)
README.md
How to run + link to the UI
Package scripts
{
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts"
}
}
Step 12: Root Package Script
Add a root-level script in the monorepo package.json
:
"example:{framework}": "dotenv -- turbo run dev --filter=evlog-{framework}-example"
The dotenv --
prefix loads the root .env
file (containing POSTHOG_API_KEY
and other adapter keys) into the process before turbo starts. Turborepo does not load .env
files — dotenv-cli
handles this at the root level so individual examples need no env configuration.
Step 13: Changeset
Create .changeset/{framework}-integration.md
:
---
"evlog": minor
---
Add {Framework} middleware integration (`evlog/{framework}`) with automatic wide-event logging, drain, enrich, and tail sampling support
Step 15 & 16: PR Scopes
Add the framework name as a valid scope in both files so PR title validation passes:
.github/workflows/semantic-pull-request.yml
— add {framework}
to the scopes
list:
scopes: |
# ... existing scopes
{framework}
.github/pull_request_template.md
— add {framework}
to the Scopes section:
- {framework} ({Framework} integration)
Verification
After completing all steps, run from the repo root:
cd packages/evlog
bun run build # Verify build succeeds with new entry
bun run test # Verify unit tests pass
bun run lint # Verify no lint errors
Then type-check the example:
cd examples/{framework}
npx tsc --noEmit # Verify no TS errors in the example