Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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`, 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.

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