| 1 | --- |
| 2 | name: create-evlog-framework-integration |
| 3 | description: 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 (e.g., Hono, Elysia, Fastify, Express, NestJS) to the evlog package. Covers source code, build config, package exports, tests, example app, and all documentation. |
| 4 | --- |
| 5 | |
| 6 | # Create evlog Framework Integration |
| 7 | |
| 8 | 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 |
| 9 | |
| 10 | ## PR Title |
| 11 | |
| 12 | Recommended format for the pull request title: |
| 13 | |
| 14 | ``` |
| 15 | feat({framework}): add {Framework} middleware integration |
| 16 | ``` |
| 17 | |
| 18 | ## Touchpoints Checklist |
| 19 | |
| 20 | | # | File | Action | |
| 21 | |---|------|--------| |
| 22 | | 1 | `packages/evlog/src/{framework}/index.ts` | Create integration source |
| 23 | | 2 | `packages/evlog/tsdown.config.ts` | Add build entry + external |
| 24 | | 3 | `packages/evlog/package.json` | Add |
| 25 | | 4 | `packages/evlog/test/{framework}.test.ts` | Create tests |
| 26 | | 5 | `apps/docs/content/1.getting-started/2.installation.md` | Add framework section |
| 27 | | 6 | `apps/docs/content/6.examples/{N}.{framework}.md` | Create dedicated example page |
| 28 | | 7 | `apps/docs/content/0.landing.md` | Add framework code snippet |
| 29 | | 8 | `apps/docs/app/components/features/FeatureFrameworks.vue` | Add framework tab |
| 30 | | 9 | `AGENTS.md` | Add framework to integration section |
| 31 | | 10 | `examples/{framework}/` | Create example app with test UI |
| 32 | | 11 | `package.json` (root) | Add |
| 33 | | 12 | `.changeset/{framework}-integration.md` | Create changeset ( |
| 34 | | 13 | `.github/workflows/semantic-pull-request.yml` | Add |
| 35 | | 14 | `.github/pull_request_template.md` | Add |
| 36 | |
| 37 | **Important**: Do NOT consider the task complete until all 14 touchpoints have been addressed. |
| 38 | |
| 39 | ## Naming Conventions |
| 40 | |
| 41 | Use these placeholders consistently: |
| 42 | |
| 43 | | Placeholder | Example (Hono) | Usage | |
| 44 | |-------------|----------------|-------| |
| 45 | | `{framework}` | `hono |
| 46 | | `{Framework}` | `Hono |
| 47 | |
| 48 | ## Shared Utilities |
| 49 | |
| 50 | All integrations share the same core utilities. **Never reimplement logic that exists in shared/**. |
| 51 | |
| 52 | | Utility | Location | Purpose | |
| 53 | |---------|----------|---------| |
| 54 | | `createMiddlewareLogger` | `../shared/middleware |
| 55 | | `extractSafeHeaders` | `../shared/headers |
| 56 | | `extractSafeNodeHeaders` | `../shared/headers |
| 57 | | `MiddlewareLoggerOptions` | `../shared/middleware |
| 58 | |
| 59 | ### Test Helpers |
| 60 | |
| 61 | | Utility | Location | Purpose | |
| 62 | |---------|----------|---------| |
| 63 | | `createPipelineSpies()` | `test/helpers/framework |
| 64 | | `assertDrainCalledWith()` | `test/helpers/framework |
| 65 | | `assertEnrichBeforeDrain()` | `test/helpers/framework |
| 66 | | `assertSensitiveHeadersFiltered()` | `test/helpers/framework |
| 67 | | `assertWideEventShape()` | `test/helpers/framework |
| 68 | |
| 69 | ## Step 1: Integration Source |
| 70 | |
| 71 | Create `packages/evlog/src/{framework}/index.ts`. |
| 72 | |
| 73 | 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 |
| 74 | |
| 75 | ### Template Structure |
| 76 | |
| 77 | ```typescript |
| 78 | import { AsyncLocalStorage } |
| 79 | import type { DrainContext, |
| 80 | import { createMiddlewareLogger } |
| 81 | import { extractSafeHeaders } |
| 82 | // OR |
| 83 | import { extractSafeNodeHeaders } |
| 84 | |
| 85 | const storage = new |
| 86 | |
| 87 | export interface Evlog |
| 88 | include?: string[] |
| 89 | exclude?: string[] |
| 90 | routes?: Record<string |
| 91 | drain?: (ctx: |
| 92 | enrich?: (ctx: |
| 93 | keep?: (ctx: |
| 94 | } |
| 95 | |
| 96 | // Type augmentation for typed logger access (framework-specific) |
| 97 | // For Express: declare module 'express-serve-static-core' { interface Request { log: RequestLogger } } |
| 98 | // For Hono: export type EvlogVariables = { Variables: { log: RequestLogger } } |
| 99 | |
| 100 | /** |
| 101 | * Get the request-scoped logger from anywhere in the call stack. |
| 102 | * Uses AsyncLocalStorage — works across async boundaries. |
| 103 | */ |
| 104 | export function useLogger<T |
| 105 | const logger = |
| 106 | if (!logger) |
| 107 | throw new Error( |
| 108 | '[evlog] useLogger() was called outside of an evlog middleware context. ' |
| 109 | + 'Make sure the evlog middleware is registered before your routes.', |
| 110 | ) |
| 111 | } |
| 112 | return logger as |
| 113 | } |
| 114 | |
| 115 | export function evlog(options |
| 116 | return async (frameworkContext |
| 117 | const { logger, |
| 118 | method: /* extract from framework context */ |
| 119 | path: /* extract from framework context */ |
| 120 | requestId: /* extract x-request-id or crypto.randomUUID() */ |
| 121 | headers: extractSafeHeaders(/* |
| 122 | ...options, |
| 123 | }) |
| 124 | |
| 125 | if (skipped) { |
| 126 | await next() |
| 127 | return |
| 128 | } |
| 129 | |
| 130 | // Store logger in framework-specific context |
| 131 | // e.g., c.set('log', logger) for Hono |
| 132 | // e.g., req.log = logger for Express |
| 133 | |
| 134 | // Wrap next() in AsyncLocalStorage.run() for useLogger() support |
| 135 | // Express: storage.run(logger, () => next()) |
| 136 | // Hono: await storage.run(logger, () => next()) |
| 137 | } |
| 138 | } |
| 139 | ``` |
| 140 | |
| 141 | ### Reference Implementations |
| 142 | |
| 143 | - **Hono** (~40 lines): |
| 144 | - **Express** (~80 lines): |
| 145 | - **Elysia** (~70 lines): |
| 146 | |
| 147 | ### Key Architecture Rules |
| 148 | |
| 149 | 1. **Use `createMiddlewareLogger |
| 150 | 2. **Use the right header extractor** — |
| 151 | 3. **Spread user options into `createMiddlewareLogger |
| 152 | 4. **Store logger** in the framework's idiomatic context (e.g., |
| 153 | 5. **Export `useLogger() |
| 154 | 6. **Call `finish() |
| 155 | 7. **Re-throw errors** after |
| 156 | 8. **Export options interface** with drain/enrich/keep for feature parity across all frameworks |
| 157 | 9. **Export type helpers** for typed context access (e.g., |
| 158 | 10. **Framework SDK is a peer dependency** — never bundle it |
| 159 | 11. **Never duplicate pipeline logic** — |
| 160 | |
| 161 | ### Framework-Specific Patterns |
| 162 | |
| 163 | **Hono**: Use `MiddlewareHandler |
| 164 | |
| 165 | **Express**: Standard `(req, res, next) |
| 166 | |
| 167 | **Elysia**: Return `new Elysia({ name: 'evlog' }) |
| 168 | |
| 169 | **Fastify**: Use `fastify-plugin |
| 170 | |
| 171 | **NestJS**: `NestInterceptor |
| 172 | |
| 173 | ## Step 2: Build Config |
| 174 | |
| 175 | Add a build entry in `packages/evlog/tsdown.config.ts`: |
| 176 | |
| 177 | ```typescript |
| 178 | '{framework}/index': 'src/{framework}/index.ts', |
| 179 | ``` |
| 180 | |
| 181 | Place it after the existing framework entries (workers, next, hono, express). |
| 182 | |
| 183 | Also add the framework SDK to the `external` array: |
| 184 | |
| 185 | ```typescript |
| 186 | external: [ |
| 187 | // ... existing externals |
| 188 | '{framework-package}', // e.g., 'elysia', 'fastify', 'express' |
| 189 | ], |
| 190 | ``` |
| 191 | |
| 192 | ## Step 3: Package Exports |
| 193 | |
| 194 | In `packages/evlog/package.json`, add four entries: |
| 195 | |
| 196 | **In `exports |
| 197 | |
| 198 | ```json |
| 199 | "./{framework}": { |
| 200 | "types": |
| 201 | "import": |
| 202 | } |
| 203 | ``` |
| 204 | |
| 205 | **In `typesVersions["*"] |
| 206 | |
| 207 | ```json |
| 208 | "{framework}": [ |
| 209 | "./dist/{framework}/index.d.mts" |
| 210 | ] |
| 211 | ``` |
| 212 | |
| 213 | **In `peerDependencies |
| 214 | |
| 215 | ```json |
| 216 | "{framework-package}": "^{latest-major}.0.0" |
| 217 | ``` |
| 218 | |
| 219 | **In `peerDependenciesMeta |
| 220 | |
| 221 | ```json |
| 222 | "{framework-package}": { |
| 223 | "optional": |
| 224 | } |
| 225 | ``` |
| 226 | |
| 227 | **In `keywords |
| 228 | |
| 229 | ## Step 4: Tests |
| 230 | |
| 231 | Create `packages/evlog/test/{framework}.test.ts`. |
| 232 | |
| 233 | **Import shared test helpers** from `./helpers/framework |
| 234 | |
| 235 | ```typescript |
| 236 | import { |
| 237 | assertDrainCalledWith, |
| 238 | assertEnrichBeforeDrain, |
| 239 | assertSensitiveHeadersFiltered, |
| 240 | createPipelineSpies, |
| 241 | } from './helpers/framework' |
| 242 | ``` |
| 243 | |
| 244 | Required test categories: |
| 245 | |
| 246 | 1. **Middleware creates logger** — verify |
| 247 | 2. **Auto-emit on response** — verify event includes status, method, path, duration |
| 248 | 3. **Error handling** — verify errors are captured and event has error level + error details |
| 249 | 4. **Route filtering** — verify skipped routes don't create a logger |
| 250 | 5. **Request ID forwarding** — verify |
| 251 | 6. **Context accumulation** — verify |
| 252 | 7. **Drain callback** — use |
| 253 | 8. **Enrich callback** — use |
| 254 | 9. **Keep callback** — verify tail sampling callback receives context and can force-keep logs |
| 255 | 10. **Sensitive header filtering** — use |
| 256 | 11. **Drain/enrich error resilience** — verify errors in drain/enrich do not break the request |
| 257 | 12. **Skipped routes skip drain/enrich** — verify drain/enrich are not called for excluded routes |
| 258 | 13. **useLogger() returns same logger** — verify |
| 259 | 14. **useLogger() throws outside context** — verify error thrown when called without middleware |
| 260 | 15. **useLogger() works across async** — verify logger accessible in async service functions |
| 261 | |
| 262 | Use the framework's test utilities when available (e.g., Hono's `app.request()`, Express's `supertest`, Fastify's |
| 263 | |
| 264 | ## Step 5: Installation Docs |
| 265 | |
| 266 | Add a section in `apps/docs/content/1.getting-started/2.installation.md` for the framework. |
| 267 | |
| 268 | Follow the pattern of existing framework sections (Nuxt, Next.js, Nitro, Hono, Express). Include: |
| 269 | |
| 270 | 1. **One-liner description** mentioning |
| 271 | 2. **Install command** — |
| 272 | 3. **Setup code** — minimal working example with imports and configuration |
| 273 | 4. **Usage in routes** — how to access the logger in route handlers |
| 274 | 5. **Full pipeline example** — show drain + enrich + keep configuration |
| 275 | 6. **Error handling** — how structured errors work with the framework |
| 276 | 7. **Callout** linking to the dedicated example page |
| 277 | |
| 278 | ## Step 6: Example Docs Page |
| 279 | |
| 280 | Create `apps/docs/content/6.examples/{N}.{framework}.md` with a comprehensive guide. |
| 281 | |
| 282 | **Frontmatter**: |
| 283 | |
| 284 | ```yaml |
| 285 | --- |
| 286 | title: {Framework} |
| 287 | description: Using evlog with {Framework} — automatic wide events, structured errors, drain adapters, enrichers, and tail sampling. |
| 288 | navigation: |
| 289 | title: {Framework} |
| 290 | icon: i-simple-icons-{framework} |
| 291 | links: |
| 292 | - label: Source Code |
| 293 | icon: i-simple-icons-github |
| 294 | to: https://github.com/HugoRCD/evlog/tree/main/examples/{framework} |
| 295 | color: neutral |
| 296 | variant: subtle |
| 297 | --- |
| 298 | ``` |
| 299 | |
| 300 | **Sections** (follow the Express/Hono example pages): |
| 301 | |
| 302 | 1. **Setup** — install + register middleware |
| 303 | 2. **Wide Events** — |
| 304 | 3. **useLogger()** — accessing logger from services without passing req |
| 305 | 4. **Error Handling** — |
| 306 | 5. **Drain & Enrichers** — middleware options |
| 307 | 6. **Tail Sampling** — |
| 308 | 7. **Route Filtering** — |
| 309 | 8. **Run Locally** — clone + |
| 310 | 9. **Card group** linking to GitHub source |
| 311 | |
| 312 | ## Step 7: Landing Page |
| 313 | |
| 314 | Add a code snippet in `apps/docs/content/0.landing.md` for the framework. |
| 315 | |
| 316 | Find the `FeatureFrameworks` MDC component usage (the section with `#nuxt`, ` |
| 317 | |
| 318 | ```markdown |
| 319 | #{framework} |
| 320 | ```ts [src/index.ts] |
| 321 | // Framework-specific code example showing evlog usage |
| 322 | ``` |
| 323 | ``` |
| 324 | |
| 325 | Place the snippet in the correct order relative to existing frameworks. |
| 326 | |
| 327 | ## Step 8: FeatureFrameworks Component |
| 328 | |
| 329 | Update `apps/docs/app/components/features/FeatureFrameworks.vue`: |
| 330 | |
| 331 | 1. Add the framework to the `frameworks` array with its icon and the **next available tab index** |
| 332 | 2. Add a `<div v-show="activeTab === {N}">` with `<slot name="{framework}" />` in the template |
| 333 | 3. **Increment tab indices** for any frameworks that come after the new one |
| 334 | |
| 335 | Icons use Simple Icons format: `i-simple-icons-{name}` (e.g., `i-simple-icons-express`, `i-simple-icons-hono`). |
| 336 | |
| 337 | ## Step 9: Update AGENTS.md |
| 338 | |
| 339 | In the root `AGENTS.md` file: |
| 340 | |
| 341 | 1. Add the framework to the **"Framework Integration"** section |
| 342 | 2. Add import path and basic setup example (showing both `req.log` and `useLogger()`) |
| 343 | 3. Show drain/enrich/keep usage |
| 344 | |
| 345 | ## Step 10: Example App |
| 346 | |
| 347 | Create `examples/{framework}/` with a runnable app that demonstrates all evlog features. |
| 348 | |
| 349 | The app must include: |
| 350 | |
| 351 | 1. **`evlog()` middleware** with `drain` (PostHog) and `enrich` callbacks |
| 352 | 2. **Health route** — basic `log.set()` usage |
| 353 | 3. **Data route** — context accumulation with user/business data, using `useLogger()` in a service function |
| 354 | 4. **Error route** — `createError()` with status/why/fix/link |
| 355 | 5. **Error handler** — framework's error handler with `parseError()` + manual `log.error()` |
| 356 | 6. **Test UI** — served at `/`, a self-contained HTML page with buttons to hit each route and display JSON responses |
| 357 | |
| 358 | **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. |
| 359 | |
| 360 | Pretty printing should be enabled so the output is readable when testing locally. |
| 361 | |
| 362 | **Type the `enrich` callback parameter explicitly** — use `type EnrichContext` from `evlog` to avoid implicit `any`: |
| 363 | |
| 364 | ```typescript |
| 365 | import { type EnrichContext } from 'evlog' |
| 366 | |
| 367 | app.use(evlog({ |
| 368 | enrich: (ctx: EnrichContext) => { |
| 369 | ctx.event.runtime = 'node' |
| 370 | }, |
| 371 | })) |
| 372 | ``` |
| 373 | |
| 374 | ### Test UI |
| 375 | |
| 376 | 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. |
| 377 | |
| 378 | The UI must: |
| 379 | - List all available routes with method badge + path + description |
| 380 | - Send the request on click and display the JSON response with syntax highlighting |
| 381 | - Show status code (color-coded 2xx/4xx/5xx) and response time |
| 382 | - Use a dark theme with monospace font |
| 383 | - Be a single `.ts` file (`src/ui.ts`) exporting a |
| 384 | - The root `/` route must be registered **before |
| 385 | |
| 386 | Reference: `examples/hono/src/ui.ts` for the canonical pattern. Copy and adapt for each framework. |
| 387 | |
| 388 | ### Required files |
| 389 | |
| 390 | | File | Purpose | |
| 391 | |------|---------| |
| 392 | | `src/index.ts` | App with all features demonstrated | |
| 393 | | `src/ui.ts` | Test UI — `testUI() |
| 394 | | `package.json` | `dev |
| 395 | | `tsconfig.json` | TypeScript config (if needed) | |
| 396 | | `README.md` | How to run + link to the UI | |
| 397 | |
| 398 | ### Package scripts |
| 399 | |
| 400 | ```json |
| 401 | { |
| 402 | "scripts": |
| 403 | "dev": |
| 404 | "start": |
| 405 | } |
| 406 | } |
| 407 | ``` |
| 408 | |
| 409 | ## Step 11: Root Package Script |
| 410 | |
| 411 | Add a root-level script in the monorepo `package.json`: |
| 412 | |
| 413 | ```json |
| 414 | "example:{framework}": "dotenv -- turbo run dev --filter=evlog-{framework}-example" |
| 415 | ``` |
| 416 | |
| 417 | The `dotenv --` prefix loads the root `.env` file (containing ` |
| 418 | |
| 419 | ## Step 12: Changeset |
| 420 | |
| 421 | Create `.changeset/{framework}-integration.md`: |
| 422 | |
| 423 | ```markdown |
| 424 | --- |
| 425 | "evlog": minor |
| 426 | --- |
| 427 | |
| 428 | Add {Framework} middleware integration (`evlog/{framework}`) with automatic wide-event logging, drain, enrich, and tail sampling support |
| 429 | ``` |
| 430 | |
| 431 | ## Step 13 & 14: PR Scopes |
| 432 | |
| 433 | Add the framework name as a valid scope in **both** files so PR title validation passes: |
| 434 | |
| 435 | **`.github/workflows/semantic-pull-request.yml` |
| 436 | |
| 437 | ```yaml |
| 438 | scopes: | |
| 439 | # ... existing scopes |
| 440 | {framework} |
| 441 | ``` |
| 442 | |
| 443 | **`.github/pull_request_template.md` |
| 444 | |
| 445 | ```markdown |
| 446 | - {framework} ({Framework} integration) |
| 447 | ``` |
| 448 | |
| 449 | ## Verification |
| 450 | |
| 451 | After completing all steps, run from the repo root: |
| 452 | |
| 453 | ```bash |
| 454 | cd packages/evlog |
| 455 | bun run build # |
| 456 | bun run test # |
| 457 | bun run lint # |
| 458 | ``` |
| 459 | |
| 460 | Then type-check the example: |
| 461 | |
| 462 | ```bash |
| 463 | cd examples/{framework} |
| 464 | npx tsc --noEmit # |
| 465 | ``` |