Skip to content

Logging and Reporting

Rod Christiansen edited this page Sep 5, 2026 · 2 revisions

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.

Where the logs are

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.

The flat fallback file

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

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-dd name 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 .log files 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.

Line format

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.

Levels

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.

Structured markers

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.

Session header and footer

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.

The session directory

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.

events.jsonl

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.

session.json

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.

Reading a run

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

Following a run live

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.

Status files

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.

The reporting endpoint

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.

What a healthy run looks like

In order, in bootstrap.log:

  1. === BootstrapMate Session Started === and the header block.
  2. BootstrapMate v<version> started.
  3. Network is available — or the timeout line, which is not by itself fatal.
  4. If the data volume was not ready, Data volume is now writable after Ns.
  5. If configuration came from a profile, the wait ends quietly; if it did not, you would see Still waiting for management config... (Ns elapsed).
  6. The manifest loads. No Failed to load manifest from <url> line.
  7. Preflight: either nothing (no preflight item, phase Skipped) or Preflight script exited N - continuing with bootstrap. Preflight script exited 0 means the device was deemed already configured and the whole bootstrap was intentionally skipped.
  8. Setup Assistant items, each downloading and installing. Re-runs show Already have valid file: <path>. Skipping re-download. for payloads already on disk. No Refusing to install, no All retries failed.
  9. Waiting for user session in the log, followed by a matching user-session line once somebody logs in. A Waiting for user session with nothing after it is the stuck case.
  10. Userland items run.
  11. If reporting is configured: Posting run summary to reporting endpoint: … then Run summary reported (HTTP 2xx).
  12. Session complete — removing one-shot LaunchDaemon com.github.bootstrapmate and Removed /Library/LaunchDaemons/com.github.bootstrapmate.plist.
  13. === 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.

See also

Clone this wiki locally