Operations · 04 · Linux Linux only

Logs (Linux)

Where Halton Meter's two systemd-user units log to on Linux, how to read journalctl --user, and the structlog event-name convention. Public beta.

Linux (beta) · systemd · Python 3.11+ Reading time 2 min Updated May 11, 2026

On Linux you have two equally valid log surfaces:

  • journalctl --user -u <unit> — systemd’s structured journal. Indexed, queryable by time / priority / unit, supports -f to follow.
  • ~/.halton-meter/*.log — the same stdout / stderr also redirected to plain files via StandardOutput=append: / StandardError=append: on the unit. Useful for tail -F, jq, and external log shippers.

Both contain identical content. Pick whichever fits your workflow.

On-disk layout

FileProcessSource
~/.halton-meter/daemon.out.logdaemonstdout (systemd-redirected)
~/.halton-meter/daemon.err.logdaemonstructlog stderr
~/.halton-meter/edge.out.logedgestdout
~/.halton-meter/edge.err.logedgestructlog stderr

The .err.log files carry the structured events; .out.log files are mostly empty (the daemon prints almost nothing to stdout).

Tailing live

~ — tail via journalctl
$ 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 via files
$ tail -F ~/.halton-meter/daemon.err.log
$ tail -F ~/.halton-meter/edge.err.log

Common queries

~ — useful journalctl 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:

EventWhere it fires
daemon.startup.readyAfter the daemon binds internal_port and /health returns 200
daemon.exitOn graceful shutdown (SIGTERM from systemd)
daemon.heartbeatPeriodic liveness ping; ~1 per minute
intercept.startWhen the proxy hot path begins handling a request
intercept.completeAfter the row lands in SQLite, with duration_ms
edge.sidecar_regen_requestedWhen the daemon signals the edge to refresh its config
attribution.resolvedAfter the attribution chain picks a slug; winning_layer field
attribution.evictedWhen 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:

~ — filter with jq
$ 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.d entry 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