Halton Meter logs to plain files under ~/.halton-meter/, one pair per
launchd process (stdout + stderr). The daemon and edge use structlog
with ISO-8601 UTC timestamps and dotted lowercase event names; the
watchdog and userenv login agent use the same convention. No log goes
to syslog, no log leaves the machine.
On-disk layout
| File | Process | Source |
|---|---|---|
~/.halton-meter/daemon.out.log | daemon | stdout (launchd-redirected) |
~/.halton-meter/daemon.err.log | daemon | structlog stderr |
~/.halton-meter/edge.out.log | edge | stdout |
~/.halton-meter/edge.err.log | edge | structlog stderr |
~/.halton-meter/watchdog.out.log | watchdog | stdout |
~/.halton-meter/watchdog.err.log | watchdog | structlog stderr |
~/.halton-meter/userenv.out.log | userenv login agent | stdout |
~/.halton-meter/userenv.err.log | userenv login agent | structlog stderr |
The .err.log files are the interesting ones: that is where
structured events land. The .out.log files are mostly empty (the
daemon prints almost nothing to stdout).
Tailing live
$ tail -F ~/.halton-meter/daemon.err.log # proxy hot-path events $ tail -F ~/.halton-meter/edge.err.log # edge transitions, sidecar regen $ tail -F ~/.halton-meter/watchdog.err.log # health-poll results $ tail -F ~/.halton-meter/userenv.err.log # once per login
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 (launchctl bootout or SIGTERM) |
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 launchd; in dev with halton-meter daemon from a TTY
the format is human-readable instead).
Filtering with jq
Because the format is JSON, 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
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.
For the body-capture privacy contract, see Local-only guarantee.
Rotation
Halton Meter does not rotate its own logs today. They grow without bound. For long-running installs, either:
- Add the four
.err.logpaths tonewsyslog.dwith a reasonable rotation schedule, or - Periodically run:
$ halton-meter stop && : > ~/.halton-meter/daemon.err.log && halton-meter start
In practice, the .err.log files stay small in steady-state operation
because the proxy hot path emits only intercept.start /
intercept.complete for each 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