evlog init
evlog map tells you which entry points are dark. evlog init is what you run before that — it does the setup the framework guide describes, in the project you are standing in.
npx @evlog/cli init
On a terminal it reads your project, asks what it cannot infer, and shows you the plan before touching anything:
┌ evlog init checkout
│
◇ Detected Nuxt
│
◇ Service name on every wide event
│ checkout
│
◇ In development, where should events go?
│ Local files
│
◇ And in production?
│ Axiom, Sentry
│
◇ Anything else?
│ Context
│ ◻ Request enrichers
│ Delivery
│ ◻ Batching and retry
│ ◻ Sampling
│ Catalogs
│ ◼ Error catalog · 3 repeated errors found
│ ◻ Audit actions · 2 sensitive routes with no trail
│
◇ Plan ───────────────────────────────────╮
│ │
│ run pnpm add evlog │
│ update nuxt.config.ts │
│ create server/plugins/evlog-drain.ts │
│ create server/plugins/evlog-enrich.ts │
│ │
├──────────────────────────────────────────╯
│
◇ Apply?
│ Yes
│
◇ 4 ok · 1 warn · 0 fail
│
◇ Set these before anything is received ─────────────╮
│ │
│ AXIOM_DATASET dataset to write to │
│ AXIOM_API_KEY API token with ingest permission │
│ │
├─────────────────────────────────────────────────────╯
│
│ verify evlog doctor
│ score evlog map
│
└ Nuxt wired · evlog.dev/integrate/frameworks/nuxt
The production picker is a fuzzy search — type ax for Axiom — and takes more than one destination: the same event fans out to each. Extras are grouped and only list what your project can actually use, with the evidence that put them there.
What it does
- Reads your project with the same analysis
evlog mapruns — framework, installed packages, errors you repeat, entry points with no audit trail. That is what the offers are built from. - Installs
evlogwith the package manager your lockfile implies, unless it is already there.--no-installprints the command instead of running it. - Registers the integration — the Nuxt module, the Nitro module, or the Next.js instrumentation files.
- Wires your destinations, dev and production branched in one place, with batching, enrichers and sampling when you asked for them.
- Runs
evlog doctorbefore it finishes, so the run answers "did it work" instead of telling you to go and check.
Everything it writes is reported line by line, including the parts that were already in place.
It only offers what it can back up
An option that does not apply is not shown, and one that is shown says why:
| Offer | Appears when |
|---|---|
| Error catalog | The scan found the same createError in more than one file |
| Audit actions | map flagged sensitive entry points with no log.audit() |
| AI SDK logging | ai is a dependency |
| Auth identity | better-auth is a dependency |
| Batching and retry | A production destination was picked — there is nothing to batch otherwise |
| Vite plugin | The framework is Vite-based |
Passing one of these as a flag against a project that cannot use it drops it and says so, rather than wiring something inert.
Non-interactive
Prompts are skipped — and every answer comes from flags and defaults — whenever anything suggests nobody is watching:
| Condition | Why |
|---|---|
--yes | You said so |
--json | The payload is the contract; half a TUI in front of it helps nobody |
| stdin or stdout is not a TTY | Piped, redirected, or spawned by a tool |
CI is set to anything but false / 0 | A workflow runner has no keyboard |
evlog init --yes --prod-drain axiom,sentry --extras pipeline,enrichers
evlog init --yes --extras sampling --sampling high
evlog init --json --drain none # plan and result as JSON, nothing to read
--drain or --extras value stops the run rather than falling back to a default, because silently wiring the wrong destination is worse than a failed command.It will not overwrite your code
init appends; it never rewrites. Concretely:
- A config file is patched at the exact offsets of the node it is adding to. Your comments, quote style, and formatting survive — nothing is reprinted from an AST.
- A file that already exists is left alone and reported as already present. There is no
--force. - Anything it cannot do safely becomes a manual step with the snippet to paste and the reason it stopped. A
moduleskey built from a variable is the common case: there is no correct place to splice a string into an array the CLI cannot see.
Which makes it safe to run twice. The second run reports what is already wired and writes nothing:
· nuxt.config.ts already registers evlog/nuxt
· nuxt.config.ts already has an evlog block
· server/plugins/evlog-drain.ts already exists
Use --dry-run first if you would rather see the plan before anything is touched.
Per framework
Nuxt
Adds 'evlog/nuxt' to modules and an evlog block with your service name. useLogger, createError, and parseError are auto-imported from there.
export default defineNuxtConfig({
modules: ['@nuxt/ui', 'evlog/nuxt'],
evlog: {
env: { service: 'checkout' },
},
})
Nitro
Adds the import and the module call, picking the subpath that matches your major — evlog/nitro/v3 for Nitro 3, evlog/nitro for nitropack. Creates nitro.config.ts when there is none.
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
modules: [
evlog({
env: { service: 'api' },
}),
],
})
Next.js
Creates instrumentation.ts and lib/evlog.ts — under src/ when your app lives there, since Next only loads the one that matches.
Wrapping handlers stays yours: Next has no ambient request logger, so each route opts in with withEvlog(). init prints the snippet.
import { createEvlog } from 'evlog/next'
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'web',
})
TanStack Start
Writes the Nitro v3 config with experimental.asyncContext enabled — without it useRequest() returns nothing and you get an install that looks complete and logs no business context.
The evlogErrorHandler middleware on your root route is a manual step: __root.tsx is a component file, and splicing into it is guesswork.
Destinations
Two questions rather than one list: nobody sends local development traffic to Axiom, and nobody reads production logs off the box's filesystem. --drain sets the development sink (fs or none); --prod-drain takes one or more hosted destinations.
| Id | Destination | Needs |
|---|---|---|
fs | Local files under .evlog/logs (dev default) | nothing |
axiom | Axiom | AXIOM_DATASET, AXIOM_API_KEY |
otlp | Any OpenTelemetry collector | OTEL_EXPORTER_OTLP_ENDPOINT |
posthog | PostHog | POSTHOG_API_KEY |
sentry | Sentry | SENTRY_DSN |
better-stack | Better Stack | BETTER_STACK_API_KEY |
datadog | Datadog | DATADOG_API_KEY, DATADOG_SITE |
hyperdx | HyperDX | HYPERDX_API_KEY |
none | Pretty console output only | nothing |
Picking several production destinations fans the same event out to each. The generated plugin branches on the environment in one place, so "which drain runs where" is never something to reconstruct:
const drains = import.meta.dev
? [createFsDrain()]
: [pipeline(createAxiomDrain()), pipeline(createSentryDrain())]
Batching wraps the network sends only — buffering a local file write adds latency to the one loop where you want the event on screen immediately.
init never asks for a secret. It prints which environment variables the adapter reads and leaves them to you. A setup command that prompts for an API token is one that writes a credential into a file it chose, and the answer lands in your shell history on the way there.Extras
| Id | What you get |
|---|---|
enrichers | Pick from user agent, geo, request size, trace context (--enrichers) |
pipeline | Batching and retry instead of one HTTP call per request |
sampling | A rate profile, see below (--sampling) |
error-catalog | A typed catalog seeded with the errors you already repeat |
audit-catalog | Typed audit actions for the sensitive routes with no trail |
ai | Token usage, tool calls and cost on AI SDK generations |
better-auth | The signed-in user on every event |
vite | The Vite plugin that strips log.debug() from production builds |
An extra that does not apply is dropped and reported, not refused — so --extras vite,enrichers does the right thing across a monorepo of mixed apps.
Sampling presets
Named by the traffic your app takes. Info is what moves — it is the bulk of the volume and the bulk of the bill — and warnings only give way at the top, where halving them still leaves the shape.
| Id | Info | Warnings | For |
|---|---|---|---|
all | everything | everything | The right answer until volume or cost says otherwise |
low | 50% | 100% | A small app that has started to repeat itself |
medium | 25% | 100% | Steady traffic with a bill worth watching |
high | 10% | 100% | Info is most of what you are paying for |
very-high | 1% | 50% | Trends rather than individual requests |
Errors are kept at 100% in every tier — a sampling config that drops errors hides the only events anybody reads at three in the morning. The generated config states it explicitly so the invariant is visible where somebody would otherwise wonder.
debug is never sampled, and never appears in the config. An unspecified level is kept in full, and debug events only exist because somebody turned them on to chase something: a 5% sample of the logs you switched on to investigate a problem is a 5% chance of seeing the line you needed. Production builds strip log.debug() anyway, so there is rarely volume there to sample.
evlog: {
env: { service: 'shop' },
sampling: {
rates: { info: 10, warn: 100, error: 100 },
},
}
Catalogs are seeded, not scaffolded
error-catalog does not write a template with the names filled in. It writes the errors your code already repeats, with the prose you already wrote:
export const shopErrors = defineErrorCatalog('shop', {
/** Currently written inline in server/api/checkout.post.ts, server/api/refund.post.ts */
CARD_DECLINED: {
status: 402,
message: 'Card declined',
why: 'The issuer refused it',
fix: 'TODO: what they should do about it',
},
})
audit-catalog works the same way, naming an action per entry point map flagged as sensitive and untracked. Both land as a to-do list with the types already written, so the migration is a find and replace rather than a design exercise.
Monorepos
Run from a workspace root and init sets up the apps rather than the root package, which serves no traffic. It lists the workspace packages that have a detectable framework — a shared utils package has no entry points to instrument — and asks which to wire:
evlog init # pick from the list
evlog init --yes --apps apps/web,apps/api
Each app keeps its own service name, taken from its package.json.
The local sink
On Nitro-based apps init writes a drain plugin; on Next.js it adds the drain to lib/evlog.ts. The filesystem drain — and only that one — is gated to development:
import { createFsDrain } from 'evlog/fs'
const drain = createFsDrain()
export default defineNitroPlugin((nitroApp) => {
// Local files are a development convenience — never a production sink.
if (!import.meta.dev) return
nitroApp.hooks.hook('evlog:drain', drain)
})
A drain that writes files writes them on whatever box serves the request. Turning one on in production without being asked is not a decision a setup command should make for you. Every hosted destination is generated without the guard, because that is what you picked it for.
The directory it writes to is .evlog/logs, and evlog gitignores it on first write. It is also what evlog doctor looks for.
Flags
| Flag | What it does |
|---|---|
--yes, -y | Skip every question and take the defaults |
--framework <name> | Override detection (nuxt, nitro, next, tanstack-start) |
--service <name> | Service name on every wide event (default: your package name, unscoped) |
--drain <id> | Development sink: fs (default) or none |
--prod-drain <a,b> | Production destinations — see the table above |
--extras <a,b> | Comma-separated, see Extras |
--enrichers <a,b> | user-agent, geo, request-size, trace-context (default: all) |
--sampling <id> | all, low, medium (default), high, very-high |
--apps <a,b> | Workspace packages to set up (monorepo root only) |
--dry-run | Print the plan, write nothing |
--no-install | Do not run the package manager; print the command instead |
--cwd <dir> | Set up another app in the workspace |
--json | The plan and result as JSON on stdout (implies non-interactive) |
Next
evlog doctor— confirm the wiring resolves and logs are being writtenevlog map— score what is still dark now that evlog is in place- Quick Start — the concepts behind the wiring
Overview
The evlog command line — map your app's observability coverage, diagnose your setup, and control telemetry. Install, global flags, output streams, exit codes, and monorepo behaviour.
map
A static observability score for your app, Lighthouse-style. Scans every entry point in a Nuxt, Nitro, Next.js, or TanStack Start project, scores wide-event coverage, and names the entry points to fix first.