create-evlog-framework-integration
Create evlog Framework Integration
Add a new framework integration to evlog. The recommended path is the manifest mode built on defineFrameworkIntegration from evlog/toolkit — for any framework with a request/response middleware shape (Hono, Express, Elysia, Fastify, …). For frameworks with a fundamentally different lifecycle (NestJS interceptors, SvelteKit handle hooks, Next.js App Router) you'll fall back to the lower-level createMiddlewareLogger.
Two paths
- Manifest mode (preferred, ~30 lines of glue) — call
defineFrameworkIntegration({ name, extractRequest, attachLogger, storage? })once at module level, then write a tiny middleware that callsintegration.start(ctx, options)and runs the framework'snext()insiderunWith. Reference implementations:packages/evlog/src/{hono,express,elysia,fastify}/index.ts. - Custom mode — use
createMiddlewareLoggerdirectly when the framework's lifecycle doesn't fit a standard middleware (NestJS, Next.js, SvelteKit). All current built-ins for those frameworks live underpackages/evlog/src/{nestjs,next,sveltekit}/.
Manifest mode covers ~80% of integrations and reduces glue from 50–80 lines to ~30. Use custom mode only when you can't extract a request synchronously at the start of the lifecycle.
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 |
|---|---|---|
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
defineFrameworkIntegrationfor 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:
```markdown
| **{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 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
More from hugorcd/evlog
review-logging-patterns
Review code for logging patterns and suggest evlog adoption. Guides setup on Nuxt, Next.js, SvelteKit, Nitro, TanStack Start, React Router, NestJS, Express, Hono, Fastify, Elysia, Cloudflare Workers, and standalone TypeScript. Detects console.log spam, unstructured errors, and missing context. Covers wide events, structured errors, drain adapters (Axiom, OTLP, HyperDX, PostHog, Sentry, Better Stack), sampling, enrichers, and AI SDK integration (token usage, tool calls, streaming metrics).
690create-evlog-adapter
Create a new built-in evlog adapter to send wide events to an external observability platform. Use when adding a new drain adapter (e.g., for Datadog, Sentry, Loki, Elasticsearch, etc.) to the evlog package. Covers source code, build config, package exports, tests, and all documentation.
659create-evlog-enricher
Create a new built-in evlog enricher to add derived context to wide events. Use when adding a new enricher (e.g., for deployment metadata, tenant context, feature flags, etc.) to the evlog package. Covers source code, tests, and all documentation.
633analyze-logs
Analyze application logs from the .evlog/logs/ directory. Use when debugging errors, investigating slow requests, understanding request patterns, or answering questions about application behavior. Reads structured NDJSON wide events written by evlog's file system drain.
203