create-evlog-framework-integration

Create a new evlog framework integration to add automatic wide-event logging to an HTTP framework. Use when adding middleware/plugin support for a framework…

INSTALLATION
npx skills add https://github.com/hugorcd/evlog --skill create-evlog-framework-integration
Run in your project or agent environment. Adjust flags if your CLI version differs.

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 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

  • **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 Headers or Node IncomingHttpHeaders from extractRequest; the manifest picks the right extractor.
  • **storage triggers ALS + fork** — when you provide a storage, defineFrameworkIntegration automatically attaches log.fork() and runWith runs the handler inside storage.run.
  • 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.
  • 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., EvlogVariables for Hono).
  • Framework SDK is a peer dependency — never bundle it.
  • 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:

  • 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 HandlingcreateError() + parseError() + framework error handler
  • Drain &#x26; Enrichers — middleware options with inline example
  • Pipeline (Batching &#x26; Retry)createDrainPipeline example
  • Tail Samplingkeep callback
  • Route Filteringinclude / 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 &#x26; 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/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 &#x26; 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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card