Skip to content
Open
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
13 changes: 8 additions & 5 deletions .agents/skills/create-map-rule/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ Current requirements: `wide-event` (40), `audit` (25), `structured-errors` (20),
|---|------|--------|
| 1 | `packages/cli/src/lib/map/rules/{id}.ts` | Create the rule (one exported const) |
| 2 | `packages/cli/src/lib/map/rules/index.ts` | Import + one line in `REGISTRY` |
| 3 | `packages/cli/src/lib/map/types.ts` | Add the id to the `CheckId` union (a type assert in `index.ts` fails the build if the registry and union drift) |
| 3 | `packages/cli/src/lib/map/types.ts` + `packages/evlog/src/shared/define.ts` | Add the id to the `CheckId` union and to `EvlogMapRuleId`, which types `map.rules` in `evlog.config.ts` (type asserts in `index.ts` fail the build if the registry and either union drift) |
| 4 | `packages/cli/test/map/rules.test.ts` | Add cases (the file has an ESLint-`RuleTester`-style `Case` harness (`runRuleSet` exercises one rule in isolation)) |
| 5 | `apps/docs/content/3.cli/3.rules.md` | Add a row to the Requirements or Opportunities table + a `### {title}` section |
| 6 | `apps/docs/content/3.cli/4.scoring.md` | Requirements only: reflect the new weight in the scoring explanation |
| 7 | `skills/review-logging-patterns/references/code-review.md` | Add a row to the matching rules table |
| 8 | `.changeset/{id}-map-rule.md` | Changeset for `"@evlog/cli": minor` |
| 8 | `.changeset/{id}-map-rule.md` | Changeset for `"@evlog/cli": minor` and `"evlog": minor` (`map.rules` accepts the new id) |

**Important**: Do NOT consider the task complete until all applicable touchpoints have been addressed.

Expand Down Expand Up @@ -95,10 +95,12 @@ Key rules:
- **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.

## Steps 2 and 3: Registry + CheckId
## Steps 2 and 3: Registry, CheckId and EvlogMapRuleId

Add the import and one `REGISTRY` line in `rules/index.ts` (report order matters: requirements before opportunities, heaviest first), and the id to the `CheckId` union in `types.ts`. The `AssertIdsMatch` type in `index.ts` fails the build if you forget either side.

The id also goes in `EvlogMapRuleId` in `packages/evlog/src/shared/define.ts`, the type behind `map.rules` in `evlog.config.ts`. `AssertConfigIdsMatch` in `index.ts` checks it against `CheckId`. The CLI reads that type from the built `evlog` package, so rebuild `evlog` before the CLI typecheck.

## Step 4: Tests

`packages/cli/test/map/rules.test.ts` has a declarative `Case` harness: source code in, expected check results out, with knobs for `kind`, `framework`, `path` (sensitivity), `hasEvlog`, `features`, `pairable`, `dependencies`, `catalogs`, `barrels`. Use `runRuleSet([yourRule], run)` to exercise the rule in isolation.
Expand All @@ -124,13 +126,14 @@ Read `apps/docs/AGENTS.md` before touching anything under `apps/docs/`. Then in

## Step 8: Changeset

`.changeset/{id}-map-rule.md` with `"@evlog/cli": minor`, written from the user's perspective: what the rule checks, when it fires, whether it moves the score.
`.changeset/{id}-map-rule.md` with `"@evlog/cli": minor` and `"evlog": minor`, written from the user's perspective: what the rule checks, when it fires, whether it moves the score.

## Verification

```bash
pnpm --filter evlog run build # the CLI typechecks against evlog's built types
pnpm --filter @evlog/cli run lint
pnpm --filter @evlog/cli run typecheck # catches REGISTRY/CheckId drift
pnpm --filter @evlog/cli run typecheck # catches REGISTRY/CheckId/EvlogMapRuleId drift
pnpm --filter @evlog/cli run test
```

Expand Down
5 changes: 5 additions & 0 deletions .changeset/cli-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@evlog/cli": minor
---

`evlog map` and `evlog logs` read `evlog.config.ts` (or `.mts`, `.js`, `.mjs`), the nearest one from the package up to the workspace root, without running it. `map.rules` turns checks off or back on for every entry point, `map.ignore` leaves entry points out by file glob and `--json` reports how many as `ignored`, `map.minScore` and `map.baseline` gate like `--min-score` and `--baseline`, and `logs.dir` and `logs.limit` set the defaults of `--dir` and `--limit`. A flag wins over the config. The config can extend a local file or a published preset, one level deep. `evlog config` prints the settings that apply, grouped into service, sampling, redaction, pipeline and CLI, with the file and line each one comes from and evlog's defaults where the file says nothing, `--json` for the same as JSON, and `evlog doctor` reports a config it cannot read. A misspelt setting, an `extends` written as a path instead of an imported config, a value under `map` or `logs` computed at runtime, or turning off `wide-event` or `context` stops the command with a `cli.CONFIG_*` error. The CLI now needs `evlog` 2.31.0 or later.
11 changes: 10 additions & 1 deletion apps/docs/content/3.cli/0.overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The `evlog` executable ships with the `evlog` package. It runs separately from y

