CLI

evlog init

Wire evlog into an existing app — an interactive setup that installs the package, registers the framework integration, and wires the destination you pick. Fully driveable by flags for CI and agents.

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.

Terminal
npx @evlog/cli init

On a terminal it reads your project, asks what it cannot infer, and shows you the plan before touching anything:

Interactive
┌  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

  1. Reads your project with the same analysis evlog map runs — framework, installed packages, errors you repeat, entry points with no audit trail. That is what the offers are built from.
  2. Installs evlog with the package manager your lockfile implies, unless it is already there. --no-install prints the command instead of running it.
  3. Registers the integration — the Nuxt module, the Nitro module, or the Next.js instrumentation files.
  4. Wires your destinations, dev and production branched in one place, with batching, enrichers and sampling when you asked for them.
  5. Runs evlog doctor before 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:

OfferAppears when
Error catalogThe scan found the same createError in more than one file
Audit actionsmap flagged sensitive entry points with no log.audit()
AI SDK loggingai is a dependency
Auth identitybetter-auth is a dependency
Batching and retryA production destination was picked — there is nothing to batch otherwise
Vite pluginThe 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:

ConditionWhy
--yesYou said so
--jsonThe payload is the contract; half a TUI in front of it helps nobody
stdin or stdout is not a TTYPiped, redirected, or spawned by a tool
CI is set to anything but false / 0A workflow runner has no keyboard
Terminal
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
Written for agents too. The non-interactive path can express every choice the prompts can, so an agent reproduces exactly what a human just did — and it can never end up waiting on a keystroke that is not coming. An unknown --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 modules key 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:

Output
· 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.

nuxt.config.ts
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.

nitro.config.ts
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.

lib/evlog.ts
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.

IdDestinationNeeds
fsLocal files under .evlog/logs (dev default)nothing
axiomAxiomAXIOM_DATASET, AXIOM_API_KEY
otlpAny OpenTelemetry collectorOTEL_EXPORTER_OTLP_ENDPOINT
posthogPostHogPOSTHOG_API_KEY
sentrySentrySENTRY_DSN
better-stackBetter StackBETTER_STACK_API_KEY
datadogDatadogDATADOG_API_KEY, DATADOG_SITE
hyperdxHyperDXHYPERDX_API_KEY
nonePretty console output onlynothing

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:

server/plugins/evlog-drain.ts
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

IdWhat you get
enrichersPick from user agent, geo, request size, trace context (--enrichers)
pipelineBatching and retry instead of one HTTP call per request
samplingA rate profile, see below (--sampling)
error-catalogA typed catalog seeded with the errors you already repeat
audit-catalogTyped audit actions for the sensitive routes with no trail
aiToken usage, tool calls and cost on AI SDK generations
better-authThe signed-in user on every event
viteThe 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.

IdInfoWarningsFor
alleverythingeverythingThe right answer until volume or cost says otherwise
low50%100%A small app that has started to repeat itself
medium25%100%Steady traffic with a bill worth watching
high10%100%Info is most of what you are paying for
very-high1%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.

nuxt.config.ts
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:

server/utils/errors.ts
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:

Terminal
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:

server/plugins/evlog-drain.ts
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

FlagWhat it does
--yes, -ySkip 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-runPrint the plan, write nothing
--no-installDo not run the package manager; print the command instead
--cwd <dir>Set up another app in the workspace
--jsonThe plan and result as JSON on stdout (implies non-interactive)

Next

  • evlog doctor — confirm the wiring resolves and logs are being written
  • evlog map — score what is still dark now that evlog is in place
  • Quick Start — the concepts behind the wiring