From 16e8ffe5f0189f9652c2e8a8172e45337f030c77 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Sat, 15 Aug 2026 13:59:53 +0100 Subject: [PATCH 1/2] docs: replace the dashes in every list item with the punctuation each one needed --- .agents/skills/create-adapter/SKILL.md | 16 ++-- .../references/adapter-template.md | 6 +- .../references/test-template.md | 12 +-- .agents/skills/create-enricher/SKILL.md | 18 ++--- .../references/enricher-template.md | 14 ++-- .../create-framework-integration/SKILL.md | 76 +++++++++---------- .agents/skills/create-map-rule/SKILL.md | 12 +-- .../references/corrections.md | 6 ++ .../references/rules/universal.md | 2 +- AGENTS.md | 48 ++++++------ apps/docs/AGENTS.md | 6 +- apps/docs/content/1.start/1.introduction.md | 2 +- apps/docs/content/1.start/2.why-evlog.md | 8 +- apps/docs/content/1.start/4.quick-start.md | 2 +- apps/docs/content/2.learn/0.overview.md | 2 +- apps/docs/content/2.learn/6.redaction.md | 12 +-- apps/docs/content/3.cli/1.init.md | 8 +- apps/docs/content/3.cli/2.map.md | 4 +- apps/docs/content/3.cli/3.rules.md | 2 +- apps/docs/content/3.cli/8.agents.md | 6 +- apps/docs/content/4.integrate/0.overview.md | 2 +- .../4.integrate/adapters/cloud/05.datadog.md | 6 +- .../4.integrate/adapters/hybrid/03.otlp.md | 2 +- .../frameworks/12.cloudflare-workers.md | 2 +- .../5.use-cases/3.better-auth/01.overview.md | 2 +- .../5.use-cases/4.audit/05.compliance.md | 6 +- .../5.use-cases/4.telemetry/01.overview.md | 20 ++--- .../5.use-cases/4.telemetry/04.reference.md | 4 +- apps/docs/content/5.use-cases/5.eve.md | 4 +- apps/docs/content/6.extend/1.stream.md | 6 +- .../content/6.extend/10.custom-framework.md | 8 +- .../6.extend/11.diagnostics-channel.md | 2 +- apps/docs/content/6.extend/2.fs-reader.md | 4 +- .../content/6.extend/3.consumer-recipes.md | 6 +- apps/docs/content/6.extend/4.plugins.md | 2 +- .../content/6.extend/5.custom-enrichers.md | 2 +- apps/docs/content/6.extend/6.tail-sampling.md | 2 +- apps/docs/content/6.extend/8.custom-drains.md | 14 ++-- .../docs/content/6.extend/9.drain-pipeline.md | 14 ++-- .../content/7.reference/1.configuration.md | 2 +- apps/docs/skills/analyze-logs/SKILL.md | 4 +- apps/docs/skills/build-audit-logs/SKILL.md | 12 +-- .../references/framework-wiring.md | 2 +- .../references/structured-errors.md | 2 +- packages/cli/README.md | 2 +- packages/evlog/README.md | 2 +- scripts/content-lint/lib/metrics.mjs | 10 ++- scripts/content-lint/lib/metrics.test.mjs | 6 +- 48 files changed, 213 insertions(+), 199 deletions(-) diff --git a/.agents/skills/create-adapter/SKILL.md b/.agents/skills/create-adapter/SKILL.md index 878167ae..d7c949ca 100644 --- a/.agents/skills/create-adapter/SKILL.md +++ b/.agents/skills/create-adapter/SKILL.md @@ -63,14 +63,14 @@ Create `packages/evlog/src/adapters/{name}.ts`. Read [references/adapter-templat The contract is `defineHttpDrain({ name, label, resolve, encode })`. You only ship two pieces of logic: -1. **`resolve()`** — produce a fully-resolved config or `null` to skip. Use `resolveAdapterConfig` for the standard precedence (overrides → `runtimeConfig.evlog.{name}` → `runtimeConfig.{name}` → env vars). List `NUXT_{NAME}_*` before `{NAME}_*` in `ConfigField.env` for silent Nuxt compat; show only `{NAME}_*` in user-facing messages via `formatPublicEnvKeys`. -2. **`encode(events, config)`** — a private `encode{Name}Request(events, config): HttpDrainRequest` returning `{ url, headers, body }` for a batch. HTTP transport, identity headers, retries, timeout, and error logging are handled by `defineHttpDrain` (via `httpPost`). +1. **`resolve()`**: produce a fully-resolved config or `null` to skip. Use `resolveAdapterConfig` for the standard precedence (overrides → `runtimeConfig.evlog.{name}` → `runtimeConfig.{name}` → env vars). List `NUXT_{NAME}_*` before `{NAME}_*` in `ConfigField.env` for silent Nuxt compat; show only `{NAME}_*` in user-facing messages via `formatPublicEnvKeys`. +2. **`encode(events, config)`**: a private `encode{Name}Request(events, config): HttpDrainRequest` returning `{ url, headers, body }` for a batch. HTTP transport, identity headers, retries, timeout, and error logging are handled by `defineHttpDrain` (via `httpPost`). Key rules: - **Single factory.** Export one `create{Name}Drain(overrides?: Partial<{Name}Config>)`. No dual-API factories: if a service has multiple ingest modes (logs vs events), expose them via a `mode` option (see PostHog). -- **No HTTP code in the adapter.** Never call `fetch` directly. If the service truly needs custom transport (binary envelopes, non-HTTP), use `defineDrain` from `../shared/drain` instead — see `fs.ts` and `memory.ts`. -- **Encode parity.** The standalone `sendTo{Name}` / `sendBatchTo{Name}` helpers must reuse the same private `encode{Name}Request()` and go through `sendEncodedDrainRequest(request, { label, source, timeout, retries })` — never a separate fetch path. `test/adapters/encode-parity.test.ts` pins this for a subset of adapters; add the new one to it (not every existing adapter is registered there yet — that's a gap, not a license to skip). +- **No HTTP code in the adapter.** Never call `fetch` directly. If the service truly needs custom transport (binary envelopes, non-HTTP), use `defineDrain` from `../shared/drain` instead, see `fs.ts` and `memory.ts`. +- **Encode parity.** The standalone `sendTo{Name}` / `sendBatchTo{Name}` helpers must reuse the same private `encode{Name}Request()` and go through `sendEncodedDrainRequest(request, { label, source, timeout, retries })`, never a separate fetch path. `test/adapters/encode-parity.test.ts` pins this for a subset of adapters; add the new one to it (not every existing adapter is registered there yet, and that is a gap, not a license to skip). - **No bespoke config resolution.** Always go through `resolveAdapterConfig`. Deprecated aliases (`token` → `apiKey`) go through `applyDeprecatedAlias`. - **Exported converters.** If the service needs a specific event shape, export `to{Name}Event()` / `build{Name}Payload()` helpers so they're testable independently. - **Edge-safe.** Adapters run on Cloudflare Workers: no `Buffer` (use `TextEncoder` + `btoa`, see `loki.ts`), no Node-only APIs. `fs.ts` shows the `isEdgeRuntime()` guard pattern when a runtime genuinely can't be supported. @@ -114,7 +114,7 @@ Create `packages/evlog/test/adapters/{name}.test.ts`. Read [references/test-temp Non-negotiables from the test README: -- Use `mockFetch()` / `getFetchCall` / `getFetchJson` / `getFetchHeaders` from `test/helpers/fetch.ts` — never hand-roll `vi.spyOn(globalThis, 'fetch')` boilerplate. +- Use `mockFetch()` / `getFetchCall` / `getFetchJson` / `getFetchHeaders` from `test/helpers/fetch.ts`, never hand-roll `vi.spyOn(globalThis, 'fetch')` boilerplate. - Clean up any env vars the adapter reads in `afterEach`. - Test the exported pure helpers (`to{Name}Event`, `build{Name}Payload`, URL resolvers) directly, one `describe` per helper. @@ -137,7 +137,7 @@ If the service is self-hostable, extend the local sandbox so the adapter can be - `packages/evlog/test/e2e/docker-compose.yml`: add the service - `packages/evlog/test/e2e/seed.mjs`: fan the seeder out to the new backend - `packages/evlog/test/e2e/README.md`: document it -- Root `package.json` `sandbox:e2e` script — add the local env var if needed +- Root `package.json` `sandbox:e2e` script: add the local env var if needed See the Loki and ClickHouse setups as references. @@ -161,8 +161,8 @@ Create `{NN}.{name}.md` in the right category with the next available number. Us Edit `apps/docs/content/4.integrate/adapters/01.overview.md` in **two** places (follow the pattern of existing adapters): -1. **Frontmatter `links` array** — add a link entry with icon and `/integrate/adapters/{category}/{name}` path, in category order -2. **`::card-group` section** — add a card block in the matching position +1. **Frontmatter `links` array**: add a link entry with icon and `/integrate/adapters/{category}/{name}` path, in category order +2. **`::card-group` section**: add a card block in the matching position ## Step 8: Update the Public Skill diff --git a/.agents/skills/create-adapter/references/adapter-template.md b/.agents/skills/create-adapter/references/adapter-template.md index 87f3a970..f8519331 100644 --- a/.agents/skills/create-adapter/references/adapter-template.md +++ b/.agents/skills/create-adapter/references/adapter-template.md @@ -140,8 +140,8 @@ export async function sendBatchTo{Name}(events: WideEvent[], config: {Name}Confi ## Customization Notes -- **Auth style**: Some services use `Authorization: Bearer`, others a custom header (`X-API-Key`, ClickHouse's `X-ClickHouse-User`/`X-ClickHouse-Key`) or HTTP Basic (Loki + Grafana Cloud). Adjust `encode{Name}Request` — and prefer headers over query params so credentials never land in server-side query logs. +- **Auth style**: Some services use `Authorization: Bearer`, others a custom header (`X-API-Key`, ClickHouse's `X-ClickHouse-User`/`X-ClickHouse-Key`) or HTTP Basic (Loki + Grafana Cloud). Adjust `encode{Name}Request`, and prefer headers over query params so credentials never land in server-side query logs. - **Payload format**: Raw JSON arrays (Axiom), wrapper objects (PostHog `{ api_key, batch }`), protocol structures (OTLP), NDJSON-style bodies (ClickHouse `JSONEachRow`). Adapt the encoder; export intermediate builders (`build{Name}Payload`) when the transformation is non-trivial. -- **Non-HTTP transport**: If the service cannot fit `defineHttpDrain`, use `defineDrain({ name, resolve, send })` — see `fs.ts` and `memory.ts`. +- **Non-HTTP transport**: If the service cannot fit `defineHttpDrain`, use `defineDrain({ name, resolve, send })`, see `fs.ts` and `memory.ts`. - **Deprecated aliases**: When renaming a config field (e.g. `token` → `apiKey`), keep both as `ConfigField` entries and map via `applyDeprecatedAlias(config, { adapter, from, to })` from `../shared/config`. See `axiom.ts` and `better-stack.ts`. -- **Edge safety**: no `Buffer` (use `TextEncoder` + `btoa` for Basic auth — see `toBasicCredentials` in `loki.ts`), no Node-only imports. If a runtime genuinely can't be supported, return `null` from `resolve()` with a one-time warning (see `isEdgeRuntime()` in `fs.ts`). +- **Edge safety**: no `Buffer` (use `TextEncoder` + `btoa` for Basic auth, see `toBasicCredentials` in `loki.ts`), no Node-only imports. If a runtime genuinely can't be supported, return `null` from `resolve()` with a one-time warning (see `isEdgeRuntime()` in `fs.ts`). diff --git a/.agents/skills/create-adapter/references/test-template.md b/.agents/skills/create-adapter/references/test-template.md index eda712cd..2fa3d85d 100644 --- a/.agents/skills/create-adapter/references/test-template.md +++ b/.agents/skills/create-adapter/references/test-template.md @@ -6,10 +6,10 @@ Replace `{Name}`, `{name}`, `{NAME}` with the actual service name. Rules from the test README that apply here: -- Use `mockFetch()` + `getFetchCall` / `getFetchJson` / `getFetchHeaders` from `../helpers/fetch` — don't hand-roll fetch spies in adapter tests (a few older files still do; follow the helpers, not them). -- Delete every env var the adapter reads in `afterEach` — leaked env vars make later tests order-dependent. -- Test exported pure helpers (`to{Name}Event`, `build{Name}Payload`, URL resolvers) in their own `describe` blocks — but only the ones the adapter actually exports. If the adapter has no converter (service accepts arbitrary JSON), drop the `to{Name}Event` import and its `describe` block entirely. -- No `!` non-null assertions — use `defined()` from `../helpers/defined` if narrowing is needed. +- Use `mockFetch()` + `getFetchCall` / `getFetchJson` / `getFetchHeaders` from `../helpers/fetch`, don't hand-roll fetch spies in adapter tests (a few older files still do; follow the helpers, not them). +- Delete every env var the adapter reads in `afterEach`. Leaked env vars make later tests order-dependent. +- Test exported pure helpers (`to{Name}Event`, `build{Name}Payload`, URL resolvers) in their own `describe` blocks, but only the ones the adapter actually exports. If the adapter has no converter (service accepts arbitrary JSON), drop the `to{Name}Event` import and its `describe` block entirely. +- No `!` non-null assertions, use `defined()` from `../helpers/defined` if narrowing is needed. - Register the adapter in `encode-parity.test.ts` so the drain and `sendBatchTo{Name}` are pinned to the same encoder (not every existing adapter is registered there yet; new ones should be). ```typescript @@ -160,9 +160,9 @@ describe('{name} adapter', () => { - **URL assertions**: Update expected URLs to the actual service API, including the path-already-present case if the encoder tolerates it (see `resolveLokiPushUrl`). - **Auth headers**: Match the service (`X-API-Key`, HTTP Basic, `X-ClickHouse-User`, …). -- **Body format**: Wrapper objects (PostHog `{ api_key, batch }`), raw arrays (Axiom), NDJSON (ClickHouse) — assert the real structure, not just "is an array". +- **Body format**: Wrapper objects (PostHog `{ api_key, batch }`), raw arrays (Axiom), NDJSON (ClickHouse). Assert the real structure, not just "is an array". - **Deprecated aliases**: If the adapter supports one (`token` → `apiKey`), add a test that the alias still resolves and that the canonical name wins when both are set. -- **Error swallowing**: The drain itself never throws — that contract lives in `defineHttpDrain` and is covered by `test/toolkit/toolkit.test.ts`; don't re-test it per adapter. Only direct helpers surface errors. +- **Error swallowing**: The drain itself never throws. That contract lives in `defineHttpDrain` and is covered by `test/toolkit/toolkit.test.ts`; don't re-test it per adapter. Only direct helpers surface errors. - **Service-specific helpers**: Every exported helper (`buildLokiPayload`, `toClickHouseRow`, severity mappers…) gets its own `describe` with edge cases (empty input, malformed timestamps, cardinality guards). ## Beyond unit tests diff --git a/.agents/skills/create-enricher/SKILL.md b/.agents/skills/create-enricher/SKILL.md index 6be20a63..c2dfee64 100644 --- a/.agents/skills/create-enricher/SKILL.md +++ b/.agents/skills/create-enricher/SKILL.md @@ -57,9 +57,9 @@ The contract is `defineEnricher({ name, field, compute }, options?)`. You onl Key rules: -- **Use the toolkit helpers**: `getHeader()` for case-insensitive header lookup, `normalizeNumber()` for numeric strings — both from `../shared/headers` (re-exported by `evlog/toolkit`). -- **Single event field**: each enricher writes one top-level field on `ctx.event`. If the enricher must additionally pin top-level fields (like `createTraceContextEnricher` does for `event.traceId` / `event.spanId`), wrap the `defineEnricher` result in a closure — see that enricher for the pattern. -- **Factory pattern**: `create{Name}Enricher(options: EnricherOptions = {})` returns the result of `defineEnricher(...)` — directly in the normal case, or through the thin closure wrapper when the enricher also pins top-level fields (see the single-event-field rule above). +- **Use the toolkit helpers**: `getHeader()` for case-insensitive header lookup, `normalizeNumber()` for numeric strings. Both from `../shared/headers` (re-exported by `evlog/toolkit`). +- **Single event field**: each enricher writes one top-level field on `ctx.event`. If the enricher must additionally pin top-level fields (like `createTraceContextEnricher` does for `event.traceId` / `event.spanId`), wrap the `defineEnricher` result in a closure, see that enricher for the pattern. +- **Factory pattern**: `create{Name}Enricher(options: EnricherOptions = {})` returns the result of `defineEnricher(...)`, directly in the normal case, or through the thin closure wrapper when the enricher also pins top-level fields (see the single-event-field rule above). - **No side effects**: never throw, never log; rely on `defineEnricher`'s built-in error handling if something goes wrong. - **Export the Info type**: `{Name}Info` describing the field shape, exported alongside the factory. @@ -69,12 +69,12 @@ Add tests to `packages/evlog/test/toolkit/enrichers.test.ts`, following the exis Required test categories: -1. **Sets field from headers** — verify the enricher populates the event field correctly -2. **Skips when source data missing** — verify no field is set when the required header/input is absent -3. **Preserves existing data** — verify `overwrite: false` (default) doesn't replace user-provided fields -4. **Overwrites when requested** — verify `overwrite: true` replaces existing fields -5. **Handles edge cases** — empty strings, malformed values, case-insensitive header names -6. **Default composition** — if the enricher joined `createDefaultEnrichers()`, extend that composition's tests +1. **Sets field from headers**: verify the enricher populates the event field correctly +2. **Skips when source data missing**: verify no field is set when the required header/input is absent +3. **Preserves existing data**: verify `overwrite: false` (default) doesn't replace user-provided fields +4. **Overwrites when requested**: verify `overwrite: true` replaces existing fields +5. **Handles edge cases**: empty strings, malformed values, case-insensitive header names +6. **Default composition**: if the enricher joined `createDefaultEnrichers()`, extend that composition's tests ## Step 3: Update the Enrichers Docs Page diff --git a/.agents/skills/create-enricher/references/enricher-template.md b/.agents/skills/create-enricher/references/enricher-template.md index e16d2c3c..0bd607b0 100644 --- a/.agents/skills/create-enricher/references/enricher-template.md +++ b/.agents/skills/create-enricher/references/enricher-template.md @@ -47,13 +47,13 @@ export function create{Name}Enricher(options: EnricherOptions = {}): (ctx: Enric ## Architecture Rules 1. **Use the toolkit primitive**: `defineEnricher({ name, field, compute }, options)` from `../shared/enricher` (re-exported as `evlog/toolkit`). -2. **Use the toolkit helpers**: `getHeader()` for case-insensitive header lookup and `normalizeNumber()` for numeric strings — both from `../shared/headers`. -3. **Single event field** — each enricher writes one top-level field on `ctx.event` (declared via the `field` option). -4. **Return `undefined` to skip** — `compute` returning `undefined` makes the enricher a no-op for that event (no field merge, no errors). -5. **Factory pattern** — always wrap `defineEnricher` in a `create{Name}Enricher(options?)` factory and return its result (directly, or through the closure wrapper of rule 7 when pinning top-level fields). -6. **No try/catch** — `defineEnricher` already isolates errors (logs as `[evlog/{name}] enrich failed:` and never throws to the pipeline). -7. **No mutation outside `compute`** — let `defineEnricher` handle the merge via `mergeEventField`. The one sanctioned exception: pinning top-level fields in addition to the enricher's own field, done by wrapping the `defineEnricher` result in a closure (see `createTraceContextEnricher`, which also sets `event.traceId` / `event.spanId`). -8. **Composition** — to combine several enrichers into one callback, use `composeEnrichers` from `../shared/compose` (that's how `createDefaultEnrichers()` is built) instead of a manual loop. +2. **Use the toolkit helpers**: `getHeader()` for case-insensitive header lookup and `normalizeNumber()` for numeric strings. Both come from `../shared/headers`. +3. **Single event field**: each enricher writes one top-level field on `ctx.event` (declared via the `field` option). +4. **Return `undefined` to skip**: `compute` returning `undefined` makes the enricher a no-op for that event (no field merge, no errors). +5. **Factory pattern**: always wrap `defineEnricher` in a `create{Name}Enricher(options?)` factory and return its result (directly, or through the closure wrapper of rule 7 when pinning top-level fields). +6. **No try/catch**: `defineEnricher` already isolates errors (logs as `[evlog/{name}] enrich failed:` and never throws to the pipeline). +7. **No mutation outside `compute`**: let `defineEnricher` handle the merge via `mergeEventField`. The one sanctioned exception: pinning top-level fields in addition to the enricher's own field, done by wrapping the `defineEnricher` result in a closure (see `createTraceContextEnricher`, which also sets `event.traceId` / `event.spanId`). +8. **Composition**: to combine several enrichers into one callback, use `composeEnrichers` from `../shared/compose` (that's how `createDefaultEnrichers()` is built) instead of a manual loop. ## Available Helpers diff --git a/.agents/skills/create-framework-integration/SKILL.md b/.agents/skills/create-framework-integration/SKILL.md index 633be63d..8bd32a46 100644 --- a/.agents/skills/create-framework-integration/SKILL.md +++ b/.agents/skills/create-framework-integration/SKILL.md @@ -9,7 +9,7 @@ Add a new framework integration to evlog. The recommended path is the **manifest ## 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. +- **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). @@ -19,7 +19,7 @@ Manifest mode now covers all classic HTTP frameworks. Use custom mode only when 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) +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`, …) @@ -141,24 +141,24 @@ export function evlog(options: Evlog{Framework}Options = {}): FrameworkMiddlewar ### 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) +- **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) ### 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. +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`. +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 @@ -205,18 +205,18 @@ Create `packages/evlog/test/frameworks/{framework}.test.ts`. Read `packages/evlo 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. +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 +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 -8. Streaming (if applicable) — event deferred until the body closes +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 +8. Streaming (if applicable). Event deferred until the body closes Use fake timers for anything time-based; `defined()` instead of `!`. @@ -244,16 +244,16 @@ links: **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 the request -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}` +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 the request +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 @@ -309,11 +309,11 @@ Create `examples/{framework}/` with a runnable app demonstrating all evlog featu 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 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 +2. **Health route**: basic `log.set()` usage +3. **Data route**: context accumulation with user/business data, using `useLogger()` in a service function +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. diff --git a/.agents/skills/create-map-rule/SKILL.md b/.agents/skills/create-map-rule/SKILL.md index 1d33dc25..785e26f9 100644 --- a/.agents/skills/create-map-rule/SKILL.md +++ b/.agents/skills/create-map-rule/SKILL.md @@ -87,11 +87,11 @@ export const {camelId}Rule = { Key rules: - **Reporting nothing means the rule passed.** `context.report()` only for gaps. -- **Read `FileFacts` first** (`../facts.ts`) — if the answer isn't there, consider extending the facts rather than writing AST listeners; facts are computed once per file for all rules. +- **Read `FileFacts` first** (`../facts.ts`). If the answer isn't there, consider extending the facts rather than writing AST listeners; facts are computed once per file for all rules. - **`project` (`ProjectFacts`) is the gate for opportunities**: `project.features` (evlog features in use), `project.pairable` (installed packages evlog integrates with), `project.catalogs` (for naming things in suggestions). -- **Messages are report copy.** Concrete, lowercase, pointing at the evidence (`"X is spelled out here and in 2 other files — one catalog entry would cover them"`). No exclamation marks, no advice-column tone. +- **Messages are report copy.** Concrete, lowercase, pointing at the evidence (`"X is spelled out here and in 2 other files, and one catalog entry would cover them"`). No exclamation marks, no advice-column tone. - **Weights are a scoring decision**: look at `score.ts` and the existing spread (40 down to 15) and discuss the number in the PR rather than inventing precedent. -- Every rule id is also a suppression target (`evlog-map-disable {id}`) and part of the public `evlog.map.json` contract — renaming later is a breaking change. +- Every rule id is also a suppression target (`evlog-map-disable {id}`) and part of the public `evlog.map.json` contract. Renaming later is a breaking change. ## Steps 2 and 3: Registry + CheckId @@ -105,10 +105,10 @@ Cover at minimum: 1. The gap fires (with the message and line you expect) 2. The compliant version passes -3. The `n/a` boundaries — wrong `kind`, gated `when` returning false, `hasEvlog: false` phrasing if the rule branches on it -4. Opportunity gating — does NOT fire when the project doesn't use the feature +3. The `n/a` boundaries: wrong `kind`, gated `when` returning false, `hasEvlog: false` phrasing if the rule branches on it +4. Opportunity gating. Does NOT fire when the project doesn't use the feature 5. `suggest()` output when it adapts to the project (e.g. names an existing catalog) -6. Suppression (`evlog-map-disable {id}`) behaves like the other rules — usually free via the shared harness +6. Suppression (`evlog-map-disable {id}`) behaves like the other rules. Usually free via the shared harness Run: `pnpm --filter @evlog/cli exec vitest run test/map/rules.test.ts` diff --git a/.agents/skills/write-evlog-content/references/corrections.md b/.agents/skills/write-evlog-content/references/corrections.md index 3481b205..c14074a5 100644 --- a/.agents/skills/write-evlog-content/references/corrections.md +++ b/.agents/skills/write-evlog-content/references/corrections.md @@ -80,3 +80,9 @@ Applies to: every surface. `metrics.mjs` measures the share of sections that lis Flagged: nothing, for a year. The rule only ever read headings and paragraphs, so 273 dashes sat in list items untouched, most of them in the `Next steps` list at the bottom of a page. Actual: 159 were a bold term glossed after a dash, which the corpus elsewhere writes with a colon. The remaining 117 put a full clause after the dash and need a reader. Applies to: list items on every surface. Table cells stay out: a cell is a fragment and a dash between two of its parts is layout. + +## 2026-08-15 · U-14 · A dash between two numbers is a range + +Flagged: `~30–80 lines of glue`. +Actual: an en dash between two numbers is the mark that reads as a range, and no comma, colon or period replaces it. The rule was never about that dash. +Applies to: every surface. `metrics.mjs` ignores a dash with a digit on each side. diff --git a/.agents/skills/write-evlog-content/references/rules/universal.md b/.agents/skills/write-evlog-content/references/rules/universal.md index ead5a879..a05b25b6 100644 --- a/.agents/skills/write-evlog-content/references/rules/universal.md +++ b/.agents/skills/write-evlog-content/references/rules/universal.md @@ -119,7 +119,7 @@ Why: the admission is what makes the rest of the page believable, and evlog's co **U-14 · No em dashes, no en dashes** · `standard` -Rule: no `—` and no `–` in prose, in any language, on any surface. Hyphens in compound words are fine, and so are dashes inside code blocks and inside a verbatim quote. +Rule: no `—` and no `–` in prose, in any language, on any surface. Hyphens in compound words are fine, and so are dashes inside code blocks, inside a verbatim quote, and between two numbers, where the en dash is the mark that reads as a range (`~30–80 lines`). Bad: "The drain batches events, then retries with backoff, before it gives up." written as "The drain batches events — then retries with backoff — before it gives up." Better: "The drain batches events, retries with backoff, then gives up." Why: this is a maintainer decision about how evlog sounds, and it is the punctuation most associated with machine-written prose. Every occurrence is a finding, and there is no density threshold to argue about. diff --git a/AGENTS.md b/AGENTS.md index 59cb1a6c..f72e5e58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,10 +55,10 @@ scripts/ Repo tooling (run-app, cli-sandbox, release-notes, co - All code in TypeScript. Follow existing patterns in `packages/evlog/src/`. - JSDoc on all public APIs. - No HTML comments (``) in Vue templates. -- `README.md` at root is a **symlink** to `packages/evlog/README.md` — edit the source directly. +- `README.md` at root is a **symlink** to `packages/evlog/README.md`. Edit the source directly. - `evlog/toolkit` is the public entrypoint for `src/shared/`. Never use `evlog/shared`. -- `evlog/browser` is deprecated — use `evlog/http` instead. -- Every framework integration exposes the **same contract**: `evlog()` middleware, `useLogger()`, `log.fork()`, and the full `BaseEvlogOptions` surface. Framework-native accessors (`c.get('log')`, `req.log`, `event.locals.log`, `context.get(loggerContext)`) stay alongside it — they are the idiomatic path inside handlers, `useLogger()` is for the layers underneath. When adding an integration, provide both. +- `evlog/browser` is deprecated, use `evlog/http` instead. +- Every framework integration exposes the **same contract**: `evlog()` middleware, `useLogger()`, `log.fork()`, and the full `BaseEvlogOptions` surface. Framework-native accessors (`c.get('log')`, `req.log`, `event.locals.log`, `context.get(loggerContext)`) stay alongside it. They are the idiomatic path inside handlers, `useLogger()` is for the layers underneath. When adding an integration, provide both. - `useLogger()` is backed by `AsyncLocalStorage`. On Cloudflare Workers that needs the `nodejs_compat` / `nodejs_als` flag, so `evlog/workers` deliberately has no `useLogger()` and passes the logger as the handler's fourth argument instead. - New export? Update both `packages/evlog/package.json` exports and `packages/evlog/tsdown.config.ts`. - Creating a new adapter, enricher, or framework integration? Read the matching skill at `.agents/skills/` **before starting**: @@ -66,17 +66,17 @@ scripts/ Repo tooling (run-app, cli-sandbox, release-notes, co - `.agents/skills/create-enricher/SKILL.md` - `.agents/skills/create-framework-integration/SKILL.md` - `.agents/skills/create-map-rule/SKILL.md` (also covers new `evlog map` framework adapters) -- Writing or reviewing prose — a docs page, the landing, a blog post, a package README, a skill, an AGENTS.md, a changeset? Read `.agents/skills/write-evlog-content/SKILL.md` first, and run `pnpm content:lint ` before the review. It carries the voice, the atomic rules, the terminology, the competitor dossiers, and the AI-tell corpus with the legitimate twin for each tell. These files are content too: `pnpm content:lint --surface skill` and `--surface agents` rank them. -- **Skills must stay in sync with the code.** There are two sets: internal skills in `.agents/skills/` and published skills in `apps/docs/skills/` (served from the docs site via `.well-known/skills`). When a change touches something a skill documents — an adapter, enricher, integration, API surface, or workflow — update the affected SKILL.md (and its `references/`) in the same PR. A skill that describes the old behavior is worse than no skill. +- Writing or reviewing prose, a docs page, the landing, a blog post, a package README, a skill, an AGENTS.md, a changeset? Read `.agents/skills/write-evlog-content/SKILL.md` first, and run `pnpm content:lint ` before the review. It carries the voice, the atomic rules, the terminology, the competitor dossiers, and the AI-tell corpus with the legitimate twin for each tell. These files are content too: `pnpm content:lint --surface skill` and `--surface agents` rank them. +- **Skills must stay in sync with the code.** There are two sets: internal skills in `.agents/skills/` and published skills in `apps/docs/skills/` (served from the docs site via `.well-known/skills`). When a change touches something a skill documents (an adapter, enricher, integration, API surface, or workflow), update the affected SKILL.md (and its `references/`) in the same PR. A skill that describes the old behavior is worse than no skill. ### Code style: no slop -- **No gratuitous defensive code.** Don't add try/catch, null checks, or input validation the surrounding file doesn't have — especially on paths already validated upstream. Match the file's level of paranoia. +- **No gratuitous defensive code.** Don't add try/catch, null checks, or input validation the surrounding file doesn't have, especially on paths already validated upstream. Match the file's level of paranoia. - **No silent fallbacks.** No empty `catch`, no `?? default` that masks a bug, no `as any` to silence TypeScript. If something can fail, let it fail loudly or handle it explicitly. - **Comments are rare and earn their place.** Only for constraints the code can't express (a protocol quirk, a deliberate perf trade-off). Never paraphrase the code, never narrate a change. When in doubt: no comment. -- **A comment states a durable constraint, not the moment you wrote it.** One or two lines. No issue ids, no measurements, no before/after story, no "I found that…" — that belongs in the PR body, the changeset, or a doc. Code outlives the task that produced it; a paragraph pinned to last Tuesday's investigation reads as noise six months later and nobody dares delete it. -- **This extends to all prose**: test names, error/log messages, changeset descriptions, PR bodies. Factual and plain — no emoji, no superlatives, no filler. -- **No speculative code.** No unrequested options or parameters, no "just in case" branches, no keeping the old code path alongside the new one. Delete dead code; public API deprecations are a maintainer decision — ask first. +- **A comment states a durable constraint, not the moment you wrote it.** One or two lines. No issue ids, no measurements, no before/after story, no "I found that…". That belongs in the PR body, the changeset, or a doc. Code outlives the task that produced it; a paragraph pinned to last Tuesday's investigation reads as noise six months later and nobody dares delete it. +- **This extends to all prose**: test names, error/log messages, changeset descriptions, PR bodies. Factual and plain, no emoji, no superlatives, no filler. +- **No speculative code.** No unrequested options or parameters, no "just in case" branches, no keeping the old code path alongside the new one. Delete dead code; public API deprecations are a maintainer decision. Ask first. - **Prefer deleting and simplifying over working around.** If the fix needs a workaround, question the design before adding the workaround. ### Changesets @@ -86,7 +86,7 @@ scripts/ Repo tooling (run-app, cli-sandbox, release-notes, co - **When to add a changeset:** any change that affects the public API, adds a feature, fixes a bug, or introduces a breaking change. If a consumer of evlog would notice the difference, it needs a changeset. - **When you can skip:** internal-only changes (CI config, docs typos, test refactors, devDeps bumps) that don't touch the published package. - **Bump type:** `patch` for fixes, `minor` for features, `major` for breaking changes. -- **Description:** write from the consumer's perspective — what changed and how to use it. See existing changesets in `.changeset/` for tone and level of detail. +- **Description:** write from the consumer's perspective: what changed and how to use it. See existing changesets in `.changeset/` for tone and level of detail. A PR without a changeset for a user-facing change will not be merged. Changes confined to `apps/*` or `examples/*`, docs included, never need one. For the rare published-package change that genuinely needs no release note, run `pnpm changeset add --empty`. @@ -94,7 +94,7 @@ A PR without a changeset for a user-facing change will not be merged. Changes co PR titles and commits follow [Conventional Commits](https://conventionalcommits.org). The CI source of truth is `.github/workflows/semantic-pull-request.yml` (lints PR titles via `amannn/action-semantic-pull-request`); `.github/pull_request_template.md` mirrors the same lists for contributors. -- **Subject must not start with an uppercase letter.** `feat: add stream server` ✓ — `feat: Add stream server` ✗. +- **Subject must not start with an uppercase letter.** `feat: add stream server` ✓. `feat: Add stream server` ✗. - **Omit the scope when the change is cross-cutting** (touches multiple subsystems, or is repo-wide). Don't use `evlog` as a scope: the whole monorepo *is* evlog, so a no-scope title already means "evlog itself". - **Use a scope only to point at one subsystem.** Adapters get their own scope (one per entrypoint, e.g. `axiom`, `datadog`, `fs`); framework integrations get the framework's name (`nuxt`, `next`, `hono`, ...); core internals (logger, pipeline, error, redact, catalog) go under `core`. - **When you add a new subsystem** (adapter, integration, top-level entrypoint), add its scope to **both** the workflow and the template. Keep both lists alphabetically sorted. Because title validation reads the base branch's scope list, either register the scope in a preceding PR or omit the scope from the subsystem PR title. @@ -122,7 +122,7 @@ Rules: 1. Every change has a matching test. Bug fixes require a *failing* regression test before the fix. 2. Always import real source helpers, never re-implement them in tests. 3. Use the helpers in `test/helpers/` (drain spies, fake timers, fetch mock, framework matrix). The full decision table is in `test/README.md`. -4. Framework tests must use the framework's real request driver (supertest, `app.inject`, `app.handle`, `Test.createTestingModule`, ...) — see the fidelity matrix in `test/README.md`. +4. Framework tests must use the framework's real request driver (supertest, `app.inject`, `app.handle`, `Test.createTestingModule`, ...), see the fidelity matrix in `test/README.md`. ## Definition of Done @@ -141,19 +141,19 @@ A task is complete when **all** of the following pass: **Always do:** - Run lint, typecheck, and test before reporting done -- Follow existing code patterns — read neighboring files before writing new ones +- Follow existing code patterns: read neighboring files before writing new ones - Use the skills at `.agents/skills/` for new adapters, enrichers, or integrations -- Add a changeset (`pnpm changeset`) for every user-facing change — features, bug fixes, breaking changes +- Add a changeset (`pnpm changeset`) for every user-facing change: features, bug fixes, breaking changes **Ask first:** -- Adding new dependencies — note `pnpm-workspace.yaml` sets `minimumReleaseAge: 2880`: a package published less than 48h ago fails to install unless added to `minimumReleaseAgeExclude` +- Adding new dependencies: note `pnpm-workspace.yaml` sets `minimumReleaseAge: 2880`: a package published less than 48h ago fails to install unless added to `minimumReleaseAgeExclude` - Changing package exports or build config - Architectural decisions that affect multiple packages **Never:** - Commit secrets, `.env` files, or API keys - Skip tests or lint to "fix later" -- Loosen an assertion, widen a type, or delete a test to make it pass — a failing test is a signal; fix the cause +- Loosen an assertion, widen a type, or delete a test to make it pass: a failing test is a signal; fix the cause - Ship a feature, bug fix, or refactor without a matching test - Add HTML comments in Vue `