Start with [`evlog map`](/cli/map) to check supported entry points for evlog logging patterns and get a static observability score with suggested fixes. It writes `evlog.map.json` unless you pass `--no-write`. The score describes recognized source patterns, not the logs your handlers produce in production.

[`evlog init`](/cli/init) adds evlog configuration to the app. [`evlog logs`](/cli/logs) reads the events the fs drain wrote: the latest requests, the failures, the slowest, or one request in full. [`evlog agents`](/cli/agents) writes logging conventions for the AI agents working in the repository.
[`evlog init`](/cli/init) adds evlog configuration to the app. [`evlog logs`](/cli/logs) reads the events the fs drain wrote: the latest requests, the failures, the slowest, or one request in full. [`evlog agents`](/cli/agents) writes logging conventions for the AI agents working in the repository. [`evlog.config.ts`](/cli/config) holds the settings `evlog map` and `evlog logs` apply, next to the sampling and redaction the app imports.

::warning{icon="i-lucide-flask-conical"}
**Early days.** `evlog map` has adapters for Nuxt, Nitro, Next.js App Router, TanStack Start, Hono, Express, and Fastify. Each adapter recognizes specific entry-point shapes. Check the detected framework and entry-point count before interpreting the score. Rules can change between releases, so [pin the version](/cli/ci#pin-the-version) when you gate CI on the number.
Expand Down Expand Up @@ -98,6 +98,15 @@ The CLI requires Node 22 or later.
:::
:::card
---
icon: i-lucide-settings-2
title: config
to: /cli/config
color: neutral
---
Show the evlog.config that applies here and where each setting comes from.
:::
:::card
---
icon: i-lucide-bar-chart-3
title: telemetry
to: /cli/telemetry
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/content/3.cli/10.logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Filters compose with any view.

A time, level, status or limit that cannot be read stops the command (exit 2) rather than silently widening the query.

`--dir` and `--limit` can be set once for the project in [`evlog.config.ts`](/cli/config), as `logs.dir` and `logs.limit`. The flag wins when both are set.

```bash [Terminal]
evlog logs errors --since 1h
evlog logs slow --over 2s --path /api/reports
Expand Down
238 changes: 238 additions & 0 deletions apps/docs/content/3.cli/11.config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
---
title: evlog.config
description: "One evlog.config.ts sets what evlog map checks, where evlog logs reads, and the sampling, redaction and drains your app imports."
navigation:
title: config
icon: i-lucide-settings-2
links:
- label: Map rules
icon: i-lucide-list-checks
to: /cli/rules
color: neutral
variant: subtle
- label: Sampling
icon: i-lucide-filter
to: /learn/sampling
color: neutral
variant: subtle
---

Without a config file, the CI gate is a flag on every `evlog map` run, a check you decided not to care about is disabled file by file, and sampling and redaction live in whichever file calls `initLogger`. `evlog.config.ts` holds all of it, and a preset carries it from one repository to the next. The CLI reads the file without running it, and the app imports it like any other module.

Here an app builds on a shared preset, turns a check back on, and leaves its dev routes out of the map:

::code-group
```ts [evlog.config.ts]
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import preset from './evlog.preset'

export default defineEvlog({
extends: preset,
service: 'checkout',
drain: createAxiomDrain(),
sampling: { rates: { info: 25 } },
map: {
rules: { 'audit-coverage': 'on' },
ignore: ['src/routes/_dev/**'],
},
logs: { limit: 100 },
})
```

```ts [evlog.preset.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
sampling: { rates: { info: 10, debug: 0 } },
redact: { paths: ['user.password', 'card.number'] },
map: { rules: { 'error-catalog': 'off', 'audit-coverage': 'off' }, minScore: 70 },
})
```
::

`evlog config` prints the merged result grouped by what each setting does, with the line it is written on, and fills in what evlog uses where the file says nothing:

```bash [Terminal]
evlog config
```

```text [Output]
evlog.config.ts Β· extends ./evlog.preset β†’ evlog.preset.ts

Service
service checkout evlog.config.ts:7
environment from NODE_ENV default

Sampling
trace 0% default
debug 0% evlog.preset.ts:4
info 25% evlog.config.ts:9
warn 100% default
error 100% default
fatal always default

Redaction
redact on evlog.preset.ts:5
builtins creditCard, email, ipv4, phone, jwt, bearer, iban default
paths user.password evlog.preset.ts:5
card.number evlog.preset.ts:5

