Every Binions daemon writes structured logs, and the logger daemon is the single place they all land. Instead of scattering free-form text across a dozen services, each daemon emits a typed Log event onto the internal event bus; the logger daemon collects those events and writes them as machine-readable JSON Lines — one JSON object per line — that you can slice with ordinary command-line tools. This page shows you where the logs live, what a line looks like, and how to follow a single piece of work as it moves across services.
Good to know. Logs come in two complementary forms: the structured
JSON Linesfiles for searching and scripting, and the familiar systemd journal for live tailing and crash diagnosis. You will reach for both, often in the same session.
Logging in Binions is event-driven, just like everything else the platform does. When any daemon wants to record something — a playbook step starting, a webhook returning an error, a message being processed — it does not write to a file directly. It emits a Log event onto the internal event bus, and the logger daemon is the one service subscribed to those events.
The logger daemon takes the whole event envelope — not just the message text, but the metadata that travels with every Binions event — and appends it as a single JSON object to a per-service file. One service, one file, one line per event:
# Each daemon gets its own append-only JSON Lines file
/var/log/binions/logger/events.jsonl
/var/log/binions/playbook/events.jsonl
/var/log/binions/mailbox/events.jsonl
/var/log/binions/webhookcaller/events.jsonl
# ...one directory per daemon under /var/log/binions/
At the same time, the same records flow to journald, so journalctl sees them too. That gives you the best of both: durable, greppable JSON on disk for analysis, and the live systemd journal for tailing and boot-time troubleshooting. Because the logger is itself a daemon, it logs its own activity to /var/log/binions/logger/events.jsonl as well.
Why JSON Lines. One JSON object per line means you can
grep,tail, and stream the file like plain text, but also parse any line with a real JSON tool. You get human-readable and machine-readable at the same time, with no database to query.
Every log line is a complete Binions event envelope, so it carries far more than a timestamp and a message. The fields you will use most often are below. The envelope is the same structure every Binions event uses, which is why the same correlation and trace fields let you follow work across the whole platform.
| Field | What it holds |
|---|---|
event_type | Always the literal "Service.Log" for a log record — that is how you tell a log envelope apart from other events on the bus. |
level | The severity, one of Debug, Info, Warn, or Error. This is your primary filter. |
message | The human-readable log line — a short sentence describing what happened. |
fields | A structured object of extra context the daemon attached: identifiers, counts, durations, status codes. This is what makes the logs queryable rather than just readable. |
correlation_id | The id that ties together every event belonging to the same piece of work, across every daemon that touched it. The single most useful field for tracing a run. |
traceparent | A W3C trace-context string carried in the envelope metadata. Its middle segment is the trace id that links these logs to distributed traces (see Monitoring & tracing). |
A single line, as written to disk, looks like this (shown on one line in the file; reformatted here only for readability when you pretty-print it):
{"event_type":"Service.Log","service":"playbook","correlation_id":"c1f8a4e2-7b09-4d6c-9a13-0f2e5d8b41aa","timestamp":"2026-05-31T08:14:22.519Z","payload":{"level":"Warn","message":"webhook step returned non-2xx, will retry","fields":{"playbook":"invoice-intake","step":"notify-team","attempt":2,"status_code":503,"backoff_ms":4000}},"metadata":{"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}}
The payload wraps the level, message, and fields; the correlation_id sits at the top of the envelope; and the trace id lives inside metadata.traceparent. Knowing that layout is all you need to write effective filters.
Because each line is valid JSON, jq turns the log files into a small, fast query engine — no extra tooling required. Start by pretty-printing a file or following it live, then narrow down by level and by the fields you care about.
# Pretty-print the most recent lines from one service
tail -n 20 /var/log/binions/playbook/events.jsonl | jq .
# Follow a service live, but only show warnings and errors
tail -f /var/log/binions/playbook/events.jsonl \
| jq 'select(.payload.level == "Warn" or .payload.level == "Error")'
# Collapse each line to a compact, scannable summary
tail -n 50 /var/log/binions/mailbox/events.jsonl \
| jq -r '"\(.timestamp) \(.payload.level) \(.payload.message)"'
You can reach into the structured fields just as easily — that is the whole point of logging structured data rather than flat strings:
# Show only webhook steps that came back with a 5xx status code
tail -n 200 /var/log/binions/webhookcaller/events.jsonl \
| jq 'select(.payload.fields.status_code >= 500)
| {time: .timestamp, msg: .payload.message, code: .payload.fields.status_code}'
# Count log lines by level across a whole file
jq -r '.payload.level' /var/log/binions/playbook/events.jsonl | sort | uniq -c
Tip. The
fieldsobject is whatever each daemon chose to attach, so a quickjq '.payload.fields' <file>on a few recent lines is the fastest way to learn what is available to filter on for a given service.
A single event — an email arriving, a webhook firing — usually flows through several daemons, and the correlation_id stitches that journey back together. Every event Binions creates while handling one piece of work shares the same correlation id, so once you have it you can pull the complete story out of every service’s log file at once.
# Grab the correlation_id from a line you care about
RUN=c1f8a4e2-7b09-4d6c-9a13-0f2e5d8b41aa
# Pull every log line for that run, from all daemons, in time order
grep -h "$RUN" /var/log/binions/*/events.jsonl \
| jq -r '"\(.timestamp) \(.service) \(.payload.level) \(.payload.message)"' \
| sort
The result is a single timeline: the mailbox daemon receiving the message, the playbook daemon deciding what to do, the webhook caller making an outbound call, and so on — each step in order, regardless of which service produced it. When a playbook misbehaves, this one command is usually where the diagnosis starts.
For live tailing, crash diagnosis, and boot-time problems, reach for the systemd journal. Every daemon runs as a systemd service, so journalctl -u gives you the same records the logger collects, plus anything systemd itself reports about the unit (starts, restarts, watchdog actions).
# Follow one daemon's journal live
journalctl -u binions-playbook.service -f
# Errors and worse, since this morning
journalctl -u binions-mailbox.service -p err --since "08:00"
# Everything since the last boot, for the whole platform
journalctl -u "binions-*" --since today
Use -f to follow, -p to filter by priority (for example -p warning or -p err), and --since / --until to bound a time window. The journal is the right tool when a daemon will not stay up and never gets far enough to write to its events.jsonl file — systemd captures the failure regardless.
The JSON Lines files are rotated automatically, so they never grow without bound. A logrotate policy ships with the platform and runs daily. Its key settings are:
| Setting | Effect |
|---|---|
daily | Rotation is considered once per day. |
rotate 7 | Seven rotated generations are kept, then the oldest is discarded — roughly a week of history. |
size 100M | A file is also rotated early if it reaches 100 MB, so a busy day cannot fill the disk. |
compress + delaycompress | Old generations are gzipped to save space, but the most recent rotation is left uncompressed so it stays easy to read. |
dateext | Rotated files are named with a date suffix rather than a numeric one, so you can find a given day at a glance. |
The important operational detail is what happens after a rotation. Because the logger daemon holds its output files open, logrotate sends it a hang-up signal in a postrotate step so it cleanly reopens and starts writing to the fresh file:
# What logrotate runs after rotating, so the logger reopens its files
systemctl kill --signal=HUP binions-logger.service
You never need to run that by hand — it is part of the shipped policy — but it explains why log writing continues seamlessly across a rotation, with no restart and no lost lines.
Heads up. Rotated and compressed files keep the same JSON-per-line format. To search history, decompress on the fly:
zcat /var/log/binions/playbook/events.jsonl-*.gz | jq 'select(.payload.level == "Error")'.
Logs tell you what happened; traces show you how long each step took and where the time went — and the two are linked by a shared trace id. That trace id is the middle segment of the traceparent string in each envelope’s metadata. Pull it out with jq, and you can pivot straight from a suspicious log line into the matching trace.
# Extract the trace id (middle segment of traceparent) for a run
grep -h "$RUN" /var/log/binions/*/events.jsonl \
| jq -r '.metadata.traceparent | split("-")[1]' \
| sort -u
With distributed tracing enabled, that trace id is searchable in the bundled Jaeger UI, where the same run appears as a waterfall of timed spans across daemons. Tracing is opt-in — it is switched on per daemon in configuration — and the trace viewer is reachable on your LAN, never published to the internet. The full workflow, including how to turn tracing on, is covered in Monitoring & tracing.
Stays on your host. Both the JSON log files and the trace data live entirely on your own machine. Nothing is shipped to an external service; what you do with the logs — ship them to a SIEM, archive them, or leave them in place — is entirely up to you.