On Linux you have two equally valid log surfaces:
journalctl --user -u <unit>: systemd's structured journal. Indexed, queryable by time / priority / unit, supports-fto follow.~/.halton-meter/*.log: the same stdout / stderr also redirected to plain files viaStandardOutput=append:/StandardError=append:on the unit. Useful fortail -F,jq, and external log shippers.
Both contain identical content. Pick whichever fits your workflow.
On-disk layout
| File | Process | Source |
|---|---|---|
~/.halton-meter/daemon.out.log | daemon | stdout (systemd-redirected) |
~/.halton-meter/daemon.err.log | daemon | structlog stderr |
~/.halton-meter/edge.out.log | edge | stdout |
~/.halton-meter/edge.err.log | edge | structlog stderr |
The .err.log files carry the structured events; .out.log files are
mostly empty (the daemon prints almost nothing to stdout).
Tailing live
$ journalctl --user -u halton-meter.service -f # proxy hot-path events $ journalctl --user -u halton-meter-edge.service -f # edge CONNECT decisions
Or the redirected files:
$ tail -F ~/.halton-meter/daemon.err.log $ tail -F ~/.halton-meter/edge.err.log
Common queries
$ journalctl --user -u halton-meter.service --since "10 minutes ago" --no-pager $ journalctl --user -u halton-meter.service -p warning --no-pager $ systemctl --user show halton-meter.service -p NRestarts -p RestartUSec $ journalctl --user -u halton-meter.service -b --no-pager # since last boot
Event-name convention
Events are dotted lowercase. The convention is <subsystem>.<verb> or
<subsystem>.<noun>.<verb> for sub-areas:
| Event | Where it fires |
|---|---|
daemon.startup.ready | After the daemon binds internal_port and /health returns 200 |
daemon.exit | On graceful shutdown (SIGTERM from systemd) |
daemon.heartbeat | Periodic liveness ping; ~1 per minute |
intercept.start | When the proxy hot path begins handling a request |
intercept.complete | After the row lands in SQLite, with duration_ms |
edge.sidecar_regen_requested | When the daemon signals the edge to refresh its config |
attribution.resolved | After the attribution chain picks a slug; winning_layer field |
attribution.evicted | When the daemon's attribution cache evicts stale rows |
Each event is a JSON object on its own line in daemon.err.log (when
running under systemd; in dev with halton-meter daemon from a TTY the
format is human-readable instead).
Filtering with jq
The redirected .err.log is line-delimited JSON, so jq works directly:
$ jq -c 'select(.event | startswith("intercept"))' ~/.halton-meter/daemon.err.log $ jq -c 'select(.event == "attribution.resolved") | {at: .timestamp, slug: .project, layer: .winning_layer}' ~/.halton-meter/daemon.err.log $ jq -c 'select(.level == "warning" or .level == "error")' ~/.halton-meter/daemon.err.log
For journal queries, use --output=json and pipe to jq:
$ journalctl --user -u halton-meter.service --output=json --no-pager | jq -c .
What logs do not contain
Log lines never carry prompt or response bodies. The proxy hot path
records token counts, model ids, latency, and event names, not content.
Body capture is a separate, opt-out-able feature that lands in SQLite
(request_bodies table) with redaction; logs and bodies are not mixed.
See Local-only guarantee.
Rotation
The .err.log files use StandardError=append: so they grow without
bound. systemd's journal has its own rotation (SystemMaxUse= in
/etc/systemd/journald.conf). For the append-mode files, either:
- Drop a
logrotate.dentry covering~/.halton-meter/*.log, or - Periodically truncate:
$ halton-meter stop && : > ~/.halton-meter/daemon.err.log && halton-meter start
In practice the .err.log files stay small in steady state because the
proxy hot path emits only intercept.start / intercept.complete per
request, tens of bytes each.
What's next
- Troubleshooting: failure modes keyed on what the logs say
halton-meter status: pre-log overview of process health