create-evlog-framework-integration
About create-evlog-framework-integration
A repository Skill for contributors adding framework integrations to evlog. It explains when to use the shared manifest approach or custom middleware, and details the required API, tests, package changes, and documentation work.
Key features
- 01Manifest and custom integration paths
- 02Shared middleware and storage utilities
- 03Required integration API surface
- 04Framework-specific testing guidance
- 05Build, package, and documentation checklist
Use cases
- 01Adding evlog support for a framework
- 02Choosing an integration architecture
- 03Preparing integration tests and package changes
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ evloghq/ evlog --skill create-evlog-framework-integrationAfter installing, type/create-evlog-framework-integrationin Claude Code to confirm it loaded.
Details
From the project READMECreate 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 callsintegration.start(ctx, options)and runs the framework'snext()insiderunWith. Reference implementations: all ofpackages/evlog/src/{hono,express,fastify,elysia,nestjs,orpc,react-router,sveltekit,workers}/index.tsuse it. - Custom mode: use
createMiddlewareLoggerdirectly 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:
evlog()middleware/plugin accepting the fullBaseEvlogOptions(drain,enrich,keep,include,exclude,routes,plugins)useLogger()(ALS-backed). Two sanctioned exceptions: Workers (ALS needs a compat flag there;defineWorkerFetchattaches the logger instead) and Effect (the logger is theEvlogLoggerservice in the Effect context)log.fork()support (automatic whenstorageis provided to the manifest)- 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).
| 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
Headersand NodeIncomingHttpHeaders(so you don't need to pick a header extractor) - generates a
requestIdwhen none is present - calls
createMiddlewareLoggerand surfaces its{ logger, finish, skipped, middlewareOptions } - attaches
log.fork()automatically whenstorageis provided - exposes
runWith(fn)to run downstream handlers inside the integration's ALS - forwards
waitUntilwhen 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)+ ALSuseLogger(), streaming deferral viashouldDeferEmitForResponse,waitUntildetection - 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-pluginwrapper - 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. On@nestjs/platform-fastifythe middleware only sees the raw request, so apreHandlerhook (registered throughHttpAdapterHost) copies the logger ontoFastifyRequest.log - 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 ALSuseLogger()(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
- Prefer
defineFrameworkIntegration: it handles header normalization, request-id generation, ALS, fork attachment, andwaitUntil. - Status / error reporting stays framework-side: call
finish({ status })on success andfinish({ error })on failure.finishruns emit + enrich + drain + plugin hooks. - Re-throw errors after
finish({ error })so the framework's own error handler still runs. - Streaming responses: if the framework can return streaming bodies, defer the emit until the stream closes (
shouldDeferEmitForResponse; see Hono and Elysia). - Framework SDK is an optional peer dependency: never bundle it.
- Never duplicate pipeline logic:
runEnrichAndDrainis internal tocreateMiddlewareLogger/finish. - Export type helpers for typed context access (e.g.,
EvlogVariablesfor 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",
"default": "./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.
test/toolkit/entrypoint-registry.test.ts fails when an export has no matching typesVersions["*"] or tsdown.config.ts entry, or when its import and default do not point at the .mjs next to types. It runs without a build.
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:
- 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. - Wire the shared matrix. Call
describeStandardHttpMatrix({ name, mount })fromtest/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:
- Framework-native accessor returns the logger (
c.get('log'),req.log, …) - Error handling. Errors captured, event has error level + details, error re-thrown
- Route filtering. Skipped routes don't create a logger, skip drain/enrich
- Context accumulation.
logger.set()data appears in the emitted event - Drain / enrich / keep callbacks (use
createPipelineSpies(),assertHttpEventEmitted,waitForDrainCalls,findEventViaDrainfromtest/helpers/framework.ts) - Drain/enrich error resilience. Errors there never break the request
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, fromdefineWorkerFetch/withEvlog- 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/evloghq/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 the logger from services without passing the request, or, for an integration without ALS, the accessor it ships in its place
- Error Handling:
createError()+parseError()+ framework error handler - Drain & Enrichers: middleware options with inline example
- Pipeline (Batching & Retry):
createDrainPipelineexample - Tail Sampling:
keepcallback - Route Filtering:
include/exclude/routes - Client-Side Logging: HTTP drain (
evlog/http) (only if the framework has a client-side story) - Run Locally: clone +
pnpm example {framework} - Card group linking to GitHub source
Step 6: Overview & Installation Cards
In apps/docs/content/4.integrate/frameworks/00.overview.md:
- Add a row to the Overview table: framework name, import, type, logger access, status
- 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 `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