Skip to content

Commit 750b4e4

Browse files
committed
Merge branch 'docs/last-findings'
2 parents e5cfe19 + aeb15f6 commit 750b4e4

17 files changed

Lines changed: 184 additions & 37 deletions

File tree

.agents/skills/write-evlog-content/references/corrections.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,27 @@ Applies to: every surface. `metrics.mjs` ignores a dash with a digit on each sid
9292
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.
9393
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.
9494
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.
95+
96+
## 2026-08-15 · U-12 · `without X` is a condition, not a comparison
97+
98+
Flagged: `Without \`setup\`, OpenTelemetry export is untouched`.
99+
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.
100+
Applies to: every surface. `corpus.mjs` reports those two words only when the alternative is their object, and leaves the unconditional comparatives alone.
101+
102+
## 2026-08-15 · T-11 · A seam is a stitch, not a page
103+
104+
Flagged: two paragraphs 87 and 141 lines apart, on a page whose other paragraphs offered no contraction to count.
105+
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.
106+
Applies to: every surface. `metrics.mjs` only reports a seam between paragraphs at most three apart on the page.
107+
108+
## 2026-08-15 · U-14 · Two hyphens are an em dash
109+
110+
Flagged: nothing. `--` between spaces reached nothing at all, and the README carried five of them.
111+
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.
112+
Applies to: prose on every surface.
113+
114+
## 2026-08-15 · D-12 · Renaming a heading breaks the links to it
115+
116+
Flagged: nothing. Three anchors across two pages pointed at headings this branch had renamed, and one had been dead since before it.
117+
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.
118+
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.

.agents/skills/write-evlog-content/references/rules/docs.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,11 @@ Why: this is the only class of docs defect that silently converts a correct read
8181
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.
8282
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.
8383
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.
84+
85+
---
86+
87+
**D-12 · An anchor points at a heading that exists** · `critical`
88+
89+
Rule: every `#fragment` in a link resolves to a heading on the page it targets, whether that page is this one or another.
90+
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.
91+
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.

apps/docs/content/2.learn/7.typed-fields.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ export default defineEventHandler(async (event) => {
4848
})
4949
```
5050

51-
TypeScript catches typos and unknown fields at compile time, before they reach production.
51+
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.
5252

5353
## Internal Fields
5454

55-
evlog sets some fields internally (`status`, `service`). These are always accepted regardless of your type, through the `InternalFields` type:
55+
Some fields evlog sets itself. `status` and `service` are always accepted whatever your type says, through `InternalFields`:
5656

5757
```typescript [server/api/checkout.post.ts]
5858
log.set({ status: 200 }) // OK - internal field
@@ -75,7 +75,7 @@ Typed fields are fully opt-in.
7575
## Nuxt Auto-Import
7676

7777
::callout{icon="i-lucide-triangle-alert" color="warning"}
78-
When using typed fields with `useLogger<T>`, you **must** use an explicit import. The Nuxt auto-import does not support excess property checking for generics due to a TypeScript limitation.
78+
Typed fields with `useLogger<T>` need an explicit import. The auto-import cannot carry excess property checking through a generic, a TypeScript limitation rather than a module one.
7979
::
8080

8181
```typescript [server/api/checkout.post.ts]
@@ -89,7 +89,7 @@ const log = useLogger<MyFields>(event)
8989
log.set({ typo: 'oops' }) // No error (silently accepted)
9090
```
9191

92-
The auto-import works perfectly for untyped usage. Only add the explicit import when you need typed fields.
92+
Untyped usage keeps the auto-import. Add the explicit one only where you pass a generic.
9393

9494
## Outside Nuxt
9595

apps/docs/content/5.use-cases/2.ai-sdk/03.options.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ links:
1717
variant: subtle
1818
---
1919

