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 integrationTouchpoints 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), wrapsnext()in try/catch - Express (~80 lines):
packages/evlog/src/express/index.ts— Node.js headers,req.log,res.on('finish'),AsyncLocalStorageforuseLogger() - Elysia (~70 lines):
packages/evlog/src/elysia/index.ts— Web API Headers,derive()plugin,onAfterHandle/onError,AsyncLocalStorageforuseLogger()
Key Architecture Rules
- Use
createMiddlewareLogger— never callcreateRequestLoggerdirectly - Use the right header extractor —
extractSafeHeadersfor Web APIHeaders,extractSafeNodeHeadersfor Node.jsIncomingHttpHeaders - Spread user options into
createMiddlewareLogger—drain,enrich,keepare handled automatically byfinish() - Store logger in the framework's idiomatic context (e.g.,
c.set()for Hono,req.logfor Express,.derive()for Elysia) - Export
useLogger()— backed byAsyncLocalStorageso 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.,
EvlogVariablesfor Hono) - Framework SDK is a peer dependency — never bundle it
- Never duplicate pipeline logic —
callEnrichAndDrainis internal tocreateMiddlewareLogger
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')orreq.logreturns aRequestLogger - 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-idheader 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) —
createDrainPipelineexample - Tail Sampling —
keepcallback - Route Filtering —
include/exclude/routes - Client-Side Logging — HTTP drain (
evlog/http) (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
:::cardin the appropriate section (Full-Stack or Server Frameworks) withcolor: neutral
In apps/docs/content/1.getting-started/2.installation.md:
- Add a
:::cardin the "Choose Your Framework"::card-groupwithcolor: 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:
- Add the framework to the
frameworksarray with its icon and the next available tab index - Add a
<div v-show="activeTab === {N}">with<slot name="{framework}" />in the template - 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):
- Add
### {Framework}in the "Framework Setup" section, after the last existing framework entry and before "Cloudflare Workers" - 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
- 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:
- Add a
## {Framework}section after the Elysia section (before## Browser), with a minimal setup snippet and a link to the example app - Add a row to the "Framework Support" table:
| **{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 withdrain(PostHog) andenrichcallbacks- 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()+ manuallog.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
.tsfile (src/ui.ts) exporting atestUI()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 supportStep 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 errorsThen type-check the example:
cd examples/{framework}
npx tsc --noEmit # Verify no TS errors in the example