Skip to content

Logging and Reporting

Rod Christiansen edited this page Sep 3, 2026 · 1 revision

Logging and reporting

StartSet writes three things: a Serilog stream (console and the Windows event log), a per-run session directory on disk, and a set of aggregated reports for monitoring tools to read.

Serilog sinks

Sink When Minimum level
Console CLI only (not the service) the configured level
Windows event log — log Application, source StartSet service mode, and CLI when the configured level is at or below Warning Warning

There is no Serilog file sink. File logging is entirely the session logger's job.

The level comes from Config.yaml: log_level if set, otherwise Debug when debug or verbose is true, otherwise Information. The --verbose / --debug command-line options do not change it.

Read the event log for a quick view of failures:

Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='StartSet'} -MaxEvents 50

If the source has never been registered, register it once from an elevated shell:

New-EventLog -LogName Application -Source StartSet

Session directories

Every run — a service boot pass, a logon pass, a trigger pass, a CLI invocation — opens a session.

C:\ProgramData\ManagedState\logs\YYYY-MM-DD\HHMM\
├── startset.log     human-readable
├── session.json     session metadata and summary
└── events.jsonl     structured event stream, one JSON object per line

The session id is YYYY-MM-DD-HHMM. Two sessions in the same minute collide, and the second gets a _2 suffix on the directory and the id, up to _9.

The run type is recorded in session.json and is one of boot, login, trigger, or — from the CLI — the first argument you typed (so a managedstatekeeper list run has run type list).

startset.log lines look like this:

[2026-03-01 08:14:22] INFO  Found 3 scripts in C:\ProgramData\ManagedState\login-every
[2026-03-01 08:14:24] INFO  [Map-DepartmentDrives] stdout | Mapped R: to \\fileserver\share
[2026-03-01 08:14:24] WARN  Deferred Set-Wallpaper.ps1: could not reach the console user's session (no active console session). The script did NOT run.

Payload stdout and stderr are folded into this log when log_script_output is true, prefixed [<script name>] stdout | or [<script name>] stderr |, capped at 500 lines per stream per payload with a ... output truncated after 500 lines marker. There are no per-script sidecar files; a payload runs as a child of the StartSet process so its output is already in hand, and splitting the record in two only made it harder to read.

session.json

{
  "session_id": "2026-03-01-0814",
  "start_time": "2026-03-01T08:14:20.1234567-08:00",
  "end_time": "2026-03-01T08:14:31.7654321-08:00",
  "run_type": "login",
  "status": "completed",
  "duration_seconds": 11,
  "summary": {
    "total_scripts": 3,
    "succeeded": 2,
    "failed": 0,
    "skipped": 1,
    "scripts_handled": ["Map-DepartmentDrives.ps1", "Set-Wallpaper.ps1", "Configure-Taskbar.ps1"]
  },
  "environment": {
    "hostname": "WORKSTATION01",
    "user": "SYSTEM",
    "os_version": "Microsoft Windows NT 10.0.26200.0",
    "architecture": "x64",
    "process_id": 4812,
    "log_version": "1.0"
  }
}

status is running while the session is open, then completed, completed_with_errors or failed. Note the summary counts do not carry a deferred column — deferred payloads are visible in events.jsonl and items.json, and in the log warnings.

events.jsonl

One JSON object per line. Script executions look like this:

{"event_id":"2026-03-01-0814-638...","session_id":"2026-03-01-0814","timestamp":"2026-03-01T08:14:24.12","level":"INFO","event_type":"script_execution","script_name":"Map-DepartmentDrives.ps1","action":"LoginEvery","status":"completed","message":"Exit code 0","duration_ms":1834}
Field Values
event_type script_execution
action the payload type
status completed, failed, skipped, deferred
level ERROR when failed, INFO when completed, otherwise DEBUG
duration_ms, error present when applicable

Reports

C:\ProgramData\ManagedState\reports\ is regenerated at the end of every session. It is the directory a monitoring or inventory agent should read.

File Contents
sessions.json The last 100 sessions' session.json documents, newest first
events.json Every event from the last 10 session directories that is newer than 48 hours
items.json One record per payload this session considered
run.log A copy of the current session's log, truncated and rewritten each session

run.log is the "what happened most recently" file; it is deleted and recreated at the start of every session, so do not treat it as history.

items.json

One record per payload, in the same shape sibling tooling uses for managed items so a single reader serves both. The type field is startset.

[
  {
    "id": "login-every/map-departmentdrives.ps1",
    "item_name": "Map-DepartmentDrives.ps1",
    "display_name": "Map-DepartmentDrives",
    "item_type": "ps1",
    "current_status": "Installed",
    "latest_version": "",
    "last_seen_in_session": "2026-03-01-0814",
    "last_attempt_time": "2026-03-01T16:14:22.0000000Z",
    "last_attempt_status": "Installed",
    "last_update": "2026-03-01T16:14:31.0000000Z",
    "failure_count": 0,
    "warning_count": 0,
    "type": "startset",
    "last_error": "",
    "action_performed": "execute"
  }
]

Status mapping:

Execution outcome current_status
Success Installed
Skipped because the run-once ledger says it already ran Installed
Skipped for any other reason Pending
Deferred — user session unreachable Pending
Failed, timed out, unsupported type Error

action_performed is install for .msi/.msix payloads and execute for everything else, and is absent for skipped and deferred payloads. last_seen_in_session is stamped only when the run actually acted on the payload, so filtering on a non-empty value gives you what this run did.

failure_count is derived by counting failed script_execution events across the last 50 session directories within the last 7 days, folded together with whether the payload failed in this run. last_error and last_warning are the first non-blank line of the error or of stderr, truncated to 500 characters — a payload that succeeded but wrote to stderr gets a last_warning, which is the nearest thing a script has to a warning signal.

A session that considered no payloads leaves the previous items.json in place rather than writing an empty one.

Retention

Retention runs asynchronously at the start of every session and is 30 days.

Target Rule
logs\YYYY-MM-DD\ day directories Deleted whole when the date is older than the cutoff
Loose files directly in logs\ Deleted when last written before the cutoff
Files in logs\installs\ Deleted when last written before the cutoff

The loose-file sweep is deliberately not recursive — session directories are aged as a unit by date, and picking old files out of one individually would leave half a session's log set behind. It is also not filtered by filename: anything sitting loose in the log directory is treated as a log and aged. If a payload of yours writes files into C:\ProgramData\ManagedState\logs, they will be deleted after 30 days. Write payload output somewhere else if you need it kept.

reports\ is not aged — its files are rewritten in place each session.

Reading a run

Latest session directory:

Get-ChildItem C:\ProgramData\ManagedState\logs -Directory |
    Sort-Object Name -Descending | Select-Object -First 1 |
    Get-ChildItem -Directory | Sort-Object Name -Descending | Select-Object -First 1

Most recent run's log:

Get-Content C:\ProgramData\ManagedState\reports\run.log

Everything that did not come out clean:

Get-Content C:\ProgramData\ManagedState\reports\items.json |
    ConvertFrom-Json | Where-Object current_status -ne 'Installed'

Deferred payloads across recent sessions:

Get-Content C:\ProgramData\ManagedState\reports\events.json |
    ConvertFrom-Json | Where-Object status -eq 'deferred'

Clone this wiki locally