Loki streams

Loki holds two kinds of data: website deploy records pushed by the deploy-exporter, and Claude Code events sent through Alloy. Retention is 90 days.

Website deploy streams

The deploy-exporter pushes one JSON line per finished run, step and gate check. Source: push_loki() in deploy-exporter/exporter.py.

Stream jobOther labelsTimestampLine fields
site-deployssite, outcomerun finishedsite, run_id, run_number, event, conclusion, started, finished, duration_s, outcome, jobs, plus the parsed gate and map fields
site-deploy-stepssite, gh_job, stepstep finishedrun_id, conclusion, duration_s
site-gate-checkssite, checkrun finishedrun_id, result (pass, partial, pending, fail), state (3, 2, 1, 0)

On the run line, jobs lists each job's name, conclusion and duration_s. The parsed fields are trigger, gate, findings_open, warnings, checks (result per check) and map (nodes, links, committed_nodes, committed_links, kept, carried_links, unreachable_pages, dropped_pages, shipped_nodes, shipped_links). Lines from the public repositories also carry title, sha (seven characters) and url. Lines from the uncovertechtalent.com repository (private) carry none of the three.

Queries from the Website deploys dashboard, with $__interval set to 1h:

sum by (outcome) (count_over_time({job="site-deploys"}[1h]))
max by (site) (max_over_time({job="site-deploys", outcome="deployed"} | json | unwrap duration_s [1h]))
min by (check) (min_over_time({job="site-gate-checks", site="machinebehavior.io"} | json | unwrap state [1h]))

Claude Code events

Claude Code sends events over OTLP through Alloy. Loki keeps service_name as the stream label (values match claude-code.*) and stores the other attributes as structured metadata, which LogQL filters after the pipe.

event_nameFields the dashboards read
api_requestmodel, cost_usd, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, duration_ms, query_source, skill_name, agent_name
api_errorstatus_code
tool_resulttool_name, success, duration_ms
user_promptcount only
api_refusal, api_retries_exhausted, internal_errorshown as log lines
tool_decision, permission_mode_changedshown as log lines

Claude Code writes one api_request event per API call, so a sum over its fields is exact. Each event also carries request_id and trace_id. A keep stage before unwrap drops them, so the sum does not split into one series per request:

sum by (model) (sum_over_time({service_name=~"claude-code.*"} | event_name="api_request" | keep model, cost_usd | unwrap cost_usd [1h]))

Prompt and response text stay redacted, which is Claude Code's default. Alloy deletes the account email, account IDs and organisation ID before Loki stores an event. Why spend comes from these events: Counter resets from parallel sessions.