You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .agents/skills/write-evlog-content/references/corrections.md
+24Lines changed: 24 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -92,3 +92,27 @@ Applies to: every surface. `metrics.mjs` ignores a dash with a digit on each sid
92
92
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.
93
93
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.
94
94
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.
Copy file name to clipboardExpand all lines: .agents/skills/write-evlog-content/references/rules/docs.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -81,3 +81,11 @@ Why: this is the only class of docs defect that silently converts a correct read
81
81
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.
82
82
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.
83
83
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.
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.
52
52
53
53
## Internal Fields
54
54
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`:
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.
Copy file name to clipboardExpand all lines: apps/docs/content/5.use-cases/2.ai-sdk/03.options.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,7 +17,7 @@ links:
17
17
variant: subtle
18
18
---
19
19
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.
21
21
22
22
| Option | Type | Default | Description |
23
23
|--------|------|---------|-------------|
@@ -29,7 +29,7 @@ links:
29
29
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.
30
30
31
31
::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.
33
33
::
34
34
35
35
### Capture everything
@@ -81,7 +81,7 @@ const ai = createAILogger(log, {
81
81
Read the result from your handler with [`ai.getEstimatedCost()`](/use-cases/ai-sdk/metadata), which suits billing dashboards or warning users before expensive calls.
82
82
83
83
::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.
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.
187
187
188
-
## Serverless (Workers / Edge)
188
+
## Serverless: Workers and Edge
189
189
190
190
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.
Copy file name to clipboardExpand all lines: apps/docs/content/7.reference/1.configuration.md
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,7 +16,7 @@ links:
16
16
variant: subtle
17
17
---
18
18
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 perframework integration.
20
20
21
21
## Global options (`initLogger`)
22
22
@@ -43,7 +43,7 @@ initLogger({
43
43
|`enabled`|`boolean`|`true`| Enable/disable all logging globally. When `false`, all operations become no-ops |
44
44
|`env`|`Partial<EnvironmentContext>`| Auto-detected | Environment context overrides (see below) |
45
45
|`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)|
47
47
|`silent`|`boolean`|`false`| Suppress console output. Events are still built, sampled, and passed to drains |
48
48
|`stringify`|`boolean`|`true`| Emit JSON strings when `pretty` is disabled. Set to `false` for Cloudflare Workers |
49
49
|`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-
104
104
105
105
### Stamp the environment on every event
106
106
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.
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.
133
133
::
134
134
135
135
## Middleware options
@@ -227,6 +227,6 @@ See the full [Nuxt configuration](/integrate/frameworks/nuxt#configuration).
227
227
228
228
### Nitro
229
229
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.
231
231
232
232
See [Nitro drain & enrichers](/integrate/frameworks/nitro#drain--enrichers).
Copy file name to clipboardExpand all lines: apps/docs/content/7.reference/2.performance.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -87,13 +87,13 @@ The benchmarks above measure CPU + serialization cost on the main thread, with n
87
87
88
88
**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.
89
89
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).
91
91
92
92
**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.
93
93
94
94
**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.
95
95
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.
97
97
98
98
## Real-world overhead
99
99
@@ -108,11 +108,11 @@ For a typical API request:
108
108
| Enricher pipeline | 2.14µs |
109
109
|**Total**|**~2.7µs**|
110
110
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.
112
112
113
113
## Bundle size
114
114
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.
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:
615
615
616
616
```vue
617
617
<script setup>
@@ -735,7 +735,7 @@ Each enricher adds a specific field to the event:
735
735
736
736
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.
737
737
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.
0 commit comments