@hugorcd/create-framework-integration

Logging that makes sense. Wide events, structured errors, zero chaos.

View in AI SkillSafe app
3171 downloads
0 stars
0 demos
SKILL.md
namecreate-evlog-framework-integration
descriptionCreate 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.

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 apps/docs/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).

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)

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<AuditableLogger>()
  • SvelteKit: src/sveltekit/index.ts. evlog() handle + evlogHandleError() + createEvlogHooks()
  • Workers: src/workers/index.ts. defineWorkerFetch / withEvlog, no ALS useLogger() (compat-flag constraint)

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:

'{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):

"./{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:

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

  #{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 `<div v-if="activeTab === {N}">` with `<slot name="{framework}" />` in the template

Icons use Simple Icons format: `i-simple-icons-{name}`.

## Step 9: Update the Public Skill

In `apps/docs/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:

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:

cd examples/{framework}
pnpm exec tsc --noEmit

Embed badges

Add these to your README to show the skill's verification status.

SkillSafe verified badge
Verified badge
[![SkillSafe verified badge](https://api.skillsafe.ai/v1/badge/@hugorcd/create-framework-integration/verified)](https://skillsafe.ai/skill/@hugorcd/create-framework-integration/)
Installs badge
Installs badge
[![Installs badge](https://api.skillsafe.ai/v1/badge/@hugorcd/create-framework-integration/installs)](https://skillsafe.ai/skill/@hugorcd/create-framework-integration/)
Scan badge
Scan badge
[![Scan badge](https://api.skillsafe.ai/v1/badge/@hugorcd/create-framework-integration/scan)](https://skillsafe.ai/skill/@hugorcd/create-framework-integration/)
Eval pass rate badge
Eval pass rate
[![Eval pass rate badge](https://api.skillsafe.ai/v1/badge/@hugorcd/create-framework-integration/eval)](https://skillsafe.ai/skill/@hugorcd/create-framework-integration/)