Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/write-evlog-content/references/corrections.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ Applies to: list items on every surface. Table cells stay out: a cell is a fragm
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.

## 2026-08-15 · T-06 · A question does not need its mark, and the verb list was too short

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.
2 changes: 1 addition & 1 deletion apps/docs/content/4.integrate/frameworks/01.nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ All options are set in `nuxt.config.ts` under the `evlog` key:
| `exclude` | `string[]` | `undefined` | Route patterns to exclude. Exclusions take precedence |
| `routes` | `Record<string, RouteConfig>` | `undefined` | Route-specific service configuration |
| `pretty` | `boolean` | `true` in dev | Pretty print with tree formatting |
| `dev` | `'evlog' \| 'nitro' \| 'both' \| object` | `'evlog'` in pretty dev | Dev terminal presets or `{ frameworkOverlay, prettyError }` — see [Configuration — Dev terminal output](/reference/configuration#dev-terminal-output) |
| `dev` | `'evlog' \| 'nitro' \| 'both' \| object` | `'evlog'` in pretty dev | Dev terminal presets or `{ frameworkOverlay, prettyError }` — see [Configuration — Dev terminal output](/reference/configuration#tune-the-dev-terminal-output) |

::callout{icon="i-lucide-terminal" color="info"}
**Dev terminal presets:** `'evlog'` (default) gives one clean signal, evlog-only stack. `'nitro'` gives the wide event context + Nitro Youch stack (evlog prints Why/Fix only). `'both'` gives the full evlog block and the Nitro overlay. With `pretty: false`, set `dev: { frameworkOverlay: false }` to suppress Nitro while logging JSON.
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/4.integrate/frameworks/16.aws-lambda.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ If you process the whole batch as one logical unit, use a **single** `createLogg

## Stdout and `silent`

Many teams ingest Lambda logs from **CloudWatch** via stdout. If you use a **drain adapter** (OTLP, Datadog, Axiom, etc.) and want JSON or platform-specific formatting without duplicate console noise, set `silent: true` in production. See [Configuration](/reference/configuration#silent-mode).
Many teams ingest Lambda logs from **CloudWatch** via stdout. If you use a **drain adapter** (OTLP, Datadog, Axiom, etc.) and want JSON or platform-specific formatting without duplicate console noise, set `silent: true` in production. See [Configuration](/reference/configuration#silence-the-terminal).

```typescript [src/handler.ts]
import { createAxiomDrain } from 'evlog/axiom'
Expand Down
10 changes: 5 additions & 5 deletions apps/docs/content/5.use-cases/5.enrichers.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default defineNitroPlugin((nitroApp) => {

Each enricher accepts `{ overwrite?: boolean }` (default `false`) so user-set fields are preserved.

## User Agent
## `userAgentEnricher()`

Parse browser, OS, and device type from the `User-Agent` header.

Expand Down Expand Up @@ -102,7 +102,7 @@ interface UserAgentInfo {

**Detected devices:** Bot (crawlers, spiders), Tablet (iPad), Mobile (iPhone, Android phones), Desktop (fallback).

## Geo
## `geoEnricher()`

Extract geographic data from platform-injected headers.

Expand Down Expand Up @@ -136,7 +136,7 @@ interface GeoInfo {
**Cloudflare note:** Only `cf-ipcountry` is a standard Cloudflare HTTP header. Other geo fields (`city`, `region`, `latitude`, etc.) are properties of `request.cf`, which is not exposed as headers. For full Cloudflare geo data, write a [custom enricher](/extend/custom-enrichers) that reads `request.cf`, or use a Workers middleware to copy `cf` properties into custom headers.
::

## Request Size
## `requestSizeEnricher()`

Capture request and response payload sizes from `Content-Length` headers.

Expand Down Expand Up @@ -170,7 +170,7 @@ interface RequestSizeInfo {
This enricher reads the `Content-Length` header from both the request and response. If the header is missing (e.g., for chunked transfer encoding), the corresponding field will be `undefined`.
::

## Trace Context
## `traceContextEnricher()`

Extract W3C trace context from the `traceparent` and `tracestate` headers.

Expand Down Expand Up @@ -211,7 +211,7 @@ interface TraceContextInfo {
The traceparent format follows the [W3C Trace Context](https://www.w3.org/TR/trace-context/) specification: `{version}-{traceId}-{spanId}-{flags}`.
::

## Full Setup Example
## Wire all four at once

Use all built-in enrichers together. The list of enrichers is identical across frameworks. Only the wiring changes.

Expand Down
12 changes: 6 additions & 6 deletions apps/docs/content/7.reference/1.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ links:

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

## Global Options (`initLogger`)
## Global options (`initLogger`)

These options apply to all frameworks. Call `initLogger()` once at application startup for standalone frameworks (Hono, Express, Fastify, Elysia, NestJS, SvelteKit, Cloudflare Workers). For Nuxt and Nitro, these are set via module config and passed through automatically.

Expand Down Expand Up @@ -60,7 +60,7 @@ initLogger({

Evaluation order for `log.info` / `log.debug` / etc.: `enabled` → `minLevel` → head sampling → output.

### Dev terminal output
### Tune the dev terminal output

Pretty error blocks run only when `pretty: true` (default in development). Production always emits JSON wide events, with no stack snippets and no disk reads.

Expand Down Expand Up @@ -102,7 +102,7 @@ evlog: {

See [Development terminal output](/learn/structured-errors#development-terminal-output) in Structured Errors for an example of the pretty error tree.

### Environment Context
### 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.

Expand All @@ -114,7 +114,7 @@ The `env` option controls the fields included in every log event. Most values ar
| `commitHash` | `string` | `undefined` | `COMMIT_SHA`, `GITHUB_SHA`, `VERCEL_GIT_COMMIT_SHA`, `CF_PAGES_COMMIT_SHA` |
| `region` | `string` | `undefined` | `VERCEL_REGION`, `AWS_REGION`, `FLY_REGION`, `CF_REGION` |

### Silent Mode
### Silence the terminal

Use `silent` when your deployment platform captures stdout as its primary log ingestion (GCP Cloud Run, AWS Lambda, Fly.io, Railway, etc.) and you want a drain adapter to control the output format.

Expand All @@ -132,7 +132,7 @@ initLogger({
If `silent` is enabled without a drain, events are built and sampled but never output anywhere. evlog will warn you about this at startup.
::

## Middleware Options
## Middleware options

These options are passed to the framework middleware/plugin. They control per-request behavior: which routes to log, how to drain and enrich events, and custom tail sampling logic.

Expand Down Expand Up @@ -208,7 +208,7 @@ app.use(evlog({
}))
```

## Framework-Specific Options
## Framework-specific options

Some frameworks have additional options beyond the shared config:

Expand Down
10 changes: 5 additions & 5 deletions apps/docs/content/7.reference/5.vs-other-loggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ evlog is a fully-featured general-purpose logger first, with wide events as a na
- **Pick evlog over consola** when logs stop being something you read and start being something you query. consola renders for a person: three reporters ship (`fancy`, `basic`, `browser`), and structured output is a custom reporter you write. There is no pipeline, no batching, no sampling, and no request lifecycle.
- **Stay on pino** only if you emit hundreds of thousands of fire-and-forget lines per second with no context accumulation, and you have a custom transport you don't want to port. That is the one shape where [the benchmarks](/reference/performance) put pino ahead. evlog still wins the wide event lifecycle by 7.7x, but pino can edge it on raw `info('hello world')` throughput.

## Feature comparison
## Compare the feature sets

Three tables instead of one wall. The **Winner** column on the right tells you who wins each row at a glance; cells use semantic words ("Built-in", "Manual", "via X") instead of generic "Yes" so you can read the level of effort without reading the spec.

::callout{icon="i-lucide-info" color="info"}
Hover (or tap on mobile) the **info icon** next to any feature name for a one-line explanation of what the row actually compares.
::

### Core API
### What you call in a handler

| Feature | evlog | pino | consola | winston | Winner |
|---------|:-----:|:----:|:-------:|:-------:|:-------|
Expand All @@ -51,7 +51,7 @@ Hover (or tap on mobile) the **info icon** next to any feature name for a one-li
| :feature-label[Wide events (one per operation)]{tip="Accumulate context throughout a request and emit one typed event at the end — the wide-event observability pattern from Honeycomb / Charity Majors."} | Yes | No | No | No | **evlog** |
| :feature-label[Structured errors (why / fix / link)]{tip="Errors carry why (root cause), fix (actionable next step), and link (docs URL) fields that travel from server to client."} | Yes | No | No | No | **evlog** |

### Production features
### What runs in production

| Feature | evlog | pino | consola | winston | Winner |
|---------|:-----:|:----:|:-------:|:-------:|:-------|
Expand All @@ -69,7 +69,7 @@ Hover (or tap on mobile) the **info icon** next to any feature name for a one-li
| :feature-label[Self-hosted storage (NuxtHub adapter)]{tip="Store logs in your own NuxtHub-hosted database with automatic retention — no external observability vendor required."} | Built-in | No | No | No | **evlog** |
| :feature-label[Edge / Workers runtime]{tip="First-class support for Cloudflare Workers and Vercel Edge with waitUntil() integration so drains finish before the runtime terminates."} | Built-in | Partial | No | No | **evlog** |

### Footprint and ecosystem
### What it costs to install

| Feature | evlog | pino | consola | winston | Winner |
|---------|:-----:|:----:|:-------:|:-------:|:-------|
Expand Down Expand Up @@ -110,7 +110,7 @@ You configure `minLevel` in `initLogger({ minLevel: 'info' })` and that's it for

On **request-scoped wide-event loggers**, `log.setLevel('error' | 'warn' | 'info' | 'debug')` promotes the event level explicitly without touching the `error` context, which is what you want when you control the error shape yourself. Client-side `setMinLevel()` works the same way for the browser `log` API.

### No custom levels
### Custom levels are not supported

evlog ships `debug` / `info` / `warn` / `error` and that's it. pino, consola, and winston all let you define `trace`, `notice`, `fatal`, etc. We chose four levels on purpose (most teams never use more than four), but if your existing pipeline depends on `fatal` or `trace` you'll need to map them onto the closest evlog level.

Expand Down
6 changes: 3 additions & 3 deletions packages/nuxthub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

Self-hosted log retention for [evlog](https://evlog.dev) using [NuxtHub](https://hub.nuxt.com) database storage. Store, query, and automatically clean up your structured logs with zero external dependencies.

## Setup
## Install and wire it

Install the packages:

Expand Down Expand Up @@ -40,15 +40,15 @@ export default defineNuxtConfig({

The cleanup cron schedule is automatically derived from the retention value.

## Database Support
## Which databases work

NuxtHub supports multiple database dialects. The `evlog_events` table schema is automatically registered for:

- **SQLite** (default for Cloudflare D1)
- **MySQL**
- **PostgreSQL**

## Deployment
## Deploy it

For Vercel deployments, the module can create a `vercel.json` with the appropriate cron schedule during `nuxi module add`. For Cloudflare and other platforms, the Nitro scheduled task handles cleanup automatically.

Expand Down
49 changes: 48 additions & 1 deletion scripts/content-lint/lib/corpus.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -301,43 +301,90 @@ export function offNameTerms(text, context = text) {
/** Openers treated as imperative when classifying headings (T-06). */
export const IMPERATIVE_VERBS = [
'add',
'apply',
'ask',
'attach',
'build',
'call',
'catch',
'choose',
'close',
'collect',
'compare',
'configure',
'connect',
'consume',
'create',
'debug',
'declare',
'define',
'delete',
'deploy',
'disable',
'drain',
'emit',
'enable',
'export',
'expose',
'extend',
'fail',
'find',
'fix',
'follow',
'forward',
'get',
'handle',
'import',
'inspect',
'install',
'instrument',
'join',
'keep',
'log',
'make',
'measure',
'merge',
'move',
'narrow',
'open',
'pick',
'prove',
'publish',
'reach',
'read',
'redact',
'register',
'rename',
'render',
'replace',
'reset',
'resolve',
'restore',
'retry',
'reuse',
'rotate',
'run',
'sample',
'send',
'set',
'ship',
'silence',
'skip',
'spawn',
'stamp',
'start',
'stop',
'strip',
'test',
'track',
'trim',
'try',
'tune',
'turn',
'tweak',
'update',
'upgrade',
'use',
'validate',
'verify',
'wire',
'wrap',
Expand Down
5 changes: 4 additions & 1 deletion scripts/content-lint/lib/metrics.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ function classifyHeading(text) {
// procedure share a shape because they are one procedure, which is the twin
// `T-06` is meant to spare.
if (/^(?:step\s+)?\d+\s*[.):—–]?\s/i.test(trimmed)) return 'sequence'
if (trimmed.endsWith('?')) return 'question'
// A question does not need its mark. `Where the byte counts come from` and
// `Which number moves your bill` are the same shape as the ones that carry it,
// and a page built on them is a set of answers rather than one mould.
if (trimmed.endsWith('?') || /^(what|where|which|when|why|how|who|whether)\b/i.test(trimmed)) return 'question'
const first = trimmed.toLowerCase().split(/\s+/)[0]
if (IMPERATIVE_VERBS.includes(first)) return 'imperative'
if (/\b(is|are|was|were|has|have|does|do|can|will|becomes|makes|gives|takes)\b/i.test(trimmed)) {
Expand Down
32 changes: 25 additions & 7 deletions scripts/content-lint/lib/metrics.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ describe('headings', () => {
expect(metrics.headings.dominant).toBe('imperative')
expect(metrics.headings.share).toBe(1)
})

it('reads a heading opening on an interrogative as a question', () => {
const titles = ['Where the byte counts come from', 'Which number moves your bill', 'What a retained event costs']
const source = titles.map(title => `## ${title}\n\nProse under it.`).join('\n\n')

expect(measureSource(source).headings.dominant).toBe('question')
})

it('leaves evlog\'s own nouns out of the verb list', () => {
const titles = ['Route filtering', 'Trace context', 'Drain pipeline']
const source = titles.map(title => `## ${title}\n\nProse under it.`).join('\n\n')

expect(measureSource(source).headings.dominant).toBe('noun')
})

it('reads a heading opening on a symbol as an API entry', () => {
const source = ['## `getMetadata()`: final snapshot', '## `getEstimatedCost()`: quick check', '## `onUpdate()`: incremental', '## `shape`: the record'].map(h => `${h}\n\nProse about it.`).join('\n\n')

expect(measureSource(source).headings.dominant).toBe('symbol')
})
})

describe('contraction seam', () => {
Expand All @@ -82,7 +102,7 @@ describe('contraction seam', () => {
})
})

describe('dashes', () => {
describe('dashes, twins', () => {
it('reads a dash between two numbers as a range', () => {
expect(measureSource('Manifest mode is ~30–80 lines of glue.').dashes.count).toBe(0)
})
Expand All @@ -92,14 +112,12 @@ describe('dashes', () => {
expect(measureSource('## Network bridge — stream server\n\nProse.').dashes.count).toBe(1)
})

it('reads a heading opening on a symbol as an API entry', () => {
const source = ['## `getMetadata()`: final snapshot', '## `getEstimatedCost()`: quick check', '## `onUpdate()`: incremental', '## `shape`: the record'].map(h => `${h}\n\nProse about it.`).join('\n\n')

expect(measureSource(source).headings.dominant).toBe('symbol')
it('counts a dash hiding in a bullet', () => {
expect(measureSource('- Skip on serverless — the stream is in-process').dashes.count).toBe(1)
})
})

describe('bullet frames', () => {
describe('bullet frames, symbols', () => {
it('reads a bolded symbol as the symbol it is', () => {
const items = ['`message`', '`evlog`', '`dd`', '`service`', '`timestamp`']
const source = items.map(name => `- **${name}**: what the field carries and why`).join('\n')
Expand All @@ -108,7 +126,7 @@ describe('bullet frames', () => {
})
})

describe('epigram closers', () => {
describe('epigrams, twins', () => {
it('leaves a card body out of the rhythm', () => {
const source = ['::card-group', ' :::card', ' ---', ' title: Nuxt', ' ---', ' Auto-imported helpers. Zero config.', ' :::', '::'].join('\n')

Expand Down
Loading