-
Notifications
You must be signed in to change notification settings - Fork 1
Logging and Reporting
Where BootstrapMate writes its logs, exactly what a log line looks like, what status it leaves in the registry and on disk, and how to tell a healthy run from a bad one. Read this before you write any monitoring against the tool, because the status surface has a split between where status is written and where it is read.
Each run owns a directory under C:\ProgramData\ManagedBootstrap\logs, named for the run's
start time in local time — a day directory, then a time directory:
C:\ProgramData\ManagedBootstrap\logs\yyyy-MM-dd\HHmmss\
That directory holds three files:
| File | Contents |
|---|---|
bootstrap.log |
The human log. Every line the logger writes, in the format below. |
events.jsonl |
The same lines as JSON records, one per line, appended as the run proceeds. |
session.json |
The run as a whole — id, start time, run type, tool version, environment. |
The run's session id is the two directory names joined with a hyphen, yyyy-MM-dd-HHmmss. It
is the id every record in events.jsonl and session.json carries, so a session id is enough
to name both the directory and every event in it.
If a previous run started in the same second, the new directory takes a _2 through _9
suffix (HHmmss_2), and the suffix becomes part of the session id. If the directory cannot be
created at all — ten runs in one second, or the path is not writable — the run falls back to a
single flat file at the logs root, yyyy-MM-dd-HHmmss.log, and writes no structured files.
Devices provisioned before this layout existed also have flat files at the root. Search
recursively so you find both:
Get-ChildItem C:\ProgramData\ManagedBootstrap\logs -Filter *.log -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1
Every line is written by Logger.FormatLine as:
[{timestamp:yyyy-MM-dd HH:mm:ss}] {LEVEL,-5} {message}
Three fields:
- timestamp — local time, second resolution, in square brackets.
-
LEVEL — the level name, left-aligned and padded to a five-character column. That
padding is why
INFOandWARNare followed by two spaces andDEBUG/ERRORby one. - message — the text, with any ANSI colour escapes stripped.
Real lines:
[2026-09-03 08:14:02] INFO === BootstrapMate Session Started ===
[2026-09-03 08:14:03] DEBUG Downloading manifest from: https://example.com/bootstrap/manifest.json
[2026-09-03 08:14:07] WARN MSI error 1603, retrying in 10 seconds... (attempt 1/5)
[2026-09-03 08:14:19] ERROR Failed to install package Example App: Download failed: Forbidden
[2026-09-03 08:14:20] INFO [OUTPUT] Chocolatey: Installing the following packages:
The logger has five levels — Debug, Info, Warning, Error, Success — but only four
level names ever appear in the file. Debug writes DEBUG, Warning writes WARN, Error
writes ERROR, and everything else, including Success, writes INFO. A successful
install is an INFO line carrying a [SUCCESS] marker in the message, not a SUCCESS level.
The file always receives every level, including DEBUG. --verbose/-v and --silent
affect the console and the pipe, never the file.
Classification beyond the level column is carried as a bracketed marker at the start of the
message: [SECTION], [PROGRESS], [SUB-PROGRESS], [SUCCESS], [SKIPPED],
[COMPLETION], [OUTPUT]. Grep for these rather than for the level.
A package's own output is written back into the same log, one stamped line each, prefixed
[OUTPUT] {package}: . Standard output is recorded at INFO and standard error at WARN. This
covers PowerShell, EXE and Chocolatey items.
sbin-installer is the exception: its output goes to Logger.Debug as
sbin-installer stdout: … and sbin-installer stderr: …. It is still in the file, but it is
DEBUG, so it will not show up if you are filtering.
Multi-line messages are split on newline, blank lines are dropped, and every resulting line gets its own timestamp and level.
events.jsonl carries exactly the same records as bootstrap.log — every line written to the
file is also appended here — but as one JSON object per line, so a run can be read by a program
without parsing prose. The layout and field names match Cimian's session logs, so the same
reader works on both tools.
Each record has these fields:
| Field | Meaning |
|---|---|
event_id |
{session_id}-{NNNNN}, the five-digit counter incrementing across the run. |
session_id |
The run's session id. |
timestamp |
Local time to the millisecond with offset, yyyy-MM-ddTHH:mm:ss.fff+zz:zz. |
level |
DEBUG, INFO, WARN or ERROR — the same four the file uses. |
event_type |
See the table below. |
status |
SUCCESS, SKIPPED, PROGRESS, FAILED, or absent. |
message |
The line's text, with the leading marker removed when one was recognised. |
error |
The text again, on ERROR records only. Absent otherwise. |
The bracketed marker at the front of a message is lifted into event_type and status, which
is what makes the stream queryable:
| Marker | event_type |
status |
|---|---|---|
[SECTION] |
section |
none |
[PROGRESS], [SUB-PROGRESS]
|
progress |
PROGRESS |
[SUCCESS] |
item |
SUCCESS |
[SKIPPED] |
item |
SKIPPED |
[COMPLETION] |
session_end |
SUCCESS |
[OUTPUT] |
output |
none |
Anything else keeps its message intact and becomes message (or error at ERROR level, with
status FAILED). Note that [COMPLETION] is mapped to status SUCCESS whatever it says, so a
run that completed with failed packages still produces a session_end record marked SUCCESS.
Count error records, not that one.
Every failed package in a run:
Get-Content <session>\events.jsonl | ConvertFrom-Json | Where-Object level -eq ERROR | Select-Object timestamp, message
session.json describes the run itself: session_id, start_time, run_type
(provisioning), status, tool_version, an environment object (hostname, os_version,
user, pid, command_line) and a summary object (events, errors, warnings).
It is written once, when the run starts. The code that rewrites it with the outcome is never
called, so on a finished run session.json still reads "status": "running", has no
end_time or duration_seconds, and reports events, errors and warnings all zero. Read
the outcome from events.jsonl or from the registry, not from session.json.
Writing the structured files is best-effort throughout: a failure to create the directory or append a record is swallowed and the run continues.
Old logs are pruned when the logger initialises, at the start of each run, in two passes.
First the session directories. Any day directory older than 30 days is deleted whole; then, across the surviving days, all but the newest 100 session directories are deleted. The cap is on sessions, not days, so a device that runs many times a day keeps fewer days than one that runs nightly.
Then the flat files. Any *.log sitting directly in the log directory whose
LastWriteTime is older than 30 days is deleted. Age comes from last-write rather than
the filename deliberately, because the directory also collects logs from wrapper scripts that
do not use the timestamped name. This pass does not recurse, so it never touches a
bootstrap.log inside a session directory.
Pruning is best-effort: a locked file is skipped and all exceptions are swallowed. Nothing reports a prune failure.
Tail the newest log. -Recurse matters — without it this finds only the flat files from the
older layout:
Get-Content (Get-ChildItem C:\ProgramData\ManagedBootstrap\logs -Filter *.log -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName -Wait -Tail 50
The GUI's Logs tab lists one entry per run, named by session id, and still lists the flat
files left at the root by the older layout. It opens log files with FileShare.ReadWrite, so a
run in progress can be read there too, and the Run tab tails the newest log while a run is
going. The tab shows bootstrap.log only; events.jsonl and session.json are not surfaced
in the GUI. Both viewers
colourise by substring — and neither of them matches the WARN the file actually writes, so
warnings render as ordinary lines in the GUI. Use the console or the raw file when you care
about warnings.
Written to HKLM\SOFTWARE\Cimian\BootstrapMate\Status\SetupAssistant and
HKLM\SOFTWARE\Cimian\BootstrapMate\Status\Userland, in both the 64-bit and 32-bit registry
views.
| Value | Type | Meaning |
|---|---|---|
Stage |
REG_SZ |
Starting, Running, Completed, Failed or Skipped. Skipped means the manifest had no array for that phase. |
StartTime |
REG_SZ |
yyyy-MM-dd HH:mm:ss when the phase began. |
CompletionTime |
REG_SZ | Same format; written on terminal stages only. |
ExitCode |
REG_DWORD |
0, or 1 on a failed phase. |
Phase |
REG_SZ | The phase name, lowercased: setupassistant or userland. |
Architecture |
REG_SZ | OS architecture, uppercased. |
BootstrapUrl |
REG_SZ | The manifest URL used for this run. |
LastError |
REG_SZ | Exception message, or empty. |
RunId |
REG_SZ | GUID identifying this run. |
A phase in which one or more packages failed is Failed, with ExitCode 1 and a LastError
reading N package(s) failed: <names>. The other packages in that phase were still attempted;
Failed means "some package in this phase did not install", not "this phase stopped".
Phase status keys are cleaned up at startup: any whose CompletionTime is more than 24 hours
old, and whose stage is not Running, is removed.
At the end of a clean ProcessManifest, one value is written to
HKLM\SOFTWARE\Cimian\BootstrapMate in both views:
| Value | Type | Meaning |
|---|---|---|
LastRunVersion |
REG_SZ | The BootstrapMate build version that completed the run. |
A run with any failed package does not write it. Individual package failures are still caught
so the run can continue, but they are now collected and carried out: the phase is marked
Failed, the log records BootstrapMate completed with N failed package(s), and the registry
stamp is skipped so that a detection rule reading LastRunVersion keeps returning "not done"
and the deployment retries. LastRunVersion is therefore a usable signal that every package
in the manifest installed, as far as BootstrapMate can tell one installed. It cannot tell in
every case; see Troubleshooting and Gotchas.
C:\ProgramData\ManagedBootstrap\status.json — an indented JSON dictionary keyed by phase
name, rewritten on every status change. It carries the same fields as the registry values.
Status is written under SOFTWARE\Cimian\BootstrapMate. It is read from
SOFTWARE\BootstrapMate. Those are different keys, and the mismatch has direct consequences:
-
--statusreads its "Completion Status" fromHKLM\SOFTWARE\BootstrapMate\LastRunVersion, which is written by the MSI at install time, not by a run. It will report a version on a device where BootstrapMate has never completed a run. The "Registry Paths" and "Status File" hints--statusprints are pointing at the same wrong locations. -
--clear-statusdeletesSOFTWARE\BootstrapMate\Status\{phase}, a key nothing writes, and the fileC:\ProgramData\BootstrapMate\status.json, which is not where the status file lives. The real status underSOFTWARE\Cimian\BootstrapMate\Statusand the realC:\ProgramData\ManagedBootstrap\status.jsonare untouched. It is effectively a no-op. - The shipped Intune detection scripts read the MSI-written key, so they detect the MSI install rather than a successful bootstrap. See Deployment.
Query the real values directly:
Get-ItemProperty 'HKLM:\SOFTWARE\Cimian\BootstrapMate\Status\SetupAssistant'
Get-ItemProperty 'HKLM:\SOFTWARE\Cimian\BootstrapMate\Status\Userland'
Get-ItemProperty 'HKLM:\SOFTWARE\Cimian\BootstrapMate' -Name LastRunVersion
If ReportingUrl is set, BootstrapMate POSTs a JSON run summary at the end of the run, from
both the success and the failure path. An empty ReportingUrl disables it entirely. The POST
uses a 15-second timeout, sends User-Agent: BootstrapMate/{version}, and attaches
ReportingHeader as the Authorization header if one is configured. A failing POST never
fails the run — it logs Reporting POST failed: {msg} and moves on.
Payload fields: tool, platform, schemaVersion, version, runId, success,
startTime, endTime, durationSeconds, architecture, hostname, serialNumber,
manifestUrl, and phases — an object keyed SetupAssistant and Userland, each carrying
stage, exitCode, startTime, completionTime and lastError.
serialNumber comes from HKLM\HARDWARE\DESCRIPTION\System\BIOS → SystemSerialNumber, and
is an empty string when that value is unreadable. success is false when the run threw and
also when any package in it failed, so a summary with success: true is a run in which every
package installed.
Configure both values through Preferences.
In bootstrap.log, in order:
[…] INFO === BootstrapMate Session Started ===
[…] INFO Version: <yyyy.MM.dd.HHmm>
[…] INFO Settings loaded from: Management
[…] INFO Downloading manifest from: https://example.com/bootstrap/manifest.json
[…] INFO [SECTION] Processing Setup Assistant packages
[…] INFO [PROGRESS] Processing: <package>
[…] INFO [SUB-PROGRESS] Downloading from: https://example.com/pkgs/<file>
[…] INFO [SUB-PROGRESS] Downloaded: 12.4 MB
[…] INFO [SUCCESS] <package> installed successfully
[…] INFO [SECTION] Processing Userland packages
[…] INFO [COMPLETION] BootstrapMate completed successfully! (Completed: …, Total Duration: 214.7s)
A run that had failures instead ends with an ERROR line naming them, followed by a
[COMPLETION] line that counts them rather than declaring success:
[…] ERROR userland: 2 of 5 package(s) failed: Example App, Other App
[…] ERROR BootstrapMate completed with 2 failed package(s): Example App, Other App
[…] INFO [COMPLETION] BootstrapMate completed with 2 failed package(s) (Completed: …, Total Duration: 88.2s)
And on the device, after a healthy run:
- No
Failed to install packagelines anywhere in that log, and noERRORrecords inevents.jsonl. -
C:\ProgramData\ManagedBootstrap\cacheis empty. A cached file is deleted on a successful install and kept for inspection on a failure, so any file in there is a failed install. -
StageisCompleted(orSkippedfor a phase your manifest omits) under bothHKLM\SOFTWARE\Cimian\BootstrapMate\Status\SetupAssistantand...\Status\Userland. -
LastRunVersionis present underHKLM\SOFTWARE\Cimian\BootstrapMate.
The session-ended terminator you might expect is not written; [COMPLETION] is the last line
of a run either way, and session.json is left saying running.
What none of this changes is the process exit code, which is 0 on a run with failed packages
just as on a clean one. Exit code 1 is reserved for a run that threw. Detect on
LastRunVersion or the phase status, never on the exit code.