crab0.4.0
crab/docs/Trace & metrics

Two subcommands read the same session back. trace shows the shape of a run; metrics shows what it cost. Both have --json, and both are read-only.

crab trace

crab trace <FILE> [--json]     # span tree: turn → llm/tool calls
$ crab trace .crab/sessions/session-1792661000878881000.jsonl
run 0 · 63 spans
├── ✓ llm  openai/deepseek-chat · 5663ms · ↑254 ↓257.7k (cr0 cw257.7k)
├── ✓ tool read · 4ms │ read {"path":"crates/crab-cli/src/metrics.rs"}
├── ✓ tool bash · 5392ms │ bash {"command":"cargo test -p crab-cli metrics"}
├── ✓ llm  openai/deepseek-chat · 2896ms · ↑275 ↓258.8k (cr257.9k cw921)
├── ✓ tool edit · 12ms │ edit {"path":"crates/crab-cli/src/metrics.rs"}
└── ✓ llm  openai/deepseek-chat · 3482ms · ↑323 ↓260.4k (cr259.0k cw1.3k)

Read the arrows as ↑ output and ↓ input, with cr / cw for cache read and write. Two patterns are worth recognising:

  • Cheap and fast: an unchanged ↓ with a growing cr — the cached prefix is doing its job.
  • Expensive: a large ↓ with cr0 on every call — each turn is re-paying for the whole prefix.

A tool line shows the first ~90 characters of the arguments, so you can usually tell what it did without opening the JSON. Use --json when you want all of it.

crab metrics

crab metrics [DAYS] [--json]   # default window: 7 days
$ crab metrics 7
crab metrics · last 7 day(s)

BY DAY
  2026-10-22  turns   2  ↑3.6k ↓309.2k  cr308.6k cw622   cost $0.0064  tool-err 0%  p50 5481ms p90 5481ms
  2026-10-21  turns  41  ↑38.1k ↓4.2M   cr4.1M cw96.2k   cost $0.0917  tool-err 2%  p50 2210ms p90 8940ms

BY MODEL
  deepseek-chat   turns 104  ↑92.1k ↓11.2M  cost $0.2318
  local-model     turns   3  ↑2.1k  ↓41.0k  cost −

Columns: turns, output tokens, input tokens, cache read, cache write, cost, tool error rate, and p50/p90 turn latency. The tool-err figure is the one to watch — a rising rate usually means the agent is guessing at file paths or fighting a test suite.

A model crab has no price for shows −, not $0. That distinction matters when you are comparing a local model against a hosted one.

The JSON forms

$ crab metrics --json
{
  "days": 7,
  "daily": [
    { "day_ms": 1792627200000, "turns": 2,
      "prompt_tokens": 309230, "completion_tokens": 3646,
      "cache_read": 308608, "cache_write": 622,
      "cost_usd": 0.006413, "tool_calls": 7, "tool_errors": 0,
      "priced_turns": 2 }
  ],
  "models": [ { "model": "deepseek-chat", "turns": 2, "cost_usd": 0.006413 } ]
}

priced_turns is the honesty field: turns whose model had no entry in the price table are counted for tokens but excluded from cost_usd. If priced_turns is less than turns, your cost figure is a lower bound.

The ledger

.crab/metrics.sqlite, one row per run, written when the run ends. It is a telemetry sink: it never enters the model context.

CREATE TABLE turns (
  session_id        TEXT NOT NULL,
  run_id            INTEGER NOT NULL,
  ts                INTEGER NOT NULL,
  provider          TEXT,
  model             TEXT,
  prompt_tokens     INTEGER,
  completion_tokens INTEGER,
  cache_read        INTEGER,
  cache_write       INTEGER,
  cost_usd          REAL,
  llm_calls         INTEGER,
  tool_calls        INTEGER,
  tool_errors       INTEGER,
  duration_ms       INTEGER,
  interrupted       INTEGER,
  PRIMARY KEY (session_id, run_id)
);
CREATE INDEX idx_turns_ts ON turns(ts);

A normal SQLite file. Query it directly for anything the two reports do not cover — cost per repository, cost by hour of day, how often you abort a run:

sqlite3 .crab/metrics.sqlite \
  "select model, round(sum(cost_usd),4) as usd, sum(tool_errors)
     from turns group by model order by usd desc;

Concurrent runs are fine. The primary key is (session_id, run_id), so two crabs in two panes write separate rows. WAL mode means a read while another run is writing will not block it.

What it does not tell you

  • Why a turn was slow. You get duration_ms per span; queueing and network time live inside the provider call and are not separable from it.
  • Your cost if the price changed mid-window. Rows keep the cost computed at write time.
  • Anything about a turn that crashed before turn_end. The span tree shows the gap.

The design constraint behind all of this is one line from the project notes: observability data must not change the observed system. Measurement is a replay over a file, not an extra model call.

esc