Pipeline
drain createAxiomDrain() evlog.config.ts:8

CLI Β· read by evlog map and evlog logs
map.rules.error-catalog 'off' evlog.preset.ts:6
map.rules.audit-coverage 'on' evlog.config.ts:11
map.minScore 70 evlog.preset.ts:6
map.ignore ['src/routes/_dev/**'] evlog.config.ts:12
logs.limit 100 evlog.config.ts:14
```

Each redaction path keeps the line of the file that adds it, so a path the preset redacts never looks like the app's own. With `routes`, the Service section becomes Services and lists each route's service in the order evlog matches them, the first match winning. A rate that changes nothing gets a warning under its row, such as a `fatal` rate, since fatal events are always kept.

A value the file computes, like `createAxiomDrain()`, is shown as the code that produces it. `--json` returns the settings written in the files as `cli` and `app` lists of `{ path, value, source }`, one entry per item of `redact.paths`, `redact.patterns` and `sampling.keep`, with a computed value written as `{ "runtime": "createAxiomDrain()" }` and a regular expression as `{ "regexp": "/acct_\\w+/g" }`.

## Where the CLI finds the file

The CLI looks for `evlog.config.ts`, `evlog.config.mts`, `evlog.config.js` and `evlog.config.mjs`, in that order, starting in the package it runs on and walking up to the workspace root. The first file found applies on its own. Configs do not cascade, so an app with its own `evlog.config.ts` ignores the one at the root unless it extends it.

Paths in `map.ignore`, `map.baseline` and `logs.dir` are relative to the package being mapped or read, not to the config file. One config at the root of a monorepo therefore fits every app in it.

## Gate the map from the config

`evlog map` reads the `map` section, and a flag passed on the command line wins over the same setting.

| Setting | Accepts | Flag | What it does |
| --- | --- | --- | --- |
| `map.rules` | `{ [id]: 'on' \| 'off' }` | none | Turns a check off for every entry point, or back on when the preset turned it off |
| `map.ignore` | list of globs | none | Leaves the entry points whose file matches out of the map |
| `map.minScore` | whole number from 0 to 100 | `--min-score` | Exits 1 when the global score is below it |
| `map.baseline` | `true`, a path, or `git:<ref>` | `--baseline` | Exits 1 on a regression against the committed map, `true` meaning `evlog.map.json` |

The ids are the ones on [Rules](/cli/rules). Every check can be turned off except `wide-event` and `context`, because the map sorts entry points into instrumented, partial and dark by them. Leave those entry points out with `map.ignore` instead.

A check turned off in the config becomes `n/a` on every entry point, with `turned off in evlog.config` as its message in `--json`. The report says what the config changed above the score, and names the setting the gate came from:

```text [Output]
evlog.config.ts: error-catalog off, 1 entry point ignored
β–ˆβ–€β–ˆ β–€β–€β–ˆ score /100 checkout Β· Hono
β–ˆβ–€β–ˆ β–€β–ˆ β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–±β–±β–± 2 entry points scanned
β–€β–€β–€ β–€β–€β–€ good β–†β–ˆ

GATE score 83 meets map.minScore 70 β€” exit code 0
```

`evlog map --min-score 95` on the same project gates on 95 and says `--min-score 95`. To turn a check off for one handler rather than the whole project, keep using a [disable comment](/cli/rules#disabling-a-check) next to the code.

## Read logs from another directory

`evlog logs` reads the `logs` section, and its flags win the same way.

| Setting | Accepts | Flag | What it does |
| --- | --- | --- | --- |
| `logs.dir` | a path | `--dir` | Reads this directory instead of `.evlog/logs` |
| `logs.limit` | whole number of 1 or more | `--limit` | Shows at most this many events |

`--format`, `--verbose`, and `--limit` on `evlog map` stay flags only. They describe one run, not the project.

## Write values the CLI can read

The CLI parses `evlog.config.ts` and never runs it, so every value under `map` and `logs` has to be a literal, a `const`, or a value imported from a local file. A call, an environment variable, or a value imported from a package stops the command:

```text [Output]
logs.limit in evlog.config.ts:14 is computed at runtime
β†’ Write the value inline, as a const, or import it from a local file
```

The rest of the file is for the app and can compute anything: a drain, an `enrich` function, a sampling rate read from `process.env`. The CLI lists those values without evaluating them.

## Share settings with `extends`

`extends` takes another config, imported from a local file or from a package. A preset published to npm is an ordinary module whose default export is `defineEvlog({ ... })`, so a team installs it and extends it:

```ts [evlog.config.ts]
import { defineEvlog } from 'evlog'
import preset from '@acme/evlog-preset'

