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