20-
`createAILogger(log, options?)` accepts a single options bag. Every option is opt-in, and the defaults stay safe and quiet.
20+
`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.
2121

2222
| Option | Type | Default | Description |
2323
|--------|------|---------|-------------|
@@ -29,7 +29,7 @@ links:
2929
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.
3030

3131
::warning
32-
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.
32+
Tool inputs get large, and they carry SQL, API keys and customer PII. Reach for `maxLength` and `transform` before raw capture in production.
3333
::
3434

3535
### Capture everything
@@ -81,7 +81,7 @@ const ai = createAILogger(log, {
8181
Read the result from your handler with [`ai.getEstimatedCost()`](/use-cases/ai-sdk/metadata), which suits billing dashboards or warning users before expensive calls.
8282

8383
::tip
84-
Keep your `cost` map in one file alongside model selection so renaming a model in production also updates pricing. Avoid hardcoding per-route maps.
84+
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.
8585
::
8686

8787
## Error Handling

apps/docs/content/6.extend/10.custom-framework.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ npm install evlog
8080
|--------|---------|
8181
| `defineFrameworkIntegration(spec)` | Manifest factory — extract request, create logger, attach, run with ALS |
8282
| `createMiddlewareLogger(opts)` | Lower-level lifecycle (custom mode) |
83-
| `waitUntil` on middleware options | Defer drain on Cloudflare Workers / Vercel Edge (see [Serverless](#serverless-workers-edge)) |
83+
| `waitUntil` on middleware options | Defer drain on Cloudflare Workers / Vercel Edge (see [Serverless](#serverless-workers-and-edge)) |
8484
| `createRequestLogger(opts)` | Wrap a non-HTTP unit of work in a logger lifecycle |
8585
| `BaseEvlogOptions` | Base user-facing options — `drain`, `enrich`, `keep`, `include`, `exclude`, `routes`, `plugins` |
8686
| `MiddlewareLoggerResult` | Return type: `{ logger, finish, skipped }` |
@@ -185,7 +185,7 @@ const { logger, finish, skipped } = createMiddlewareLogger({
185185

186186
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.
187187

188-
## Serverless (Workers / Edge)
188+
## Serverless: Workers and Edge
189189

190190
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.
191191

apps/docs/content/7.reference/1.configuration.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ links:
1616
variant: subtle
1717
---
1818

19-
evlog has two configuration surfaces: **global options** set once at startup, and **middleware options** set per-framework integration. This page documents both.
19+
evlog has two configuration surfaces, and this page documents both: **global options** set once at startup, and **middleware options** set per framework integration.
2020

2121
## Global options (`initLogger`)
2222

@@ -43,7 +43,7 @@ initLogger({
4343
| `enabled` | `boolean` | `true` | Enable/disable all logging globally. When `false`, all operations become no-ops |
4444
| `env` | `Partial<EnvironmentContext>` | Auto-detected | Environment context overrides (see below) |
4545
| `pretty` | `boolean` | `true` in dev | Pretty print with tree formatting. Auto-detected based on `NODE_ENV` |
46-
| `dev` | `'evlog' \| 'nitro' \| 'both' \| object` | `'evlog'` in pretty dev | Dev terminal presets or `{ frameworkOverlay, prettyError }`see [Dev terminal output](#dev-terminal-output) |
46+
| `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) |
4747
| `silent` | `boolean` | `false` | Suppress console output. Events are still built, sampled, and passed to drains |
4848
| `stringify` | `boolean` | `true` | Emit JSON strings when `pretty` is disabled. Set to `false` for Cloudflare Workers |
4949
| `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-
104104

105105
### Stamp the environment on every event
106106

107-
The `env` option controls the fields included in every log event. Most values are auto-detected from environment variables.
107+
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.
108108

109109
| Field | Type | Default | Auto-detected from |
110110
|-------|------|---------|-------------------|
@@ -129,7 +129,7 @@ initLogger({
129129
```
130130

131131
::callout{icon="i-lucide-alert-triangle" color="warning"}
132-
If `silent` is enabled without a drain, events are built and sampled but never output anywhere. evlog will warn you about this at startup.
132+
If `silent` is enabled without a drain, events are built and sampled but never output anywhere, which evlog warns about at startup.
133133
::
134134

