CLI

evlog.config

One evlog.config.ts sets what evlog map checks, where evlog logs reads, and the sampling, redaction and drains your app imports.

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. The app runs it: the Nuxt and Nitro modules load it on their own, and any other app imports it.

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

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 },
})

evlog config prints the merged result, split by who applies each setting, with the line it is written on:

Terminal
evlog config
Output
evlog.config.ts · extends ./evlog.preset → evlog.preset.ts

CLI · applied 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

APP · applied by the app at runtime
  sampling.rates.info       25                                evlog.config.ts:9
  sampling.rates.debug      0                                 evlog.preset.ts:4
  redact.paths              ['user.password', 'card.number']  evlog.preset.ts:5
  service                   'checkout'                        evlog.config.ts:7
  drain                     createAxiomDrain()                evlog.config.ts:8

A value the file computes, like createAxiomDrain(), is shown as the code that produces it. --json returns the same settings as cli and app lists of { path, value, source }, with a computed value written as { "runtime": "createAxiomDrain()" }.

Where evlog finds the file

The CLI and the Nuxt and Nitro modules look for evlog.config.ts, evlog.config.mts, evlog.config.js and evlog.config.mjs, in that order, starting in the app's package 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.

SettingAcceptsFlagWhat it does
map.rules{ [id]: 'on' | 'off' }noneTurns a check off for every entry point, or back on when the preset turned it off
map.ignorelist of globsnoneLeaves the entry points whose file matches out of the map
map.minScorewhole number from 0 to 100--min-scoreExits 1 when the global score is below it
map.baselinetrue, a path, or git:<ref>--baselineExits 1 on a regression against the committed map, true meaning evlog.map.json

map.ignore matches the file an entry point is declared in. A Hono app that declares several routes in one file leaves them all out with one glob, and cannot leave out one of them alone.

The ids are the ones on 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:

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 next to the code.

Read logs from another directory

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

SettingAcceptsFlagWhat it does
logs.dira path--dirReads this directory instead of .evlog/logs
logs.limitwhole number of 1 or more--limitShows 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:

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:

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 configResult
A scalar or a function: service, drain, enrich, keepThe config's value replaces the preset's
An object: sampling.rates, routes, env, map.rulesMerged key by key, the config winning on each key
redact.paths, redact.patterns, sampling.keepThe preset's entries, then the config's
Any other list: map.ignore, include, excludeThe config's list replaces the preset's
pluginsMerged by name, a config plugin replacing the preset plugin of the same name
redact: falseRedaction off
redact: trueThe 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:

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.

Publish a preset for your organization

A preset package is one module and a package.json. Ship it as JavaScript, so every app can bundle it without compiling a dependency, and list evlog as a peer so the preset and the app share one copy:

{
  "name": "@acme/evlog-preset",
  "type": "module",
  "exports": "./index.mjs",
  "peerDependencies": {
    "evlog": ">=2.31.0"
  }
}

Every repository that extends it starts from the same redaction, sampling and CI gate. A new rule rolls out as a version bump of the preset, and an app that needs an exception writes it in its own config, where evlog config shows it next to the setting it overrides.

Use the config in your app

The app runs the file, so the values the CLI only lists, a drain, an enrich function, a rate read from process.env, apply there. map and logs are left out. How the file reaches the app depends on the framework:

FrameworkHow the config applies
Nuxt, Nitro, TanStack StartThe evlog module finds and loads it
Eve agentsdefineEvlogHook(config) in agent/hooks/evlog.ts
Next.jscreateEvlog(config) and createInstrumentation(toLoggerConfig(config))
Any other frameworktoLoggerConfig(config) where you call initLogger, toMiddlewareOptions(config) where you register the middleware

Nuxt and Nitro load it for you

The module looks the file up the same way the CLI does and bundles it into the server, so import.meta.dev and process.env work in it as they do in server code. Options passed to the module override the file, merged with the extends rules:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    sampling: { rates: { info: 100 } },
  },
})

This app keeps every info event whatever the file says, and the other rates in the file still apply. Keep a setting in one of the two places: evlog config lists only the file, so a value overridden in nuxt.config.ts is not the one it shows.

The drain, enrich and keep of the file run next to the evlog:drain, evlog:enrich and evlog:emit:keep hooks. A server plugin hooked there keeps working, and an event reaches both drains. A drain wrapped in createDrainPipeline is flushed when the server closes, so the file needs no close hook.

On Nuxt the file applies to the server. The browser logger keeps reading enabled, pretty, console, minLevel and transport from the evlog key in nuxt.config.ts.

Other apps import it

toLoggerConfig keeps the options initLogger takes, and toMiddlewareOptions keeps the ones a framework middleware takes:

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)))

An Eve agent spreads the config into its hook and adds its own options, see Eve.

The evlog/vite plugin does not read the file. Its auto-init is serialized at build time and cannot carry a drain, so import the config where you call initLogger instead.

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 reports the same error as a failing config check. Each error carries a code from the CLI's catalog:

CodeRaised when
cli.CONFIG_PARSE_FAILEDThe file has a syntax error
cli.CONFIG_NO_EXPORTThere is no default export of an object or defineEvlog({ ... })
cli.CONFIG_NOT_STATICThe default export, extends, or a map or logs value is computed at runtime
cli.CONFIG_INVALIDA setting is misspelt, has the wrong type, or turns off wide-event or context
cli.CONFIG_EXTENDS_NOT_FOUNDThe extends import does not lead to a file
cli.CONFIG_EXTENDS_DEPTHThe extended config extends another one

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

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: the ids map.rules takes, and what each check expects
  • CI: gate a pull request on the score