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