--- name: create-evlog-framework-integration 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., Koa, H3 standalone, Deno Fresh, etc.) to the evlog package. Covers source code, build config, package exports, tests, example app, and all documentation. metadata: internal: true --- # 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. For frameworks with a fundamentally different lifecycle you'll fall back to the lower-level `createMiddlewareLogger`. ## Two paths - **Manifest mode** (preferred, ~30–80 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: all of `packages/evlog/src/{hono,express,fastify,elysia,nestjs,orpc,react-router,sveltekit,workers}/index.ts` use it. - **Custom mode**: use `createMiddlewareLogger` directly when the framework's lifecycle doesn't fit a standard middleware. Current custom-mode integrations: Next.js (`src/next/`), Nitro v2/v3 (`src/nitro/`, `src/nitro-v3/`), Eve (`src/eve/`). Manifest mode now covers all classic HTTP frameworks. Use custom mode only when you can't extract a request synchronously at the start of the lifecycle (server actions, module-level hooks, agent turns). ## Required API surface (from AGENTS.md) Every framework integration must expose: 1. `evlog()` middleware/plugin accepting the full `BaseEvlogOptions` (`drain`, `enrich`, `keep`, `include`, `exclude`, `routes`, `plugins`) 2. `useLogger()` (ALS-backed). Workers is the one sanctioned exception (ALS needs a compat flag there; `defineWorkerFetch` attaches the logger instead) 3. `log.fork()` support (automatic when `storage` is provided to the manifest) 4. The framework-native accessor (`c.get('log')`, `req.log`, `event.locals.log`, …) ## PR Title ``` feat({framework}): add {Framework} middleware integration ``` **Scope timing caveat**: the semantic PR check reads its scope list from the **base branch**, so a brand-new scope can't validate the very PR that introduces it. Either register the scope in a small preceding PR, or use an unscoped title (`feat: add {Framework} middleware integration`) on the introducing PR. ## 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` + optional peer dep + keyword | | 4 | `packages/evlog/test/frameworks/{framework}.test.ts` | Create tests (real request driver + `describeStandardHttpMatrix`) | | 5 | `packages/evlog/test/toolkit/__snapshots__/api-surface.test.ts.snap` | Regenerated by `pnpm run build` + `pnpm test` | | 6 | `apps/docs/content/4.integrate/frameworks/{NN}.{framework}.md` | Create framework docs page | | 7 | `apps/docs/content/4.integrate/frameworks/00.overview.md` | Add table row + card | | 8 | `apps/docs/content/1.start/3.installation.md` | Add card in "Choose Your Framework" | | 9 | `apps/docs/content/0.landing.md` | Add framework code snippet slot | | 10 | `apps/docs/app/components/features/FeatureFrameworks.vue` | Add framework tab | | 11 | `skills/review-logging-patterns/SKILL.md` | Add framework setup section + update frontmatter description | | 12 | `packages/evlog/README.md` | Add framework section + row in the Framework Support table | | 13 | `examples/{framework}/` | Create example app with test UI (auto-discovered by `pnpm example {framework}`, no root script needed) | | 14 | `.changeset/{framework}-integration.md` | Create changeset (`minor`) | | 15 | `.github/workflows/semantic-pull-request.yml` + `.github/pull_request_template.md` | Register `{framework}` as a PR scope in both files | **Important**: Do NOT consider the task complete until all 15 touchpoints have been addressed. ## Naming Conventions | Placeholder | Example (Hono) | Usage | |-------------|----------------|-------| | `{framework}` | `hono` | Directory names, import paths, file names, PR scope | | `{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](https://evlog.dev/extend/custom-framework)). | 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 | | `BaseEvlogOptions` | `../shared/middleware` | Base user-facing options type with `drain`, `enrich`, `keep`, `include`, `exclude`, `routes`, `plugins` | | `createLoggerStorage` | `../shared/storage` (`evlog/toolkit/storage`) | Factory returning `{ storage, useLogger }` for `AsyncLocalStorage`-backed `useLogger()`. Prefer `evlog/toolkit/storage` on Workers / edge | | `shouldDeferEmitForResponse` | `../shared/streamResponse` | Defer the wide event until a streaming body closes (see Hono/Elysia) | `defineFrameworkIntegration` automatically: - normalizes both Web `Headers` and Node `IncomingHttpHeaders` (so you don't need to pick a header extractor) - 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 - forwards `waitUntil` when the runtime provides one (Workers, Hono on Workers) ## Step 1: Integration Source Create `packages/evlog/src/{framework}/index.ts`. ### Template Structure (manifest mode) ```typescript import type { AuditableLogger } from '../audit' import { defineFrameworkIntegration } from '../shared/integration' import type { BaseEvlogOptions } from '../shared/middleware' import { createLoggerStorage } from '../shared/storage' const { storage, useLogger } = createLoggerStorage( 'middleware context. Make sure the evlog middleware is registered before your routes.', 'evlog:{framework}', ) 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: AuditableLogger } } // - Hono: export type EvlogVariables = { Variables: { log: AuditableLogger } } 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 }, storage, }) 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**: `src/hono/index.ts`. `c.set('log', logger)` + ALS `useLogger()`, streaming deferral via `shouldDeferEmitForResponse`, `waitUntil` detection - **Express**: `src/express/index.ts`. `req.log`, ALS storage, `res.on('finish')` for terminal status - **Fastify**: `src/fastify/index.ts`. Fastify hooks (`onRequest` / `onResponse` / `onError`), `fastify-plugin` wrapper - **Elysia**: `src/elysia/index.ts`. Plugin with `.derive({ as: 'global' })`, `storage.enterWith`-style ALS, streaming deferral - **NestJS**: `src/nestjs/index.ts`. `EvlogModule.forRoot()` / `forRootAsync()` on top of the manifest - **oRPC**: `src/orpc/index.ts`. `evlog()` procedure middleware + `withEvlog(handler)` wrapper - **React Router**: `src/react-router/index.ts`. `loggerContext = createContext()` - **SvelteKit**: `src/sveltekit/index.ts`. `evlog()` handle + `evlogHandleError()` + `createEvlogHooks()` - **Workers**: `src/workers/index.ts`. `defineWorkerFetch` / `withEvlog`, no ALS `useLogger()` (compat-flag constraint) For integrations that bind with `enterWith()`, use `createSharedEnterWithStorage` from `src/shared/asyncStorageScope.ts`. Its capability probe runs in a temporary scope with a distinct store, preserving the importer's async context on Bun and Node. ### Key Architecture Rules 1. **Prefer `defineFrameworkIntegration`**: it handles header normalization, request-id generation, ALS, fork attachment, and `waitUntil`. 2. **Status / error reporting stays framework-side**: call `finish({ status })` on success and `finish({ error })` on failure. `finish` runs emit + enrich + drain + plugin hooks. 3. **Re-throw errors** after `finish({ error })` so the framework's own error handler still runs. 4. **Streaming responses**: if the framework can return streaming bodies, defer the emit until the stream closes (`shouldDeferEmitForResponse`; see Hono and Elysia). 5. **Framework SDK is an optional peer dependency**: never bundle it. 6. **Never duplicate pipeline logic**: `runEnrichAndDrain` is internal to `createMiddlewareLogger`/`finish`. 7. **Export type helpers** for typed context access (e.g., `EvlogVariables` for Hono). ### When to fall back to custom mode Use `createMiddlewareLogger` directly (skipping `defineFrameworkIntegration`) when: - The middleware doesn't have a clear "request entry / response exit" pair (Next.js App Router server actions, Eve agent turns). - Logger creation spans multiple lifecycle phases owned by a module system (Nitro plugins + hooks). - The status is not knowable until after the response stream completes and the framework gives you no hook for it. ## Step 2: Build Config Add a build entry in `packages/evlog/tsdown.config.ts`: ```typescript '{framework}/index': 'src/{framework}/index.ts', ``` Also add the framework SDK to the `external` array (e.g., `'elysia'`, `'fastify'`). ## Step 3: Package Exports In `packages/evlog/package.json`: **In `exports`** (after the last framework entry): ```json "./{framework}": { "types": "./dist/{framework}/index.d.mts", "import": "./dist/{framework}/index.mjs" } ``` **In `typesVersions["*"]`**: `"{framework}": ["./dist/{framework}/index.d.mts"]` **In `peerDependencies`** (version range) + **`peerDependenciesMeta`** (`"optional": true`), and add the framework name to `keywords`. Exports without matching `tsdown.config.ts` entries fail `test/toolkit/api-surface.test.ts`. ## Step 4: Tests Create `packages/evlog/test/frameworks/{framework}.test.ts`. Read `packages/evlog/test/README.md` first, especially the **Framework runtime fidelity** table. Two non-negotiables: 1. **Real request driver.** Use the framework's own driver: supertest (Express/NestJS), `app.request()` (Hono), `app.inject()` (Fastify), `app.handle(new Request(...))` (Elysia). If no Node-friendly driver exists, call the user-facing contract directly with realistic input shapes (see the SvelteKit and React Router tests). Never extract internals to test a substitute. 2. **Wire the shared matrix.** Call `describeStandardHttpMatrix({ name, mount })` from `test/helpers/frameworkMatrix.ts`. It covers the standard sweep (event emission, `x-request-id`, route service) for every HTTP framework. On top of the matrix, cover the framework-specific surface: 1. Framework-native accessor returns the logger (`c.get('log')`, `req.log`, …) 2. Error handling. Errors captured, event has error level + details, error re-thrown 3. Route filtering. Skipped routes don't create a logger, skip drain/enrich 4. Context accumulation. `logger.set()` data appears in the emitted event 5. Drain / enrich / keep callbacks (use `createPipelineSpies()`, `assertHttpEventEmitted`, `waitForDrainCalls`, `findEventViaDrain` from `test/helpers/framework.ts`) 6. Drain/enrich error resilience. Errors there never break the request 7. `useLogger()`: same logger as the native accessor, works across async boundaries, throws outside context. Skip it for an integration without ALS, and test the accessor it ships instead: on Workers that is the handler's fourth argument, from `defineWorkerFetch` / `withEvlog` 8. Streaming (if applicable). Event deferred until the body closes Use fake timers for anything time-based; `defined()` instead of `!`. ## Step 5: Framework Docs Page Create `apps/docs/content/4.integrate/frameworks/{NN}.{framework}.md` with a comprehensive, self-contained guide. Check existing files for the next zero-padded number. **Frontmatter**: ```yaml --- 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/evloghq/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 the logger from services without passing the request, or, for an integration without ALS, the accessor it ships in its place 4. **Error Handling**: `createError()` + `parseError()` + framework error handler 5. **Drain & Enrichers**: middleware options with inline example 6. **Pipeline (Batching & Retry)**: `createDrainPipeline` example 7. **Tail Sampling**: `keep` callback 8. **Route Filtering**: `include` / `exclude` / `routes` 9. **Client-Side Logging**: HTTP drain (`evlog/http`) (only if the framework has a client-side story) 10. **Run Locally**: clone + `pnpm example {framework}` 11. **Card group** linking to GitHub source ## Step 6: Overview & Installation Cards **In `apps/docs/content/4.integrate/frameworks/00.overview.md`**: 1. Add a row to the **Overview table**: framework name, import, type, logger access, status 2. Add a card in the appropriate section, and a row in the **API cheat sheet** if the bootstrap/access pattern is new **In `apps/docs/content/1.start/3.installation.md`**: add a card in the "Choose Your Framework" `::card-group`, in the same order as the frameworks overview. ## Step 7: Landing Page In `apps/docs/content/0.landing.md`, find the `FeatureFrameworks` MDC section (slots `#nuxt`, `#nextjs`, `#hono`, …) and add a new slot: ```markdown #{framework} ```ts [src/index.ts] // Framework-specific code example showing evlog usage ``` ``` ## 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 `
` with `` in the template Icons use Simple Icons format: `i-simple-icons-{name}`. ## Step 9: Update the Public Skill In `skills/review-logging-patterns/SKILL.md` (published on evlog.dev): 1. Add `### {Framework}` in the **"Framework Setup"** section, in the same order as the docs 2. Include: import + `initLogger` + middleware setup; native logger access; a `useLogger()` snippet, or the accessor that replaces it when the integration has no ALS; full pipeline example (`drain`, `enrich`, `keep`) 3. Update the `description:` line in the YAML frontmatter to mention the new framework name ## Step 10: Update README In `packages/evlog/README.md` (root `README.md` is a symlink): 1. Add a `## {Framework}` section near the other framework sections with a minimal setup snippet and a link to the example app 2. Add a row to the **Framework Support** table Keep the snippet short: init, register middleware, one route handler showing logger access. ## Step 11: Example App Create `examples/{framework}/` with a runnable app demonstrating all evlog features. It is auto-discovered by the root runner: `pnpm example {framework}` (which loads the root `.env` via dotenv, so no root `package.json` change is needed). 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, or the integration's own accessor when it has no ALS 4. **Error route**: `createError()` 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`). `POSTHOG_API_KEY` is set in the root `.env` (maintainer's key, not committed), so every example exercises a real external drain. Without the env var the drain resolves to `null` and skips, so someone cloning the repo sends nothing anywhere unless they opt in with their own key. Enable pretty printing for readable local output. **Type the `enrich` callback parameter explicitly**, as `(ctx: EnrichContext) => ...` with `type EnrichContext` imported from `evlog`. ### Test UI Reference: `examples/hono/src/ui.ts`, a single `src/ui.ts` exporting `testUI()` returning a self-contained dark-theme HTML string (route list with method badges, click-to-fetch, JSON display, status colors, response time). Register the `/` route **before** the evlog middleware so it isn't logged. ### Required files | File | Purpose | |------|---------| | `src/index.ts` | App with all features demonstrated | | `src/ui.ts` | Test UI | | `package.json` | `dev` and `start` scripts (`bun --watch src/index.ts` / `bun src/index.ts`) | | `tsconfig.json` | TypeScript config (if needed) | | `README.md` | How to run + link to the UI | There is also `examples/community-framework-skeleton/` showing the community-facing (toolkit-only) variant, so keep it in mind if the new integration changes the toolkit contract. ## Step 12: Changeset Create `.changeset/{framework}-integration.md`: ```markdown --- "evlog": minor --- feat({framework}): add {Framework} middleware integration (`evlog/{framework}`) with automatic wide-event logging, drain, enrich, and tail sampling support ``` ## Step 13: PR Scopes Add `{framework}` to the `scopes` list in `.github/workflows/semantic-pull-request.yml` **and** to the Scopes section of `.github/pull_request_template.md`, in alphabetical order. Remember the timing caveat from the PR Title section: this registration only takes effect for PRs whose base branch already contains it. ## Verification After a clean install, run `pnpm run dev:prepare` from the repo root first, then: ```bash pnpm run dev:prepare cd packages/evlog pnpm run build # required before test — api-surface snapshot is gated on dist/ pnpm run test pnpm run lint ``` Then type-check the example: ```bash cd examples/{framework} pnpm exec tsc --noEmit ```