create-evlog-framework-integration

Installation
SKILL.md

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 calls integration.start(ctx, options) and runs the framework's next() inside runWith. Reference implementations: packages/evlog/src/{hono,express,elysia,fastify}/index.ts.
  • Custom mode — use createMiddlewareLogger directly when the framework's lifecycle doesn't fit a standard middleware (NestJS, Next.js, SvelteKit). All current built-ins for those frameworks live under packages/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 Headers and Node IncomingHttpHeaders (so you don't need to pick the right extractSafeHeaders*)
  • generates a requestId when none is present
  • calls createMiddlewareLogger and surfaces its { logger, finish, skipped, middlewareOptions }
  • attaches log.fork() automatically when storage is 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.tsc.set('log', logger), no ALS storage
  • Express (~50 lines): packages/evlog/src/express/index.tsreq.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 for enterWith-style ALS

Key Architecture Rules

  1. Prefer defineFrameworkIntegration for any standard middleware shape — it handles header normalization, request-id generation, ALS, and fork attachment.
  2. Header normalization is automatic — pass either Web Headers or Node IncomingHttpHeaders from extractRequest; the manifest picks the right extractor.
  3. storage triggers ALS + fork — when you provide a storage, defineFrameworkIntegration automatically attaches log.fork() and runWith runs the handler inside storage.run.
  4. Status / error reporting stays framework-side — call finish({ status }) on success and finish({ error }) on failure. finish is what runs emit + enrich + drain + plugin hooks.
  5. Re-throw errors after finish({ error }) so the framework's own error handler still runs.
  6. Export options interface as BaseEvlogOptions (or a framework-specific extension) for feature parity.
  7. Export type helpers for typed context access (e.g., EvlogVariables for Hono).
  8. Framework SDK is a peer dependency — never bundle it.
  9. Never duplicate pipeline logicrunEnrichAndDrain is internal to createMiddlewareLogger/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 handle hook + 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:

  1. Middleware creates logger — verify c.get('log') or req.log returns a RequestLogger
  2. Auto-emit on response — verify event includes status, method, path, duration
  3. Error handling — verify errors are captured and event has error level + error details
  4. Route filtering — verify skipped routes don't create a logger
  5. Request ID forwarding — verify x-request-id header is used when present
  6. Context accumulation — verify logger.set() data appears in emitted event
  7. Drain callback — use assertDrainCalledWith() helper
  8. Enrich callback — use assertEnrichBeforeDrain() helper
  9. Keep callback — verify tail sampling callback receives context and can force-keep logs
  10. Sensitive header filtering — use assertSensitiveHeadersFiltered() helper
  11. Drain/enrich error resilience — verify errors in drain/enrich do not break the request
  12. Skipped routes skip drain/enrich — verify drain/enrich are not called for excluded routes
  13. useLogger() returns same logger — verify useLogger() === req.log (or framework equivalent)
  14. useLogger() throws outside context — verify error thrown when called without middleware
  15. 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):

  1. Quick Start — install + register middleware (copy-paste minimum setup)
  2. Wide Events — progressive log.set() usage
  3. useLogger() — accessing logger from services without passing req
  4. Error HandlingcreateError() + parseError() + framework error handler
  5. Drain & Enrichers — middleware options with inline example
  6. Pipeline (Batching & Retry)createDrainPipeline example
  7. Tail Samplingkeep callback
  8. Route Filteringinclude / exclude / routes
  9. Client-Side Logging — HTTP drain (evlog/http) (only if framework has a client-side story)
  10. Run Locally — clone + pnpm run example:{framework}
  11. Card group linking to GitHub source

Step 6: Overview & Installation Cards

In apps/docs/content/2.frameworks/00.overview.md:

  1. Add a row to the Overview table with framework name, import, type, logger access, and status
  2. 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:

  1. Add a :::card in the "Choose Your Framework" ::card-group with color: neutral
  2. 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:

  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 routecreateError() 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:

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
Related skills
Installs
405
Repository
hugorcd/evlog
GitHub Stars
1.3K
First Seen
Mar 6, 2026