diff --git a/.agents/skills/write-evlog-content/references/corrections.md b/.agents/skills/write-evlog-content/references/corrections.md index 01d670b7..6e82cd24 100644 --- a/.agents/skills/write-evlog-content/references/corrections.md +++ b/.agents/skills/write-evlog-content/references/corrections.md @@ -92,3 +92,27 @@ Applies to: every surface. `metrics.mjs` ignores a dash with a digit on each sid Flagged: `Where the byte counts come from`, `Which number moves your bill`, `Try it against your numbers`, `Ask it from your editor`, and 8 pages of the same kind. Actual: the classifier only saw a question when the heading ended on `?`, and its verb list held 42 words while the corpus writes with far more. Both made a page of answers look like a page of nouns. Applies to: every surface. `classifyHeading` reads an interrogative opener as a question, and the verb list grew to 89. Words that are evlog's own nouns first (`log`, `route`, `stream`, `trace`, `filter`, `drain`) are kept out of it, since counting `## Route filtering` as an imperative would weaken the rule rather than correct it. + +## 2026-08-15 · U-12 · `without X` is a condition, not a comparison + +Flagged: `Without \`setup\`, OpenTelemetry export is untouched`. +Actual: the sentence states what evlog does when an option is absent. `without` and `instead of` only compare what directly follows them, and here that is `setup`, not the alternative named after the comma. +Applies to: every surface. `corpus.mjs` reports those two words only when the alternative is their object, and leaves the unconditional comparatives alone. + +## 2026-08-15 · T-11 · A seam is a stitch, not a page + +Flagged: two paragraphs 87 and 141 lines apart, on a page whose other paragraphs offered no contraction to count. +Actual: the metric walked the paragraphs that had opportunities and called any two of them adjacent. Two registers at opposite ends of a page are not what a stitch looks like; the tell is a passage dropped into another one. +Applies to: every surface. `metrics.mjs` only reports a seam between paragraphs at most three apart on the page. + +## 2026-08-15 · U-14 · Two hyphens are an em dash + +Flagged: nothing. `--` between spaces reached nothing at all, and the README carried five of them. +Actual: `is auto-imported -- no import needed` is the same mark written with the keys at hand. Table cells and fenced code keep theirs, since `evlog-map-disable-next-line wide-event -- reason` is the CLI's own syntax. +Applies to: prose on every surface. + +## 2026-08-15 · D-12 · Renaming a heading breaks the links to it + +Flagged: nothing. Three anchors across two pages pointed at headings this branch had renamed, and one had been dead since before it. +Actual: a broken fragment reports no error anywhere. The page loads, the link resolves, and the reader arrives at the top of it. The scanner checked no anchor at all, and the audit written by hand only compared cross-page links, so same-page ones stayed invisible twice. +Applies to: `apps/docs/content/`. `reach.mjs` now resolves every fragment against the headings of the page it targets. Rename a heading and the link is a second edit, not an optional one. diff --git a/.agents/skills/write-evlog-content/references/rules/docs.md b/.agents/skills/write-evlog-content/references/rules/docs.md index de876323..89c11c3c 100644 --- a/.agents/skills/write-evlog-content/references/rules/docs.md +++ b/.agents/skills/write-evlog-content/references/rules/docs.md @@ -81,3 +81,11 @@ Why: this is the only class of docs defect that silently converts a correct read Rule: at least one other page links to this one in prose, a table, or a card. The navigation is not a substitute: it lists what exists, it does not tell a reader when they need it. Why: `voice.md` promises that the docs suggest the next move rather than waiting to be searched. A page nothing points at is a page that only answers a search someone already knew how to run. Note: the scanner reads links from prose, from table cells, and from `to:` / `href:` props in MDC components, so a card group counts. A section index is exempt, since the navigation is how it is meant to be reached, and a page linking to its own route does not count as being suggested. + +--- + +**D-12 · An anchor points at a heading that exists** · `critical` + +Rule: every `#fragment` in a link resolves to a heading on the page it targets, whether that page is this one or another. +Why: a renamed heading takes its anchor with it, and nothing reports the break. The link still resolves, the page still loads, and the reader lands at the top of a long page having been promised a section. +Note: the fragment is slugged the way the renderer does it, which removes punctuation rather than collapsing it. `Drain & Enrichers` anchors as `drain--enrichers` and `The ratchet: --baseline` as `the-ratchet---baseline`, both carrying the extra dash the removed character left behind. Links to another host carry someone else's fragments and are left alone. diff --git a/apps/docs/content/2.learn/7.typed-fields.md b/apps/docs/content/2.learn/7.typed-fields.md index 12eaecd1..b639eadd 100644 --- a/apps/docs/content/2.learn/7.typed-fields.md +++ b/apps/docs/content/2.learn/7.typed-fields.md @@ -48,11 +48,11 @@ export default defineEventHandler(async (event) => { }) ``` -TypeScript catches typos and unknown fields at compile time, before they reach production. +TypeScript then catches a typo or an unknown field at compile time, which is the only place a field-name mistake is cheap: once the event is in your drain the wrong key is already indexed, already queried, and already in someone's dashboard. ## Internal Fields -evlog sets some fields internally (`status`, `service`). These are always accepted regardless of your type, through the `InternalFields` type: +Some fields evlog sets itself. `status` and `service` are always accepted whatever your type says, through `InternalFields`: ```typescript [server/api/checkout.post.ts] log.set({ status: 200 }) // OK - internal field @@ -75,7 +75,7 @@ Typed fields are fully opt-in. ## Nuxt Auto-Import ::callout{icon="i-lucide-triangle-alert" color="warning"} -When using typed fields with `useLogger`, you **must** use an explicit import. The Nuxt auto-import does not support excess property checking for generics due to a TypeScript limitation. +Typed fields with `useLogger` need an explicit import. The auto-import cannot carry excess property checking through a generic, a TypeScript limitation rather than a module one. :: ```typescript [server/api/checkout.post.ts] @@ -89,7 +89,7 @@ const log = useLogger(event) log.set({ typo: 'oops' }) // No error (silently accepted) ``` -The auto-import works perfectly for untyped usage. Only add the explicit import when you need typed fields. +Untyped usage keeps the auto-import. Add the explicit one only where you pass a generic. ## Outside Nuxt diff --git a/apps/docs/content/5.use-cases/2.ai-sdk/03.options.md b/apps/docs/content/5.use-cases/2.ai-sdk/03.options.md index 28a0f8e7..05eb5fd3 100644 --- a/apps/docs/content/5.use-cases/2.ai-sdk/03.options.md +++ b/apps/docs/content/5.use-cases/2.ai-sdk/03.options.md @@ -17,7 +17,7 @@ links: variant: subtle --- -`createAILogger(log, options?)` accepts a single options bag. Every option is opt-in, and the defaults stay safe and quiet. +`createAILogger(log, options?)` accepts a single options bag. Every option is opt-in. The defaults stay quiet, and they stay safe: nothing a model was sent or returned reaches your drain until you ask for it by name. | Option | Type | Default | Description | |--------|------|---------|-------------| @@ -29,7 +29,7 @@ links: By default, `ai.toolCalls` is a `string[]` of tool names. Enable `toolInputs` to capture inputs too, which suits debugging agent behaviour or auditing what data the model reached for. ::warning -Tool inputs can be large and may contain sensitive data (SQL, API keys, customer PII). Use `maxLength` and `transform` rather than enabling raw capture in production. +Tool inputs get large, and they carry SQL, API keys and customer PII. Reach for `maxLength` and `transform` before raw capture in production. :: ### Capture everything @@ -81,7 +81,7 @@ const ai = createAILogger(log, { Read the result from your handler with [`ai.getEstimatedCost()`](/use-cases/ai-sdk/metadata), which suits billing dashboards or warning users before expensive calls. ::tip -Keep your `cost` map in one file alongside model selection so renaming a model in production also updates pricing. Avoid hardcoding per-route maps. +Keep your `cost` map in one file alongside model selection so renaming a model in production also updates pricing. Per-route maps drift the moment two routes disagree about which model they call, so keep one. :: ## Error Handling diff --git a/apps/docs/content/6.extend/10.custom-framework.md b/apps/docs/content/6.extend/10.custom-framework.md index ecb91bb0..b6a7fc7e 100644 --- a/apps/docs/content/6.extend/10.custom-framework.md +++ b/apps/docs/content/6.extend/10.custom-framework.md @@ -80,7 +80,7 @@ npm install evlog |--------|---------| | `defineFrameworkIntegration(spec)` | Manifest factory — extract request, create logger, attach, run with ALS | | `createMiddlewareLogger(opts)` | Lower-level lifecycle (custom mode) | -| `waitUntil` on middleware options | Defer drain on Cloudflare Workers / Vercel Edge (see [Serverless](#serverless-workers-edge)) | +| `waitUntil` on middleware options | Defer drain on Cloudflare Workers / Vercel Edge (see [Serverless](#serverless-workers-and-edge)) | | `createRequestLogger(opts)` | Wrap a non-HTTP unit of work in a logger lifecycle | | `BaseEvlogOptions` | Base user-facing options — `drain`, `enrich`, `keep`, `include`, `exclude`, `routes`, `plugins` | | `MiddlewareLoggerResult` | Return type: `{ logger, finish, skipped }` | @@ -185,7 +185,7 @@ const { logger, finish, skipped } = createMiddlewareLogger({ You'll be responsible for ALS wrapping (`storage.run`), `log.fork()` attachment (via `attachForkToLogger`), and finishing the lifecycle, but you keep the full pipeline (route filtering, sampling, emit, enrich, drain, plugins) for free. -## Serverless (Workers / Edge) +## Serverless: Workers and Edge On Cloudflare Workers and Vercel Edge, the runtime can terminate as soon as the response is returned. If your drain sends HTTP to an observability backend, pass `waitUntil` so enrich still runs inline but drain work survives after the response, the same behavior as [`evlog/workers`](/integrate/frameworks/cloudflare-workers) and the Nitro plugin. diff --git a/apps/docs/content/7.reference/1.configuration.md b/apps/docs/content/7.reference/1.configuration.md index 08643110..e08487c9 100644 --- a/apps/docs/content/7.reference/1.configuration.md +++ b/apps/docs/content/7.reference/1.configuration.md @@ -16,7 +16,7 @@ links: variant: subtle --- -evlog has two configuration surfaces: **global options** set once at startup, and **middleware options** set per-framework integration. This page documents both. +evlog has two configuration surfaces, and this page documents both: **global options** set once at startup, and **middleware options** set per framework integration. ## Global options (`initLogger`) @@ -43,7 +43,7 @@ initLogger({ | `enabled` | `boolean` | `true` | Enable/disable all logging globally. When `false`, all operations become no-ops | | `env` | `Partial` | Auto-detected | Environment context overrides (see below) | | `pretty` | `boolean` | `true` in dev | Pretty print with tree formatting. Auto-detected based on `NODE_ENV` | -| `dev` | `'evlog' \| 'nitro' \| 'both' \| object` | `'evlog'` in pretty dev | Dev terminal presets or `{ frameworkOverlay, prettyError }` — see [Dev terminal output](#dev-terminal-output) | +| `dev` | `'evlog' \| 'nitro' \| 'both' \| object` | `'evlog'` in pretty dev | Dev terminal presets or `{ frameworkOverlay, prettyError }`, see [Tune the dev terminal output](#tune-the-dev-terminal-output) | | `silent` | `boolean` | `false` | Suppress console output. Events are still built, sampled, and passed to drains | | `stringify` | `boolean` | `true` | Emit JSON strings when `pretty` is disabled. Set to `false` for Cloudflare Workers | | `minLevel` | `'debug' \| 'info' \| 'warn' \| 'error'` | `'debug'` | Minimum severity for the global `log` API only (not `createLogger` / request wide events). Order: debug < info < warn < error | @@ -104,7 +104,7 @@ See [Development terminal output](/learn/structured-errors#development-terminal- ### Stamp the environment on every event -The `env` option controls the fields included in every log event. Most values are auto-detected from environment variables. +The `env` option controls the fields included in every log event, and the table below names the variable each one is read from when you leave it unset. | Field | Type | Default | Auto-detected from | |-------|------|---------|-------------------| @@ -129,7 +129,7 @@ initLogger({ ``` ::callout{icon="i-lucide-alert-triangle" color="warning"} -If `silent` is enabled without a drain, events are built and sampled but never output anywhere. evlog will warn you about this at startup. +If `silent` is enabled without a drain, events are built and sampled but never output anywhere, which evlog warns about at startup. :: ## Middleware options @@ -227,6 +227,6 @@ See the full [Nuxt configuration](/integrate/frameworks/nuxt#configuration). ### Nitro -The Nitro module accepts `enabled`, `env`, `pretty`, `silent`, `sampling`, `include`, `exclude`, and `routes` in `nitro.config.ts`. Drain and enrichment are done via Nitro hooks. +The Nitro module accepts `enabled`, `env`, `pretty`, `silent`, `sampling`, `include`, `exclude`, and `routes` in `nitro.config.ts`, and leaves drain and enrichment to the Nitro hooks. See [Nitro drain & enrichers](/integrate/frameworks/nitro#drain--enrichers). diff --git a/apps/docs/content/7.reference/2.performance.md b/apps/docs/content/7.reference/2.performance.md index 613c84f2..a4e6eab3 100644 --- a/apps/docs/content/7.reference/2.performance.md +++ b/apps/docs/content/7.reference/2.performance.md @@ -87,13 +87,13 @@ The benchmarks above measure CPU + serialization cost on the main thread, with n **Fire-and-forget hot paths with pino-via-worker-thread.** In production, pino is typically configured with a [worker-thread transport](https://getpino.io/#/docs/transports) (`pino-pretty`, `pino-loki`, vendor-specific transports). The serialization and I/O move off the main thread entirely. For a workload that emits hundreds of thousands of `log.info('foo')` lines per second with no context accumulation, pino-via-worker can hit ~2-3M ops/s on the main thread because it's just queueing. We can't benchmark that mode fairly inside a single-threaded vitest process, so it's not in our table, and it is a real scenario where pino is faster. -**CLI / pretty-only output without serialization.** consola's no-op reporter mode in our benchmarks (`level: 4, reporters: [{ log: () => {} }]`) skips JSON serialization entirely. That's realistic if you're using consola for a CLI with terminal-only output, but it's why consola wins "simple string" and "burst": it is not doing the same work. evlog and pino both serialize to JSON; consola in those benchmarks does not. If your use case is "pretty terminal output, no shipping logs anywhere", consola is genuinely lighter. +**CLI / pretty-only output without serialization.** consola's no-op reporter mode in our benchmarks (`level: 4, reporters: [{ log: () => {} }]`) skips JSON serialization entirely. That's realistic if you're using consola for a CLI with terminal-only output, but it's why consola wins "simple string" and "burst": it is not doing the same work. evlog and pino both serialize to JSON; consola in [those benchmarks](#results) does not. If your use case is "pretty terminal output, no shipping logs anywhere", consola is genuinely lighter, which is why it leads the [simple string and burst rows](#results). **Single `log.info` calls, no context accumulation.** evlog and pino are roughly tied on `pino.info('hello')` vs `evlog.info('hello')` (1.83M vs 1.09M ops/s in our run, but the gap closes further if pino runs in async mode). evlog's ~7.7x advantage shows up specifically when you'd otherwise emit N separate lines for one logical operation. If you genuinely log one line per call and don't accumulate, the speed delta is much smaller. Pick evlog for the API ergonomics (`log.set` + structured errors), not raw throughput. **Wall-clock variance is real.** Vitest bench numbers shift ±5-10% between runs on the same machine (thermal throttling, GC, other processes). The numbers above come from a single run on a MacBook, so treat them as a snapshot rather than a guaranteed floor. Run the suite yourself on the hardware you care about. -The takeaway: **the wins are real for the wide event pattern**, but if your stack is "pure fire-and-forget pino with a worker transport", that's the one place we don't claim to beat. +The takeaway: **the wins are real for the wide event pattern**, but if your stack is "pure fire-and-forget pino with a [worker transport](https://getpino.io/#/docs/transports)", that's the one place we don't claim to beat. ## Real-world overhead @@ -108,11 +108,11 @@ For a typical API request: | Enricher pipeline | 2.14µs | | **Total** | **~2.7µs** | -For context, a database query takes 1-50ms, an HTTP call takes 10-500ms. evlog's overhead is **invisible**. +For context, a database query takes 1-50ms and an HTTP call takes 10-500ms, so those 2.7µs are three orders of magnitude below the cheapest thing the request already does. ## Bundle size -Every entry point is tree-shakeable. You only pay for what you import. +Every entry point is tree-shakeable, so a project importing only `evlog` pays for the core and nothing else in the table below. | Entry | Gzip | |-------|-----:| diff --git a/apps/docs/skills/review-logging-patterns/references/structured-errors.md b/apps/docs/skills/review-logging-patterns/references/structured-errors.md index 17a16c0c..caea659b 100644 --- a/apps/docs/skills/review-logging-patterns/references/structured-errors.md +++ b/apps/docs/skills/review-logging-patterns/references/structured-errors.md @@ -438,7 +438,7 @@ try { ## Error Message Templates -Common patterns -- adapt fields to each specific case: +Common patterns, with the fields adapted to each case: | Pattern | Status | Fields | |---------|--------|--------| diff --git a/packages/evlog/README.md b/packages/evlog/README.md index c06c6f0c..7e6159dc 100644 --- a/packages/evlog/README.md +++ b/packages/evlog/README.md @@ -611,7 +611,7 @@ log.info('checkout', 'User initiated checkout') log.error({ action: 'payment', error: 'validation_failed' }) ``` -In Nuxt, `log` is auto-imported -- no import needed in Vue components: +In Nuxt, `log` is auto-imported, so a Vue component needs no import: ```vue