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/evlog/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/
- .
- 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
- (Hono, Elysia, etc.)
- extractSafeNodeHeaders
- ../shared/headers
- Convert Node.js
- IncomingHttpHeaders
- → filtered
- Record
- (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} ```ts [src/index.ts] // 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/evlog/SKILL.md
In skills/evlog/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}** |withimport { evlog } from 'evlog/{framework}'` (example) |
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