Audit log
-audit-log turns on the audit log: a stream of JSON lines that records
each change of lock ownership. The destination is a file path, or - for
stdout. The audit log is separate from the diagnostic log, which goes to
stderr. The audit log is also independent of the diagnostic log level: audit
is fully on or fully off. Storage, rotation, and delivery are external
tasks, for example logrotate or the container log pipeline.
{"time":"2026-08-08T12:00:00Z","event":"granted","lock":"nightly-import","identity":"spiffe://prod/worker/1","remote_addr":"10.0.0.5:52114","token":27262976001,"wait_ms":0}Events
Section titled “Events”Each event contains time (UTC, RFC 3339), event, lock, identity
(empty without mutual TLS (mTLS)), and
remote_addr.
Each event type adds these fields:
| Event | Extra fields | Meaning |
|---|---|---|
granted |
token, wait_ms |
the session became the owner after a wait of wait_ms |
released |
token, held_ms, reason |
an ownership ended; reason is graceful, expired, io_timeout or force |
denied |
— | the access-control list (ACL) refused an ACQUIRE |
force_release |
token |
the admin force-released the lock (the disconnected session also gets a released event with reason force) |
kick_waiter |
— | the admin removed a waiter from a queue |
In the stream, a released event always comes before the granted event of
the next owner of the same lock. Thus the history of one lock is a clean
alternation. Ownerships never overlap.
How to read the log
Section titled “How to read the log”The format is made for jq. These commands answer usual questions:
# who has held "nightly/import", for how long, and why did each hold end?jq -r 'select(.lock == "nightly/import" and .event == "released") | [.time, .identity, .held_ms, .reason] | @tsv' audit.jsonl
# which identities are being denied, and on which locks?jq -r 'select(.event == "denied") | [.identity, .lock] | @tsv' audit.jsonl | sort | uniq -c
# holds that ended by lease expiry — candidates for a bigger lease or a bugjq 'select(.event == "released" and .reason == "expired")' audit.jsonlRotation
Section titled “Rotation”SIGHUP closes the audit file and opens it again. Thus logrotate can move
the old file to a different name. In stdout mode, this operation does
nothing. A minimal logrotate policy:
/var/log/monolock/audit.jsonl { daily rotate 30 compress postrotate kill -HUP "$(pidof monolock)" endscript}If the reopen fails, the server keeps the previous file. A loss of audit data during operation is worse than writes to a renamed file.
Stdout mode
Section titled “Stdout mode”With -audit-log -, the audit stream goes to stdout, and the diagnostic log
stays on stderr. Thus the two streams never mix. In containers, this agrees
with the log pipeline. Stdout is the structured audit feed for delivery.
Stderr is for the operator, and the log levels apply to it.

