CLI

evlog logs

Read the wide events your app wrote to .evlog/logs from the terminal — the latest requests, the failures, the slowest, or one request in full.

evlog logs reads the events the file system drain wrote and shows them the way you would want to read them: one line per request, the thing that mattered at the end of it, and one request in full when you have its id. It reads files; it never touches the app.

Terminal
evlog logs
Output
6 events · .evlog/logs

08:00:00  GET    /api/health                                 200      2ms
10:00:00  POST   /api/checkout                               402    412ms  ✗ Error: Payment processing failed · Card declined by issuer
10:30:00  GET    /api/reports                                200    1.2s   report.id=r-1
11:00:00  POST   /api/refund                                 200     80ms  audit billing.refund user:usr_42 → invoice:inv_1 success
11:30:00  GET    /api/items                                  500     30ms
11:45:00  GET    /api/items                                  warn   700ms  user.id=usr_7

evlog logs errors — the 2 that failed · evlog logs <requestId> — one request in full · evlog logs slow — worst first

The newest event is at the bottom, like tail. Nothing is written: the fs drain writes on every request and this reads what it wrote, both the compact and the pretty: true layout, across the dated files.

Five views

CommandShows
evlog logsThe last 50 events, oldest first
evlog logs errorsEvents that failed: a 5xx status, an error or fatal level, or an error block
evlog logs slowEvents over 500ms, worst first (--over 1s to move the bar)
evlog logs <requestId>Every event carrying that id, in full: request, error with why and fix, audit record, then the business fields. The first block of a UUID is enough
evlog logs statsThe shape of the traffic: per route, count, errors, p50 and p95, routes with the most errors first; then counts by status class and by level

Filters

Filters compose with any view.

FlagWhat it does
--since <when>Only events after: a duration back from now (15m, 2h, 3d) or a date (2026-10-01, 2026-10-01T09:00)
--until <when>Only events before, same spellings
--level <a,b>Only these levels: trace, debug, info, warn, error, fatal
--path <path>Only requests on this exact path
--status <n>Only this status (404) or class (4xx)
--where <clause>Only events where a field matches, see below; repeat the flag for several clauses, all must hold
--over <duration>For slow: what a request has to exceed (default 500ms)
--limit <n>Most events to show (default 50)
--dir <path>Read another directory (default: the project's .evlog/logs; in a workspace with no logs at the root, every apps/*/.evlog/logs and the like, merged by time with the app in a column)
--url <endpoint>Read the memory drain's dev endpoint instead of files: a JSON array of events, or { "events": [...] }
-f, --followKeep reading as the app writes, like tail -f; Ctrl-C stops
--jsonThe events as JSON on stdout
--cwd <dir>Another project in the workspace

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

Terminal
evlog logs errors --since 1h
evlog logs slow --over 2s --path /api/reports
evlog logs --status 5xx --level error,fatal --limit 20
evlog logs -f --path /api/checkout

--where: any field on the event

A clause is a dotted field, an operator, and a value. Numbers compare as numbers, everything else as text, and the field can sit anywhere in the event, which is the point of a wide event: the business fields are there to be queried.

ClauseMatches when
user.id=usr_42the field equals the value (true/false and numbers are read as such)
audit.outcome!=successthe field differs
payment.amount>5000greater; also >=, <, <=
error.message~declinedthe field matches the regular expression, case-insensitive; an object is matched as JSON
auditthe field is present
!errorthe field is absent
Terminal
evlog logs --where 'payment.amount>5000' --where audit.outcome=failure
evlog logs errors --where 'error.data.why~card declined'
evlog logs stats --where user.plan=pro
evlog logs -f --where '!error' --where 'durationMs>1000'

Quote the whole clause whenever it carries >, < or !, which the shell reads as redirection or history before evlog ever sees them, and whenever the value has a space: 'payment.amount>5000', '!error', 'error.data.why~card declined'.

For agents

--json returns one envelope: the directory, the view, how many events matched, and the events shown.

Terminal
evlog logs errors --since 30m --json
{
  "schemaVersion": 2,
  "dir": "/app/.evlog/logs",
  "view": "errors",
  "count": 2,
  "matched": 2,
  "events": [ { "timestamp": "…", "path": "/api/checkout", "status": 402, "error": { "data": { "why": "…", "fix": "…" } } } ]
}

stats --json carries stats (total, errors, byRoute, byStatus, byLevel) in place of events. With --follow --json, each new event is one JSON line on stdout, since a stream has no end. The analyze-logs skill calls evlog logs first and falls back to reading the files only when the CLI is not available.

A workspace, and a running app

Run from a monorepo root with no .evlog/logs of its own, it reads every app that has one (apps/*, packages/*, examples/*, services/*), merges the events by time, and shows the app in a column. --cwd apps/web reads one app; --dir reads one directory.

An app on the memory drain (Cloudflare Workers, where there is no file system) has no files to read, but it can expose readMemoryLogs() on a dev route. Point --url at it; every view and filter works the same, and -f polls it once a second. A failed poll is a gap rather than the end, since the app restarting under a follower is normal; if the endpoint stays quiet the run says so once and keeps trying.

Terminal
evlog logs errors --url http://localhost:8787/_evlog/logs

What it will not do

  • It reads local files and a dev endpoint. A remote drain (Axiom, Datadog, …) has its own query language and UI; this command does not wrap them.

Next