135135
## Middleware options
@@ -227,6 +227,6 @@ See the full [Nuxt configuration](/integrate/frameworks/nuxt#configuration).
227227

228228
### Nitro
229229

230-
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.
230+
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.
231231

232232
See [Nitro drain & enrichers](/integrate/frameworks/nitro#drain--enrichers).

apps/docs/content/7.reference/2.performance.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@ The benchmarks above measure CPU + serialization cost on the main thread, with n
8787

8888
**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.
8989

90-
**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.
90+
**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).
9191

9292
**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.
9393

9494
**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.
9595

96-
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.
96+
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.
9797

9898
## Real-world overhead
9999

@@ -108,11 +108,11 @@ For a typical API request:
108108
| Enricher pipeline | 2.14µs |
109109
| **Total** | **~2.7µs** |
110110

111-
For context, a database query takes 1-50ms, an HTTP call takes 10-500ms. evlog's overhead is **invisible**.
111+
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.
112112

113113
## Bundle size
114114

115-
Every entry point is tree-shakeable. You only pay for what you import.
115+
Every entry point is tree-shakeable, so a project importing only `evlog` pays for the core and nothing else in the table below.
116116

117117
| Entry | Gzip |
118118
|-------|-----:|

apps/docs/skills/review-logging-patterns/references/structured-errors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ try {
438438

439439
## Error Message Templates
440440

441-
Common patterns -- adapt fields to each specific case:
441+
Common patterns, with the fields adapted to each case:
442442

443443
| Pattern | Status | Fields |
444444
|---------|--------|--------|

packages/evlog/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ log.info('checkout', 'User initiated checkout')
611611
log.error({ action: 'payment', error: 'validation_failed' })
612612
```
613613

614-
In Nuxt, `log` is auto-imported -- no import needed in Vue components:
614+
In Nuxt, `log` is auto-imported, so a Vue component needs no import:
615615

616616
```vue
617617
<script setup>
@@ -735,7 +735,7 @@ Each enricher adds a specific field to the event:
735735

736736
All enrichers accept an optional `{ overwrite?: boolean }` option. By default (`overwrite: false`), user-provided data on the event takes precedence over enricher-computed values. Set `overwrite: true` to always replace existing fields.
737737

738-
> **Cloudflare geo note:** Only `cf-ipcountry` is a real Cloudflare HTTP header. The `cf-region`, `cf-city`, `cf-latitude`, `cf-longitude` headers are NOT standard -- they are properties of `request.cf`. For full geo data on Cloudflare, write a custom enricher that reads `request.cf`, or use a Workers middleware to forward `cf` properties as custom headers.
738+
> **Cloudflare geo note:** Only `cf-ipcountry` is a real Cloudflare HTTP header. The `cf-region`, `cf-city`, `cf-latitude`, `cf-longitude` headers are NOT standard: they are properties of `request.cf`. For full geo data on Cloudflare, write a custom enricher that reads `request.cf`, or use a Workers middleware to forward `cf` properties as custom headers.
739739
740740
### Custom Enrichers
741741

@@ -1142,9 +1142,9 @@ export default defineNitroPlugin((nitroApp) => {
11421142

11431143
The function returned by `pipeline(drain)` is hook-compatible and exposes:
11441144

1145-
- **`drain(ctx)`** -- Push a single event into the buffer
1146-
- **`drain.flush()`** -- Force-flush all buffered events (call on server shutdown)
1147-
- **`drain.pending`** -- Number of events currently buffered
1145+
- **`drain(ctx)`**: push a single event into the buffer
1146+
- **`drain.flush()`**: force-flush all buffered events (call on server shutdown)
1147+
- **`drain.pending`**: number of events currently buffered
11481148

11491149
## API Reference
11501150

scripts/content-lint/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const scan = (file) => {
9494
*/
9595
function scanSource(source, file) {
9696
const doc = parseMarkdown(source)
97-
return { frontmatter: doc.frontmatter, links: doc.links, metrics: measure(doc), drift: checkDrift(doc, api, routes, file) }
97+
return { frontmatter: doc.frontmatter, links: doc.links, headings: doc.headings, metrics: measure(doc), drift: checkDrift(doc, api, routes, file) }
9898
}
9999

100100
// Ad-hoc input is scanned against the corpus baseline but belongs to no file,

0 commit comments

Comments
 (0)