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'
import { extractSafeNodeHeaders } from '../shared/headers'
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 }
export function evlog(options: Evlog{Framework}Options = {}): FrameworkMiddleware {
return async (frameworkContext, next) => {
const { logger, finish, skipped } = createMiddlewareLogger({
method: ,
path: ,
requestId: ,
headers: extractSafeHeaders(),
...options,
})
if (skipped) {
await next()
return
}
}
}
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: [
'{framework-package}',
],
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 c