evlog logs
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.
evlog logs
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
| Command | Shows |
|---|---|
evlog logs | The last 50 events, oldest first |
evlog logs errors | Events that failed: a 5xx status, an error or fatal level, or an error block |
evlog logs slow | Events 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 stats | The 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.
| Flag | What 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, --follow | Keep reading as the app writes, like tail -f; Ctrl-C stops |
--json | The 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.
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.
| Clause | Matches when |
|---|---|
user.id=usr_42 | the field equals the value (true/false and numbers are read as such) |
audit.outcome!=success | the field differs |
payment.amount>5000 | greater; also >=, <, <= |
error.message~declined | the field matches the regular expression, case-insensitive; an object is matched as JSON |
audit | the field is present |
!error | the field is absent |
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.
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.
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
- File system drain: what writes the files, rotation,
pretty evlog map: which handlers emit a wide event at all