-
Notifications
You must be signed in to change notification settings - Fork 0
Logging and Reporting
Every BootstrapMate run gets its own session directory holding a human log and two machine-readable files, updates two status artefacts, and optionally POSTs a run summary to an endpoint you nominate. This page is the reference for all of it, and ends with the sequence of lines a healthy run produces.
The logs root is:
/Library/Managed Bootstrap/logs/
Under it, each run owns a session directory named for the day and the time the run started, in local time:
/Library/Managed Bootstrap/logs/2026-09-01/131514/
The day directory is yyyy-MM-dd and the session directory is HHmmss. If a previous run
already started in the same second, the new run takes HHmmss_2 through HHmmss_9. The run's
session id is the two joined with a hyphen, for example 2026-09-01-131514 or
2026-09-01-131514_2.
A session directory holds three files:
bootstrap.log
events.jsonl
session.json
bootstrap.log is the human log described under Line format below.
events.jsonl and session.json are the same run in machine-readable form, described under
The session directory.
There is no size-based rotation and no appending — a new run means a new session directory.
The logs root is created by the package's postinstall, and again by the CLI at startup if it
is missing. If it cannot be created the CLI writes
bootstrapmate: failed to create log directory <dir>: <err> to stderr and continues with no
log file. There is no fallback log location outside the logs root.
If the session directory itself cannot be created — the directory tree is unwritable, or all nine same-second names are taken — the run falls back to a single flat file at the logs root, named from the start time:
/Library/Managed Bootstrap/logs/2026-09-01-131514.log
The pattern is yyyy-MM-dd-HHmmss followed by .log. That file carries the human log only;
a run on the fallback path produces no events.jsonl and no session.json. Flat files left
by the layout that preceded session directories look the same and are still listed by the GUI.
Retention runs at logger initialisation, before the new run's directory is created, and has three parts:
- Day directories under the logs root whose
yyyy-MM-ddname is older than 30 days are removed, with everything in them. - Of the session directories that survive, the newest 100 across all days are kept and the rest are removed. This cap exists so a host that runs often does not accumulate directories faster than the age sweep clears them.
- Loose
.logfiles at the logs root — the fallback files above — are removed when their modification date is older than 30 days. That sweep is non-recursive and error-tolerant.
The run logs its own result as Removed N log directory or file(s) past retention.
This is the format of bootstrap.log inside the session directory, and of the flat fallback
file when there is one.
[2026-09-01 13:15:14] INFO Session started
[2026-09-01 13:15:14] ERROR Failed to load manifest
[2026-09-01 13:15:14] WARN Retrying
[2026-09-01 13:15:14] DEBUG Detail
The fields are: a bracketed timestamp yyyy-MM-dd HH:mm:ss in local time, then the level
label padded to five characters, then the message. Every physical line gets its own timestamp,
blank lines are dropped, and \r\n and \r are normalised.
Internally there are five: debug, info, warning, error, success. On disk only four
labels appear — DEBUG, INFO, WARN, ERROR. Success is written as INFO.
The console gets icon prefixes instead: [~] debug, [i] info, [!] warning, [x] error,
[+] success. --silent suppresses console output entirely; [~] lines only appear with
--verbose. Console verbosity comes from the CLI flags — setting verboseMode or
silentMode by profile does not change it, because the logger is initialised before those
preferences are read.
Inside the message text you will also see [SECTION], [PROGRESS], [SUB-PROGRESS],
[SUCCESS], [SKIPPED], [COMPLETION], === <title> === headings, and — for output
captured from a child script — [OUTPUT] <scriptname>: . Child stdout is logged at INFO,
child stderr at WARN. These same markers are what events.jsonl turns into its event_type
and status fields.
The header is written at init and records Version:, Session Start Time:, Process ID:,
User:, Machine:, OS:, Architecture: (ARM64 or X64), Working Directory:,
Command Line:, Verbose Console: and Silent Mode:. The footer records the duration.
The Version: value in the header is not the build version — see
Troubleshooting and Gotchas.
Alongside bootstrap.log, each run writes two machine-readable files into its session
directory. The layout and field names match the session logs of the other managed tools in the
same family, so one reader works across all of them.
One JSON object per line, appended as the run proceeds. There is exactly one record per non-blank physical line of the human log, in the same order, so the two files line up one to one.
| Field | Value |
|---|---|
event_id |
<session_id>-NNNNN, a five-digit counter starting at 00001
|
session_id |
the run's session id, matching the directory names |
timestamp |
ISO-8601 with fractional seconds |
level |
DEBUG, INFO, WARN or ERROR, the same label the human log carries |
event_type |
see the table below |
status |
PROGRESS, SUCCESS, SKIPPED or FAILED, where the marker implies one |
message |
the line's text, with a recognised leading marker removed |
error |
the same text, present only on ERROR lines |
action, context
|
reserved; nothing populates them, so they are absent |
Fields with no value are omitted from the record rather than written as null.
event_type comes from the leading marker on the message:
| Marker in the log line | event_type |
status |
|---|---|---|
[SECTION] |
section |
none |
[PROGRESS], [SUB-PROGRESS]
|
progress |
PROGRESS |
[SUCCESS] |
item |
SUCCESS |
[SKIPPED] |
item |
SKIPPED |
[COMPLETION] |
session_end |
SUCCESS |
[OUTPUT] |
output |
none |
| no marker, or one not listed |
message, or error at ERROR level |
none, or FAILED
|
A marker that is not in the table is left in the message text.
The run as a whole. It is written when the run starts with "status": "running", and
rewritten when the run ends.
| Field | Value |
|---|---|
session_id |
the run's session id |
start_time, end_time
|
ISO-8601 with fractional seconds; end_time appears only once the run ends |
duration_seconds |
integer, rounded; likewise only once the run ends |
run_type |
provisioning — the CLI does not set anything else |
status |
running, then completed, or partial_failure if the run logged any ERROR line |
tool_version |
the version string the logger was given |
environment |
hostname, os_version, user, pid, command_line
|
summary |
events, errors, warnings, packages_handled
|
summary.events, errors and warnings are the counts of records written to
events.jsonl. packages_handled is always an empty array — nothing populates it.
A run that was killed before it finished leaves session.json at "status": "running" with
no end_time, which is how you tell a truncated run from a completed one without reading the
log.
Find the newest run's session directory:
ls -d /Library/Managed\ Bootstrap/logs/*/*/ | sort | tail -1
Get the outcome of every run still on disk, one line each:
find /Library/Managed\ Bootstrap/logs -name session.json -exec /usr/bin/plutil -extract status raw -- {} \;
Pull just the failures out of a run's structured events:
grep '"level":"ERROR"' /Library/Managed\ Bootstrap/logs/2026-09-01/131514/events.jsonl
With jq available, the same run's item outcomes:
jq -r 'select(.event_type == "item") | "\(.status) \(.message)"' /Library/Managed\ Bootstrap/logs/2026-09-01/131514/events.jsonl
Tail the newest run's log:
tail -f "$(ls -t /Library/Managed\ Bootstrap/logs/*/*/bootstrap.log | head -1)"
Everything also goes to the unified log under subsystem com.github.bootstrapmate, category
general, so you can stream it without knowing the filename:
log stream --predicate 'subsystem == "com.github.bootstrapmate"'
Or read back a window that has already passed:
log show --predicate 'subsystem == "com.github.bootstrapmate"' --last 1h
Levels map to debug, info (info and success), default (warning) and error.
BootstrapMate writes per-phase status, keyed by the phase names Preflight,
SetupAssistant and Userland, to two places.
/Library/Managed Bootstrap/status.json — pretty-printed JSON with sorted keys, one entry per
phase, each carrying stage, startTime, completionTime, exitCode, version, phase,
architecture, bootstrapUrl, lastError and runId.
/Library/Preferences/com.github.bootstrapmate.plist — the same information as an XML plist,
with capitalised key names (Stage, StartTime, CompletionTime, ExitCode, Version,
Phase, Architecture, BootstrapUrl, LastError, RunId).
Stage takes one of Starting, Running, Completed, Failed, Skipped. Timestamps are
yyyy-MM-dd HH:mm:ss.
On a successful run the plist is then overwritten with only LastRunVersion,
LastUpdated and Architecture, so the per-phase detail is not there afterwards. This is
listed under known issues in
Troubleshooting and Gotchas.
If reportingUrl is configured — by profile, or with --reporting-url — BootstrapMate POSTs
one JSON run summary after the dialog closes and before cleanup. It fires once, and a failure
to report never fails the run.
The request uses Content-Type: application/json and
User-Agent: BootstrapMate/<version>, with a 15-second request timeout and a 20-second bound
on the wait. If reportingHeader is set, its value is sent as the Authorization header.
The payload has sorted keys and these fields:
| Field | Value |
|---|---|
tool |
"BootstrapMate" |
platform |
"macOS" |
schemaVersion |
1 |
version |
the tool's version string |
runId |
UUID generated for this run |
success |
boolean |
startTime / endTime
|
ISO-8601 |
durationSeconds |
integer, rounded |
architecture |
ARM64 or X64
|
hostname |
the host name |
serialNumber |
the platform serial, or "" if unavailable |
manifestUrl |
the configured manifest URL |
phases |
per-phase stage, exitCode, startTime, completionTime, lastError
|
The payload is vendor-neutral; any service that accepts a JSON POST can consume it.
The three log lines to grep for are
Posting run summary to reporting endpoint: <scheme>://<host><path> (query string and
userinfo are redacted), then either Run summary reported (HTTP 2xx) or
Reporting endpoint returned HTTP <code> / Reporting POST failed: <err>.
On a successful run the phases object arrives empty. That is a known issue, not a
misconfigured endpoint — see
Troubleshooting and Gotchas.
In order, in bootstrap.log:
-
=== BootstrapMate Session Started ===and the header block. -
BootstrapMate v<version> started. -
Network is available— or the timeout line, which is not by itself fatal. - If the data volume was not ready,
Data volume is now writable after Ns. - If configuration came from a profile, the wait ends quietly; if it did not, you would see
Still waiting for management config... (Ns elapsed). - The manifest loads. No
Failed to load manifest from <url>line. - Preflight: either nothing (no preflight item, phase
Skipped) orPreflight script exited N - continuing with bootstrap.Preflight script exited 0means the device was deemed already configured and the whole bootstrap was intentionally skipped. - Setup Assistant items, each downloading and installing. Re-runs show
Already have valid file: <path>. Skipping re-download.for payloads already on disk. NoRefusing to install, noAll retries failed. -
Waiting for user sessionin the log, followed by a matching user-session line once somebody logs in. AWaiting for user sessionwith nothing after it is the stuck case. - Userland items run.
- If reporting is configured:
Posting run summary to reporting endpoint: …thenRun summary reported (HTTP 2xx). -
Session complete — removing one-shot LaunchDaemon com.github.bootstrapmateandRemoved /Library/LaunchDaemons/com.github.bootstrapmate.plist. -
=== BootstrapMate Session Ended === (Duration: N.Ns)and the total duration line.
A quick pass for anything wrong:
grep -E '\] ERROR |\] WARN ' "$(ls -t /Library/Managed\ Bootstrap/logs/*/*/bootstrap.log | head -1)"
Or read the same verdict out of the run's session.json: completed means it logged no
ERROR line, partial_failure means it logged at least one.