Skip to content

Logging and Reporting

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

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.

The session directory

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

Line format

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 INFO and WARN are followed by two spaces and DEBUG/ERROR by 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:

Levels that reach the file

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.

Captured child output

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.

Structured events

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.

Retention

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.

Following a run live

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.

Status surface

Registry — per phase

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.

Registry — completion

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.

Status file

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.

The registry-path split

Status is written under SOFTWARE\Cimian\BootstrapMate. It is read from SOFTWARE\BootstrapMate. Those are different keys, and the mismatch has direct consequences:

  • --status reads its "Completion Status" from HKLM\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 --status prints are pointing at the same wrong locations.
  • --clear-status deletes SOFTWARE\BootstrapMate\Status\{phase}, a key nothing writes, and the file C:\ProgramData\BootstrapMate\status.json, which is not where the status file lives. The real status under SOFTWARE\Cimian\BootstrapMate\Status and the real C:\ProgramData\ManagedBootstrap\status.json are 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

Run summary reporting

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\BIOSSystemSerialNumber, 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.

What a healthy run looks like

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 package lines anywhere in that log, and no ERROR records in events.jsonl.
  • C:\ProgramData\ManagedBootstrap\cache is 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.
  • Stage is Completed (or Skipped for a phase your manifest omits) under both HKLM\SOFTWARE\Cimian\BootstrapMate\Status\SetupAssistant and ...\Status\Userland.
  • LastRunVersion is present under HKLM\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.

See also

Clone this wiki locally