skills/hugorcd/evlog/create-evlog-framework-integration

create-evlog-framework-integration

SKILL.md

Create evlog Framework Integration

Add a new framework integration to evlog. Every integration follows the same architecture built on the shared createMiddlewareLogger 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 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
createMiddlewareLogger ../shared/middleware Full lifecycle: 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
MiddlewareLoggerOptions ../shared/middleware Internal options type extending BaseEvlogOptions with method, path, requestId, headers
createLoggerStorage ../shared/storage Factory returning { storage, useLogger } for AsyncLocalStorage-backed useLogger()

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

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

Template Structure

import type { RequestLogger } from '../types'
import { createMiddlewareLogger, type BaseEvlogOptions } 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)
import { createLoggerStorage } from '../shared/storage'

const { storage, useLogger } = createLoggerStorage(
  'middleware context. Make sure the evlog middleware is registered before your routes.',
)

export interface Evlog{Framework}Options extends BaseEvlogOptions {}

export { useLogger }

// 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 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, c.set('log', logger), wraps next() in try/catch
  • Express (~80 lines): packages/evlog/src/express/index.ts — Node.js headers, req.log, res.on('finish'), AsyncLocalStorage for useLogger()
  • Elysia (~70 lines): packages/evlog/src/elysia/index.ts — Web API Headers, derive() plugin, onAfterHandle/onError, AsyncLocalStorage for useLogger()

Key Architecture Rules

  1. Use createMiddlewareLogger — never call createRequestLogger directly
  2. Use the right header extractorextractSafeHeaders for Web API Headers, extractSafeNodeHeaders for Node.js IncomingHttpHeaders
  3. Spread user options into createMiddlewareLoggerdrain, enrich, keep are handled automatically by finish()
  4. Store logger in the framework's idiomatic context (e.g., c.set() for Hono, req.log for Express, .derive() for Elysia)
  5. Export useLogger() — backed by AsyncLocalStorage so the logger is accessible from anywhere in the call stack
  6. Call finish() in both success and error paths — it handles emit + enrich + drain
  7. Re-throw errors after finish() so framework error handlers still work
  8. Export options interface with drain/enrich/keep for feature parity across all frameworks
  9. Export type helpers for typed context access (e.g., EvlogVariables for Hono)
  10. Framework SDK is a peer dependency — never bundle it
  11. Never duplicate pipeline logiccallEnrichAndDrain is internal to createMiddlewareLogger

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 — browser drain (only if framework has a client-side story)
  10. Run Locally — clone + bun 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
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:

cd examples/{framework}
npx tsc --noEmit  # Verify no TS errors in the example
Weekly Installs
146
Repository
hugorcd/evlog
GitHub Stars
875
First Seen
12 days ago
Installed on
opencode143
gemini-cli143
github-copilot143
amp143
cline143
codex143