Create evlog Framework Integration
Add a new framework integration to evlog. Every integration follows the same architecture built on the shared
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 + + peer dep + keyword |
| 4 | packages/evlog/test/{framework}.test.ts
| Create tests |
| 5 | apps/docs/content/1.getting-started/2.installation.md
| Add framework section |
| 6 | apps/docs/content/6.examples/{N}.{framework}.md
| Create dedicated example page |
| 7 | apps/docs/content/0.landing.md
| Add framework code snippet |
| 8 | apps/docs/app/components/features/FeatureFrameworks.vue
| Add framework tab |
| 9 | | Add framework to integration section |
| 10 | | Create example app with test UI |
| 11 | (root) | Add script |
| 12 | .changeset/{framework}-integration.md
| Create changeset () |
| 13 | .github/workflows/semantic-pull-request.yml
| Add scope |
| 14 | .github/pull_request_template.md
| Add scope |
Important: Do NOT consider the task complete until all 14 touchpoints have been addressed.
Naming Conventions
Use these placeholders consistently:
| Placeholder | Example (Hono) | Usage |
|---|
| | Directory names, import paths, file names |
| | PascalCase in type/interface names |
Shared Utilities
All integrations share the same core utilities. Never reimplement logic that exists in shared/.
| Utility | Location | Purpose |
|---|
| | Full lifecycle: logger creation, route filtering, tail sampling, emit, enrich, drain |
| | Convert Web API → filtered (Hono, Elysia, etc.) |
| | Convert Node.js → filtered (Express, Fastify, NestJS) |
| | Base options type with , , , , , , |
Test Helpers
| Utility | Location | Purpose |
|---|
| | Creates mock drain/enrich/keep callbacks |
| | Validates drain was called with expected event shape |
assertEnrichBeforeDrain()
| | Validates enrich runs before drain |
assertSensitiveHeadersFiltered()
| | Validates sensitive headers are excluded |
| | 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
.
Template Structure
typescript
import { AsyncLocalStorage } from 'node:async_hooks'
import type { DrainContext, EnrichContext, RequestLogger, RouteConfig, TailSamplingContext } from '../types'
import { createMiddlewareLogger } 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)
const storage = new AsyncLocalStorage<RequestLogger>()
export interface Evlog{Framework}Options {
include?: string[]
exclude?: string[]
routes?: Record<string, RouteConfig>
drain?: (ctx: DrainContext) => void | Promise<void>
enrich?: (ctx: EnrichContext) => void | Promise<void>
keep?: (ctx: TailSamplingContext) => void | Promise<void>
}
// 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 useLogger<T extends object = Record<string, unknown>>(): RequestLogger<T> {
const logger = storage.getStore()
if (!logger) {
throw new Error(
'[evlog] useLogger() was called outside of an evlog middleware context. '
+ 'Make sure the evlog middleware is registered before your routes.',
)
}
return logger as RequestLogger<T>
}
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, , wraps in try/catch
- Express (~80 lines):
packages/evlog/src/express/index.ts
— Node.js headers, , , for
- Elysia (~70 lines):
packages/evlog/src/elysia/index.ts
— Web API Headers, plugin, /, for
Key Architecture Rules
- Use — never call directly
- Use the right header extractor — for Web API , for Node.js
- Spread user options into — , , are handled automatically by
- Store logger in the framework's idiomatic context (e.g., for Hono, for Express, for Elysia)
- Export — backed by so the logger is accessible from anywhere in the call stack
- Call in both success and error paths — it handles emit + enrich + drain
- Re-throw errors after 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., for Hono)
- Framework SDK is a peer dependency — never bundle it
- Never duplicate pipeline logic — is internal to
Framework-Specific Patterns
Hono: Use
return type,
,
for status,
for headers.
Express: Standard
middleware,
for response end,
storage.run(logger, () => next())
for
. Type augmentation targets
express-serve-static-core
(NOT
). Error handler uses
type.
Elysia: Return
new Elysia({ name: 'evlog' })
plugin, use
.derive({ as: 'global' })
to create logger and attach
to context,
for success path,
for error path. Use
storage.enterWith(logger)
in
for
support. Note:
is fire-and-forget and may not complete before
returns in tests — use
instead.
Fastify: Use
wrapper,
fastify.decorateRequest('log', null)
,
/
hooks.
NestJS:
with
,
/
on observable,
dynamic module.
Step 2: Build Config
Add a build entry in
packages/evlog/tsdown.config.ts
:
typescript
'{framework}/index': 'src/{framework}/index.ts',
Place it after the existing framework entries (workers, next, hono, express).
Also add the framework SDK to the
array:
typescript
external: [
// ... existing externals
'{framework-package}', // e.g., 'elysia', 'fastify', 'express'
],
Step 3: Package Exports
In
packages/evlog/package.json
, add four entries:
In (after the last framework entry):
json
"./{framework}": {
"types": "./dist/{framework}/index.d.mts",
"import": "./dist/{framework}/index.mjs"
}
json
"{framework}": [
"./dist/{framework}/index.d.mts"
]
json
"{framework-package}": "^{latest-major}.0.0"
json
"{framework-package}": {
"optional": true
}
In — add the framework name to the keywords array.
Step 4: Tests
Create
packages/evlog/test/{framework}.test.ts
.
Import shared test helpers from
:
typescript
import {
assertDrainCalledWith,
assertEnrichBeforeDrain,
assertSensitiveHeadersFiltered,
createPipelineSpies,
} from './helpers/framework'
Required test categories:
- Middleware creates logger — verify or returns a
- 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 header is used when present
- Context accumulation — verify data appears in emitted event
- Drain callback — use 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 (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
, Express's
, Fastify's
).
Step 5: Installation Docs
Add a section in
apps/docs/content/1.getting-started/2.installation.md
for the framework.
Follow the pattern of existing framework sections (Nuxt, Next.js, Nitro, Hono, Express). Include:
- One-liner description mentioning / and access
- Install command —
npm install evlog {framework}
- Setup code — minimal working example with imports and configuration
- Usage in routes — how to access the logger in route handlers
- Full pipeline example — show drain + enrich + keep configuration
- Error handling — how structured errors work with the framework
- Callout linking to the dedicated example page
Step 6: Example Docs Page
Create
apps/docs/content/6.examples/{N}.{framework}.md
with a comprehensive guide.
Frontmatter:
yaml
---
title: {Framework}
description: Using evlog with {Framework} — automatic wide events, structured errors, drain adapters, enrichers, and tail sampling.
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 example pages):
- Setup — install + register middleware
- Wide Events — / usage
- useLogger() — accessing logger from services without passing req
- Error Handling — + framework error handler
- Drain & Enrichers — middleware options
- Tail Sampling — callback
- Route Filtering — / /
- Run Locally — clone +
bun run example:{framework}
- Card group linking to GitHub source
Step 7: Landing Page
Add a code snippet in
apps/docs/content/0.landing.md
for the framework.
Find the
MDC component usage (the section with
,
,
,
, etc.) and add a new slot:
markdown
#{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 AGENTS.md
In the root `AGENTS.md` file:
1. Add the framework to the **"Framework Integration"** section
2. Add import path and basic setup example (showing both `req.log` and `useLogger()`)
3. Show drain/enrich/keep usage
## Step 10: Example App
Create `examples/{framework}/` with a runnable app that demonstrates all evlog features.
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
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`). 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`:
```typescript
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
— 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 file () exporting a function returning an HTML string
- The root route must be registered before the evlog middleware so it doesn't get logged
Reference:
for the canonical pattern. Copy and adapt for each framework.
Required files
| File | Purpose |
|---|
| App with all features demonstrated |
| Test UI — returning self-contained HTML |
| and scripts |
| TypeScript config (if needed) |
| How to run + link to the UI |
Package scripts
json
{
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts"
}
}
Step 11: Root Package Script
Add a root-level script in the monorepo
:
json
"example:{framework}": "dotenv -- turbo run dev --filter=evlog-{framework}-example"
The
prefix loads the root
file (containing
and other adapter keys) into the process before turbo starts. Turborepo does not load
files —
handles this at the root level so individual examples need no env configuration.
Step 12: Changeset
Create
.changeset/{framework}-integration.md
:
markdown
---
"evlog": minor
---
Add {Framework} middleware integration (`evlog/{framework}`) with automatic wide-event logging, drain, enrich, and tail sampling support
Step 13 & 14: 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
to the
list:
yaml
scopes: |
# ... existing scopes
{framework}
.github/pull_request_template.md
— add
to the Scopes section:
markdown
- {framework} ({Framework} integration)
Verification
After completing all steps, run from the repo root:
bash
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:
bash
cd examples/{framework}
npx tsc --noEmit # Verify no TS errors in the example