Action and notification journal¶
Available since 0.0.65 in the Python package and versioned Docker images. Use matching agent and UI versions; 0.0.64 does not accept the audit options.
Actions and configured notification adapters write versioned JSONL events to a local journal, independently of diagnostic log verbosity. Every event includes a unique ID, UTC timestamp, host, category, origin, actor and result. Lifecycle records share a correlation ID. The journal is also emitted as JSON lines on stderr for collection by Docker logging drivers or an external log collector.
What is recorded¶
Operation |
Events and results |
Actor |
|---|---|---|
CLI |
Started; succeeded or failed |
Local operating-system username |
Automatic rule action |
Started, then succeeded, failed, or unknown when Docker did not confirm the exec outcome. Includes duration and the exit code when known |
|
Matched rule delayed, in cooldown or dry-run |
Skipped or simulated, with reason |
|
Authenticated manual request |
Queued, started, completed or rejected, including protection/cooldown/expiry |
Trusted proxy username, otherwise |
Alertmanager audit webhook |
Each firing/resolved alert received; delivery |
|
Redis notification bridge |
Send started; Redis accepted or send failed |
|
DWho HTTP sender |
Send started; remote API accepted or send failed |
Local operating-system username |
A successful Docker call does not assert application health. A timeout may mean an operation reached Docker or the destination but its acknowledgement was lost. Do not blindly replay an action from a failed or incomplete journal entry. Commands, command output, raw rule expressions, tokens, request bodies, alert annotations and destination URLs are excluded from this journal. Rule and command identifiers are SHA-256 fingerprints. Names of actors, containers and alerts are included; choose names that do not contain secrets. Existing diagnostic logs are separate and can still contain command details.
The one-shot monit subcommand is attributed as manual, and serve/cron rule
execution as automatic. If an external scheduler invokes monit, use the cron
subcommand for accurate automatic attribution.
Only operations passing through these components are recorded. Docker commands run elsewhere, reverse-proxy authentication failures and the demo host’s recovery timer are recorded by their own services. Metrics polling and unmatched rules do not produce audit events. The public UI retains its bounded recent-action view; the durable journal stays private. An optional authenticated journal page is available since 0.0.66, as described below.
Storage and recovery¶
See journal schema compatibility for mixed historical events, export limits, upgrade/rollback steps and rules for future schema changes. The optional audit-migrate command, available since 0.0.78, creates verified schema 2 copies with original-byte backups and defaults to simulation.
The agent enables its journal when configured with rules, manual actions or the notification webhook. Use a persistent mount and an explicit path:
monit-docker --audit-file /var/lib/monit-docker/events.jsonl \
--audit-max-bytes 5242880 --audit-files 5 \
serve --state-file /var/lib/monit-docker/state.json --cmd 'status == exited ? start'
Without --audit-file (or MONIT_DOCKER_AUDIT_FILE), the file is
audit/events.jsonl beside the configured state file, otherwise
$XDG_STATE_HOME/monit-docker/audit/events.jsonl (default state directory:
~/.local/state). Mount the containing directory, not only the active file.
The existing manual-actions Compose example already persists the agent state
directory. Deleting its volume also deletes its journal.
From 0.0.66, the default retains five files of at most 5 MiB each (25 MiB total),
including the active file:
events.jsonl, .1 through .4. Oldest events expire by size, not age. Files use
0600; newly created directories use 0700. Rotation and exports share a local
filesystem lock, and writes are flushed before an operation begins. Use a local
Linux filesystem; distributed/network filesystem guarantees are not assumed.
Published 0.0.65 defaults to 10 MiB per file; pass --audit-max-bytes 5242880
explicitly to use the smaller limit on that release. A lower limit applies to
subsequent writes and rotations; it does not truncate existing larger archives.
Export larger archives with their original --audit-max-bytes setting before
archiving them separately or letting retention replace them. Changing the file
count does not delete backups outside the new range; remove obsolete archives
administratively after exporting them.
If the initial write fails, the action or notification send does not begin. If
the completion write fails after an operation, its real outcome is emitted to
stderr with a critical diagnostic; the operation is not repeated to repair the
journal. A crash can leave an unmatched started event: its outcome is unknown.
A truncated final line blocks further appends. Stop writers, preserve/export the
file for investigation, and repair or archive it before restarting. This is a
bounded operational journal, not a tamper-proof compliance archive.
Who performed a manual action?¶
Default actor: anonymous. An action token authenticates the proxy connection;
it does not identify a person. This is also the honest identity on the public demo.
For an authenticated UI, use agent and UI images 0.0.65 or later, ensure the
agent can only be reached by the trusted proxy, and add --trust-proxy-user to
serve --allow-actions .... The shipped Nginx proxy overwrites X-Monit-Actor
with its authenticated $remote_user. Missing/invalid identity is rejected when
this option is enabled. Without the option the header is ignored. Never enable
it behind a proxy that passes a client-supplied actor header through unchanged.
A shared account identifies that account, not an individual.
Export or forward retained events¶
Global audit options go before the subcommand. These commands do not connect to Docker. Supply the same retention settings if changed from the defaults.
monit-docker --audit-file /var/lib/monit-docker/events.jsonl \
audit-export > events.jsonl
monit-docker --audit-file /var/lib/monit-docker/events.jsonl \
audit-export --format csv --category action --since 2026-09-01T00:00:00Z > actions.csv
monit-docker --audit-file /var/lib/monit-docker/events.jsonl \
audit-send --url https://logs.example.org/events --token-file /run/secrets/logs-token
Export reads a consistent snapshot of retained files, oldest first. All formats
use the shared text encoding described above. Protect exported files with
restrictive permissions, for example run umask 077 first.
audit-send POSTs one JSON event per request, verifies HTTPS certificates, refuses
redirects and supplies Idempotency-Key: <event_id>. The optional token file holds
a Bearer credential. The receiver must implement this JSON contract; this is not
a native Loki, Elasticsearch or syslog API client. Each 2xx response acknowledges
acceptance, not downstream processing. Failure returns nonzero and preserves the
local journal. Re-running can resend acknowledged events: deduplicate by event ID
at the receiver. There is no background retry queue or durable sending cursor.
For continuous forwarding, collect the structured stderr events with your
existing logging service; configure that collector’s buffering and retention.
Forwarding does not journal its own exports recursively.
Include Alertmanager notifications¶
An additional webhook on every receiver you want to journal mirrors its notifications into the agent. It does not report whether an email or Slack message was delivered. Those outcomes remain in Alertmanager’s delivery logs/metrics; collect them alongside this journal when channel-level delivery results are needed. Silenced/inhibited alerts that Alertmanager never notifies are not mirrored.
In examples/monitoring, prepare the normal notifications stack as described in
notifications, then create a separate token:
mkdir -p notifications.local
chmod 700 notifications.local
python3 -c 'import secrets; print(secrets.token_hex(32))' > notifications.local/audit_token
chmod 644 notifications.local/audit_token
The private directory prevents host traversal; the file must be readable by the
non-root Alertmanager process when mounted as a Compose secret. Do not reuse the
manual-action token. Add this entry to each receiver’s webhook_configs in
notifications.local/alertmanager.yml, preserving existing entries/channels:
webhook_configs:
- url: http://monit-docker:9808/v1/notifications
send_resolved: true
max_alerts: 0
http_config:
authorization:
type: Bearer
credentials_file: /run/secrets/audit_token
Build and start with the optional source overlay:
docker compose -f compose.yaml -f compose.notifications.yaml -f compose.audit.yaml up -d --build
The overlay persists agent-audit-data. The private endpoint accepts authenticated
Alertmanager version 4 JSON (up to 64 KiB and 100 alerts per request), returning 202
after persistence, 400 for invalid payloads or 503 for a storage failure. HTTPdis
0.6.28 enforces the registered route limits before reading the body: 64 KiB for
notifications and 1 KiB for manual actions (HTTP 413 above the limit). Large or
truncated groups are rejected; configure suitably narrow Alertmanager grouping.
Retries can repeat receipts with the same notification ID. Keep this endpoint on
a trusted private network or behind HTTPS; the public UI proxy does not expose it.
The Redis bridge writes the same schema to its own redis-audit-data volume at
/var/lib/monit-docker/redis-events.jsonl, with JSON also on stderr. This records
Redis acknowledgement, not a consumer processing the stream. The DWho HTTP sender
supports --audit-file (default ~/.local/state/monit-docker/http-events.jsonl) and
--source automatic for scheduled invocation; otherwise its source is manual.
Its accepted result describes the destination’s response, not end-user delivery.
Export adapter files using the agent CLI on the host or a container mounting the
same volume. A central collector can combine these separate journals by event ID.
Private journal page (since 0.0.66)¶
The optional Journal view at /logs displays durable events with date,
container, source, actor, lifecycle phase and result. Filters cover local date
bounds, container name/ID, manual/automatic source, action/notification category
and result. Event details include correlation IDs, safe failure reasons and
notification delivery status. It refreshes only when requested; there is no
polling, external JavaScript, database or additional service.
Since 0.0.74, click a container, Manual action / Automatic action, or result label to filter the journal. Filters combine, restart at the newest page, and appear above the events as removable buttons. Clear removes all filters. Container labels use the full container ID when available, otherwise the name; the API retains its existing case-insensitive substring matching. Queued and In progress both select Pending. Exports follow the applied filters and current page. Quick filters use the applied view and replace unsubmitted form edits.
When upgrading the agent from 1.1.4, add --trust-proxy-user to the new agent’s
serve options to preserve per-user journal cursors behind the authenticated
proxy, including in read-only mode. Without this option all holders of the read
secret share the anonymous reader identity. The 1.2.0 journal overlay sets this option.
The API is disabled by default. Enable serve --audit-read-token-file FILE and
supply an explicit global --audit-file PATH. Use a separate 64-character hex
secret, never the action or notification secret. An authenticated proxy must
overwrite X-Monit-Audit-Token with that secret and X-Monit-Actor with the
signed-in username. Never expose the agent port directly. All authenticated users
of this proxy can read the configured journal; this is not per-container RBAC.
The shipped Nginx proxy uses its existing TLS and Basic authentication. The token
stays on the server and is never sent to browser JavaScript. The public demo does
not enable these routes or expose its journal.
For a fresh read-only setup, from examples/ui:
python3 prepare.py --journal --self-signed
docker compose -f compose.yaml -f compose.journal.yaml up -d
This uses the matching versioned agent and UI images (0.0.66 or later). Replace the development certificate
with a trusted certificate for remote use. The state volume retains the journal
at /var/lib/monit-docker/audit/events.jsonl; a fresh read-only agent has no events
until actions/rules or notification receivers are configured. Reading alone does
not execute actions or enable notification delivery.
For an existing actions deployment, preserve the existing secrets and command.
Generate a distinct audit read secret and proxy include in the private secrets
directory (the preparation script refuses to overwrite an existing directory).
Add global --audit-file /var/lib/monit-docker/audit/events.jsonl before
serve, then append --audit-read-token-file /run/secrets/audit-token to the
serve options. Reuse the token and proxy-include mounts from
compose.journal.yaml. Add --trust-proxy-user to bind journal cursors and attribute manual actions
to the authenticated username. Compose replaces command lists: do not
stack the journal and actions overlays without combining their command options.
Since 0.0.75, View action opens a separate Action history dialog for action events with a correlation ID. It shows matching action events in recorded order, oldest first, without applying the main journal filters. Each stage keeps its source, actor, host, target and reason; a recorded duration is shown in milliseconds when available. Closing the dialog preserves the journal page and filters. Notification events and legacy events without an ID have no action link.
The dialog reads one bounded page at a time. Load older events explicitly continues the search, including after an empty page; there is no background scan or polling. Refresh action starts a new snapshot and recovers from expired cursors. The display is limited to 500 events. Only retained events are shown: missing stages do not prove an action is still running or that it never completed. An ID groups recorded events; it does not invent missing lifecycle stages.
Bounded reads and downloads¶
See the HTTP API contract for
page fields, filter syntax, authentication, cursor error codes and a complete
curl example exporting the displayed page.
GET /v1/audit returns up to 100 events, newest first, with a signed continuation
cursor. Each request reads at most 1 MiB of journal bytes and retains at most
512 KiB of serialized event data, plus bounded parsing/response overhead. At most
two journal reads run concurrently; other readers receive 503 and may retry.
Opening files briefly takes a nonblocking shared journal lock. File content reads
and serialization run after releasing it, outside the action/monitoring locks.
If a writer holds the journal lock, browsing returns 503 rather than waiting.
Since 0.0.75, the correlation_id query parameter matches the complete, case-sensitive
ID. Combine it with category=action to read action stages without result/source
filters: /v1/audit?category=action&correlation_id=REQUEST_ID. It accepts at most
256 printable characters and uses the same authentication, scan limits and
filter-bound cursors as other filters. /v1/audit/export accepts it too.
A sparse filter can produce an empty page with an Older events continuation. No automatic loop scans the remaining files. Cursors are bound to the filters and authenticated user, exclude subsequent appends and expire after 15 minutes, agent restart, or loss of unread files through rotation. Refresh to start a new snapshot. Externally rewriting/truncating journal files is unsupported.
Export page · JSONL/CSV downloads the displayed snapshot only, using
GET /v1/audit/export with its page cursor and filters. It preserves the common
escaped text representation. This bounds browser memory and server response size;
use administrative audit-export for a complete retained-history export. The
CLI full export now uses a validated streaming snapshot; web page limits do
not limit the full CLI export. An expired snapshot must be refreshed before export.
The regression suite checks rotation, malformed files, filter/cursor tampering,
proxy identity replacement, writer progress during reads and browser error states.
Run .github/scripts/benchmark-audit.py with the project environment to measure a
50 MiB disposable journal, first-page memory and latency, full sparse-search cost
and a concurrent write. Measurements depend on the host and are not latency SLAs.
Since 0.0.76, opening action history immediately previews matching events from the current journal page while the agent checks retained history. Refreshing also keeps the previously displayed events visible. The loading message identifies this provisional view; a successful response replaces it, including when the new snapshot is empty. A failed request labels the retained preview as possibly incomplete or outdated. No extra background requests or persistent browser storage are used. Ordinary audit text validation also avoids unnecessary character-by-character work while preserving the canonical escaping rules. Compiled regular expressions process escape sequences and ASCII control runs; printable Unicode is preserved, with detailed checks limited to unusual Unicode runs. Malformed and noncanonical escape sequences are still rejected.
Full CLI export memory and consistency¶
audit-export opens the retained files under the journal lock, captures their
sizes, then releases the lock. It validates every record before writing output
and rereads the same descriptors to export incrementally, oldest first. JSONL/CSV
encoding and category/date filters remain unchanged. Memory is bounded by record
size and the configured number of retained files, not total journal bytes.
Cooperating appends after capture are excluded; rotations do not change the open snapshot. Slow output does not hold the writer lock. Descriptors close on success, errors or an interrupted consumer. A truncated, invalid or oversized record is rejected during validation before output. External in-place edits of journal files are unsupported; do not edit/truncate them during export. Output-device or second-pass read errors can still leave partial output: always check exit status and use a temporary destination before renaming an export into place.
AuditJournal.read() remains the materialized compatibility API. audit-send
retains its existing batch forwarding behavior. The paginated HTTP API is unchanged.