Halton Meter Docs
Docs /Operations /Logs
OPERATIONS · 04 macOS Stable

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+ ·3 min read ·Updated Jun 8, 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