SKILL.md
$27
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
defineFrameworkIntegration
../shared/integration
Manifest factory — extract request, create logger, attach, run with ALS
createMiddlewareLogger
../shared/middleware
Lower-level lifecycle (custom mode): 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, plugins
MiddlewareLoggerOptions
../shared/middleware
Internal options type extending BaseEvlogOptions with method, path, requestId, headers
createLoggerStorage
../shared/storage
Factory returning { storage, useLogger } for AsyncLocalStorage-backed useLogger()
defineFrameworkIntegration automatically:
- normalizes both Web
Headersand NodeIncomingHttpHeaders(so you don't need to pick the rightextractSafeHeaders*)
- generates a
requestIdwhen none is present
- calls
createMiddlewareLoggerand surfaces its{ logger, finish, skipped, middlewareOptions }
- attaches
log.fork()automatically whenstorageis provided
- exposes
runWith(fn)to run downstream handlers inside the integration's ALS
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 — built on defineFrameworkIntegration
Create packages/evlog/src/{framework}/index.ts. In manifest mode the file is typically 30–50 lines of framework glue — all pipeline logic (enrich, drain, keep, header filtering, ALS, fork) is handled by defineFrameworkIntegration + createMiddlewareLogger.
Template Structure (manifest mode)
import type { RequestLogger } from '../types'
import { defineFrameworkIntegration } from '../shared/integration'
import type { BaseEvlogOptions } from '../shared/middleware'
import { createLoggerStorage } from '../shared/storage'
// Only needed when the framework wants `useLogger()` ALS-style access.
// Hono/Elysia attach the logger to the framework's own context instead.
const { storage, useLogger } = createLoggerStorage(
'middleware context. Make sure the evlog middleware is registered before your routes.',
)
export type Evlog{Framework}Options = BaseEvlogOptions
export { useLogger }
// Type augmentation for typed logger access (framework-specific):
// - Express: declare module 'express-serve-static-core' { interface Request { log: RequestLogger } }
// - Hono: export type EvlogVariables = { Variables: { log: RequestLogger } }
const integration = defineFrameworkIntegration<{Framework}Context>({
name: '{framework}',
extractRequest: (ctx) => ({
method: /* ctx.method */,
path: /* ctx.path */,
headers: /* Web Headers OR Node headers OR plain object */,
requestId: /* x-request-id header or undefined → auto-generated */,
}),
attachLogger: (ctx, logger) => {
// Store in framework-idiomatic location:
// - Hono: c.set('log', logger)
// - Express: req.log = logger
// - Fastify: (req as any).log = logger
},
storage, // optional — only when using ALS-based useLogger()
})
export function evlog(options: Evlog{Framework}Options = {}): FrameworkMiddleware {
return async (ctx, next) => {
const { skipped, finish, runWith } = integration.start(ctx, options)
if (skipped) {
await next()
return
}
try {
await runWith(() => next())
await finish({ status: /* extract status from ctx */ })
} catch (error) {
await finish({ error: error as Error })
throw error
}
}
}
Reference Implementations
- Hono (~50 lines):
packages/evlog/src/hono/index.ts—c.set('log', logger), no ALS storage
- Express (~50 lines):
packages/evlog/src/express/index.ts—req.log, ALS storage,res.on('finish')for terminal status
- Fastify (~70 lines):
packages/evlog/src/fastify/index.ts— Fastify hooks (onRequest/onResponse/onError), ALS storage
- Elysia (~80 lines):
packages/evlog/src/elysia/index.ts— manifest extracts request, custom storage handling forenterWith-style ALS
Key Architecture Rules
- **Prefer
defineFrameworkIntegration** for any standard middleware shape — it handles header normalization, request-id generation, ALS, and fork attachment.
- Header normalization is automatic — pass either Web
Headersor NodeIncomingHttpHeadersfromextractRequest; the manifest picks the right extractor.
- **
storagetriggers ALS + fork** — when you provide astorage,defineFrameworkIntegrationautomatically attacheslog.fork()andrunWithruns the handler insidestorage.run.
- Status / error reporting stays framework-side — call
finish({ status })on success andfinish({ error })on failure.finishis what runs emit + enrich + drain + plugin hooks.
- Re-throw errors after
finish({ error })so the framework's own error handler still runs.
- Export options interface as
BaseEvlogOptions(or a framework-specific extension) for feature parity.
- Export type helpers for typed context access (e.g.,
EvlogVariablesfor Hono).
- Framework SDK is a peer dependency — never bundle it.
- Never duplicate pipeline logic —
runEnrichAndDrainis internal tocreateMiddlewareLogger/finish.
When to fall back to custom mode
Use createMiddlewareLogger directly (skipping defineFrameworkIntegration) when:
- The framework's middleware doesn't have a clear "request entry / response exit" pair (NestJS observable interceptor, Next.js App Router server actions).
- You need to defer the logger creation across multiple lifecycle phases (SvelteKit
handlehook + load functions).
- The framework's status is not knowable until after the response stream completes and you need bespoke wiring.
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 +
pnpm 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}
```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/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:
| {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
pnpm run build # Verify build succeeds with new entry
pnpm run test # Verify unit tests pass
pnpm run lint # Verify no lint errors
Then type-check the example:
cd examples/{framework}
npx tsc --noEmit # Verify no TS errors in the example