Operations · 04 macOS only

Logs

Where Halton Meter's four launchd processes log to, the structlog event-name convention, and how to tail them when something is wrong.

macOS 12+ · Python 3.11+ Reading time 2 min Updated May 11, 2026

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

FileProcessSource
~/.halton-meter/daemon.out.logdaemonstdout (launchd-redirected)
~/.halton-meter/daemon.err.logdaemonstructlog stderr
~/.halton-meter/edge.out.logedgestdout
~/.halton-meter/edge.err.logedgestructlog stderr
~/.halton-meter/watchdog.out.logwatchdogstdout
~/.halton-meter/watchdog.err.logwatchdogstructlog stderr
~/.halton-meter/userenv.out.loguserenv login agentstdout
~/.halton-meter/userenv.err.loguserenv login agentstructlog 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 every component
$ 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:

EventWhere it fires
daemon.startup.readyAfter the daemon binds internal_port and /health returns 200
daemon.exitOn graceful shutdown (launchctl bootout or SIGTERM)
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 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:

~ — filter by event
$ 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.log paths to newsyslog.d with 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