Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cli-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@evlog/cli": minor
---

`evlog logs` reads the wide events the fs drain wrote to `.evlog/logs`: the last 50 (`evlog logs`), the failures (`evlog logs errors`), the slowest (`evlog logs slow --over 1s`), one request in full by id (`evlog logs <requestId>`, a UUID prefix is enough), or the shape of the traffic (`evlog logs stats`: per route, status class and level). Filters compose with every view: `--since 15m`, `--until`, `--level error,fatal`, `--path`, `--status 5xx`, `--where payment.amount>5000` on any field of the event (`=`, `!=`, `>`, `>=`, `<`, `<=`, `~regex`, present, `!absent`, repeatable), `--limit`, `--dir`. `-f` follows new events like `tail -f`; `--json` returns the events as JSON. It finds the project's log directory the way `doctor` does, reads every app of a workspace when the root has none, reads the memory drain's dev endpoint with `--url` (a follow there survives the app restarting, and says so when the endpoint stays quiet), handles both the compact and the pretty layout, and never writes.
5 changes: 5 additions & 0 deletions .changeset/fs-reader-pretty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"evlog": patch
---

`readFsLogs()` and `tailFsLogs()` from `evlog/fs` now read files the drain wrote with `pretty: true`, assembling each indented event, where they used to skip every line of them as malformed.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ node_modules
logs
!examples/eve/app/api/demo/logs/
!examples/eve/app/api/demo/logs/**
!packages/cli/src/lib/logs/
!packages/cli/src/lib/logs/**
*.log

# Misc
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/3.cli/0.overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The `evlog` executable ships with the `evlog` package. It runs separately from y

Start with [`evlog map`](/cli/map) to check supported entry points for evlog logging patterns and get a static observability score with suggested fixes. It writes `evlog.map.json` unless you pass `--no-write`. The score describes recognized source patterns, not the logs your handlers produce in production.

[`evlog init`](/cli/init) adds evlog configuration to the app. [`evlog agents`](/cli/agents) writes logging conventions for the AI agents working in the repository.
[`evlog init`](/cli/init) adds evlog configuration to the app. [`evlog logs`](/cli/logs) reads the events the fs drain wrote: the latest requests, the failures, the slowest, or one request in full. [`evlog agents`](/cli/agents) writes logging conventions for the AI agents working in the repository.

::warning{icon="i-lucide-flask-conical"}
**Early days.** `evlog map` has adapters for Nuxt, Nitro, Next.js App Router, TanStack Start, Hono, Express, and Fastify. Each adapter recognizes specific entry-point shapes. Check the detected framework and entry-point count before interpreting the score. Rules can change between releases, so [pin the version](/cli/ci#pin-the-version) when you gate CI on the number.
Expand Down
135 changes: 135 additions & 0 deletions apps/docs/content/3.cli/10.logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
title: evlog logs
description: 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.
navigation:
title: logs
icon: i-lucide-scroll-text
links:
- label: File system adapter
icon: i-lucide-folder-open
to: /integrate/adapters/self-hosted/fs
color: neutral
variant: subtle
---

`evlog logs` reads the events the [file system drain](/integrate/adapters/self-hosted/fs) 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.

```bash [Terminal]
evlog logs
```

```text [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

| 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](/integrate/adapters/self-hosted/memory)'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.

```bash [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.

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

```bash [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.

```bash [Terminal]
evlog logs errors --since 30m --json
```

```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](/reference/agent-skills) 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](/integrate/adapters/self-hosted/memory) (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.

```bash [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

- [File system drain](/integrate/adapters/self-hosted/fs): what writes the files, rotation, `pretty`
- [`evlog map`](/cli/map): which handlers emit a wide event at all
11 changes: 10 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,18 @@ pnpm evlog map
| `evlog map --baseline [ref]` | Exit 1 on a regression against the committed `evlog.map.json` (path, or `git:<ref>`) |
| `evlog map --no-write` | Skip writing `evlog.map.json` to the project root |
| `evlog map --format github` | GitHub Actions annotations on stdout; or use [`evloghq/action`](https://github.com/evloghq/action), which adds the base comparison, the job summary and a pull request comment |
| `evlog map --format sarif` | SARIF 2.1.0 on stdout, for code scanning |
| `evlog map --verbose` | Show per-file parse warnings |
| `evlog map --cwd <dir>` | Scan another app in the workspace |
| `evlog logs` | The last 50 wide events the fs drain wrote, oldest first |
| `evlog logs errors` | The ones that failed: a `5xx`, an `error` or `fatal` level, or an `error` block |
| `evlog logs slow [--over 1s]` | Over the bar (default 500ms), worst first |
| `evlog logs <requestId>` | One request in full: error with `why`/`fix`, audit record, business fields |
| `evlog logs stats` | Per route: count, errors, p50, p95; then by status class and level |
| `evlog logs --where 'payment.amount>5000' --where audit.outcome=failure` | Any field on the event: `=`, `!=`, `>`, `>=`, `<`, `<=`, `~regex`, present, `!absent` |
| `evlog logs --url http://localhost:8787/_evlog/logs` | Read the memory drain's dev endpoint instead of files |
| `evlog logs -f` | Follow new events as the app writes them |
| `evlog logs --since 15m --path /api/x --status 5xx --level error` | Filters, composable with every view |
| `evlog logs --json` | The events as JSON on stdout (one line per event with `-f`) |
| `evlog doctor` | Monorepo-aware diagnosis: Node, project/workspace, stack, evlog install, `.evlog/logs` |
| `evlog doctor --cwd <dir>` | Run against another directory |
| `evlog doctor --debug` | Same, plus a debug wide event (see Debug) |
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const subCommands = {
{ name: 'doctor', description: 'Diagnose your evlog setup' },
() => import('./doctor'),
),
logs: lazyCommand(
{ name: 'logs', description: 'Read the wide events your app wrote to .evlog/logs' },
() => import('./logs'),
),
map: lazyCommand(
{ name: 'map', description: 'Static observability map — Lighthouse for wide events' },
() => import('./map'),
Expand Down
Loading
Loading