export default defineEvlog({
extends: preset,
service: 'checkout',
})
```

The CLI follows the package's `exports` to the file it ships and reads it the same way, so the preset's `map` and `logs` have to be literals too.

Settings merge by kind:

| In the config | Result |
| --- | --- |
| A scalar or a function: `service`, `drain`, `enrich`, `keep` | The config's value replaces the preset's |
| An object: `sampling.rates`, `routes`, `env`, `map.rules` | Merged key by key, the config winning on each key |
| `redact.paths`, `redact.patterns`, `sampling.keep` | The preset's entries, then the config's |
| Any other list: `map.ignore`, `include`, `exclude` | The config's list replaces the preset's |
| `plugins` | Merged by `name`, a config plugin replacing the preset plugin of the same name |
| `redact: false` | Redaction off |
| `redact: true` | The preset's redact settings, unchanged |

Redaction paths and kept events add up rather than being replaced, so an app that lists its own paths cannot drop the ones the preset redacts.

A config extends one level only. When `evlog.preset.ts` itself extends a config, extending it fails and names both files:

```text [Output]
./evlog.preset extends another config, so evlog.config.ts:6 cannot extend it
β†’ Extend the config it extends directly, or copy the settings you need into one of the two files
```

Every setting is then at most one file away from where it applies, and `evlog config` names that file.

`extends` takes the config itself, the value the app merges at runtime, so a path string is refused rather than resolved:

```text [Output]
extends in evlog.config.ts:4 is the path './evlog.preset', not a config
β†’ Import the config from that path as base, then set extends: base
```

## Use the config in your app

Nothing loads `evlog.config.ts` at runtime: the app imports it. `toLoggerConfig` keeps the options `initLogger` takes, `toMiddlewareOptions` keeps the ones a framework middleware takes, and both leave `map` and `logs` out:

```ts [src/index.ts]
import { Hono } from 'hono'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

const app = new Hono<EvlogVariables>()
app.use(evlog(toMiddlewareOptions(config)))
```

The Nuxt and Nitro modules do not read `evlog.config.ts`. Their options stay in `nuxt.config.ts` or `nitro.config.ts`, and the file there carries the `map` and `logs` settings the CLI applies.

## When the config cannot be read

`evlog map` and `evlog config` exit 1 on a config they cannot read, and `evlog logs` exits 2. [`evlog doctor`](/cli/doctor) reports the same error as a failing `config` check. Each error carries a code from the CLI's catalog:

| Code | Raised when |
| --- | --- |
| `cli.CONFIG_PARSE_FAILED` | The file has a syntax error |
| `cli.CONFIG_NO_EXPORT` | There is no default export of an object or `defineEvlog({ ... })` |
| `cli.CONFIG_NOT_STATIC` | The default export, `extends`, or a `map` or `logs` value is computed at runtime |
| `cli.CONFIG_INVALID` | A setting is misspelt, has the wrong type, or turns off `wide-event` or `context` |
| `cli.CONFIG_EXTENDS_NOT_FOUND` | The `extends` import does not lead to a file |
| `cli.CONFIG_EXTENDS_DEPTH` | The extended config extends another one |
| `cli.CONFIG_EXTENDS_STRING` | `extends` is a path string instead of an imported config |

A misspelt key is an error rather than a setting quietly ignored:

```text [Output]
logs.limt in evlog.config.ts:14 is not a setting; expected dir, limit
β†’ Use a setting and a value the config reference lists
```

## Next

- [Rules](/cli/rules): the ids `map.rules` takes, and what each check expects
- [CI](/cli/ci): gate a pull request on the score
4 changes: 4 additions & 0 deletions apps/docs/content/3.cli/2.map.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ export default defineEventHandler(() => ({ ok: true }))

The check becomes `n/a` with your reason attached, so it stops costing score, and the report says how many checks the project has disabled, so a high score never hides an app that logs nothing. Full syntax on [Rules](/cli/rules#disabling-a-check).

A check the whole project does without, like `error-catalog` in an app with no error catalog, is turned off once in [`evlog.config.ts`](/cli/config) with `map.rules`. `map.ignore` leaves whole entry points out, by their file. The report says how many it left out, and `--json` carries the count as `ignored`.

## Flags

| Flag | Default | What it does |
Expand All @@ -313,6 +315,8 @@ The check becomes `n/a` with your reason attached, so it stops costing score, an
| `--format <name>` | human | `github` writes [workflow annotations](/cli/ci#without-the-action) to stdout |
| `--limit <n>` | 10 | Most annotations `--format github` emits; GitHub keeps ten per step |

`--min-score` and `--baseline` can be set once for the project in `evlog.config.ts`, as `map.minScore` and `map.baseline`. The flag wins when both are set.

## evlog.map.json

Every run writes `evlog.map.json` to the project root: the score, the framework, the CLI version and rule-set version that wrote it, and every entry point with its checks, its suggestions, its sensitivity, and its own score. It is the same data `--json` prints.
Expand Down
Loading
Loading