Windows Event Log - ptweezy/cronstable GitHub Wiki

Windows Event Log

The eventlog reporter writes job outcomes to the Windows Event Log for Event Viewer, Windows Event Forwarding, SCOM, and SIEM tools. Configure it in a report block alongside sentry, mail, shell, webhook, or push.

See reporting for the hooks and the block's shared shape, and running on Windows for the rest of the platform's behavior.

It is Windows only. On any other platform it does nothing, and cronstable logs a warning on every configuration load, including --validate-config, rather than dropping reports silently.

Enabling it

jobs:
  - name: nightly-backup
    command: backup.cmd
    schedule: "0 3 * * *"
    onFailure:
      report:
        eventlog:
          enabled: true
    onSuccess:
      report:
        eventlog:
          enabled: true
Key Type Default Meaning
enabled bool false Write records for this hook.
source string cronstable The event source name that records are written under.
includeOutput bool false Carry the run's captured stderr (or stdout when stderr is empty), cut to its first 8000 characters, as the last insertion string.

source must be a plain name. An empty value, one containing \ or /, and the three log names Application, System, and Security are refused at configuration load, because a source names a key underneath a log rather than a log or a path. The check applies only to blocks that are enabled, because source carries its default into every report block of every hook.

What it writes

Event IDs

These are a contract. A SIEM rule, an Event Viewer custom view, and a forwarding subscription all key on the number, so a shipped ID keeps its meaning.

Event ID Level Category Fires on
1000 Information 1 a run succeeded (onSuccess)
1001 Error 1 a run failed (onFailure)
1002 Error 1 a run failed for the last time (onPermanentFailure)
1003 Warning 1 an SLA threshold was breached (onLate)
1010 Information 2 a daemon or orchestration event (notify:)
1011 Error 2 a daemon or orchestration event reporting failure
1020 Error 2 the Windows service refused to start

Jobs occupy a contiguous 1000 to 1003 band so one rule can express "anything that happened to a job", and daemon events start a fresh decade at 1010. The service host writes 1020 whenever it refuses a start before its log opens, whether or not any report: block enables the Event Log. Its source is the service's name, which is cronstable unless you installed the service under another name. An overdue job is a Warning rather than an Error on purpose. It has not failed, and an operator who pages on Level=2 in the Application log should not be paged by a threshold they set to be advisory.

The IDs are plain small integers with no severity or customer bits folded in. Event Viewer and the modern EventLog API report an ID masked to its low 16 bits, so a number carrying 0x20000000 would be documented as one value and displayed as another. Severity travels in the event Level, which is where every consumer already reads it.

Insertion strings

Every record carries the same eleven insertion strings in the same order. Position is the contract. Without a registered message DLL, there is no message table to name the fields, and a forwarder ships them as <Data> elements in order. A field that does not apply to an outcome is an empty string rather than absent, so the arity never changes.

# Field Contents
0 summary the one line a human reads in the Event Viewer list
1 name the job name (a DAG task's is <dag>.<taskId>), or the service name for 1020
2 outcome success, failure, permanent-failure, late, event, event-alert or start-refused
3 host the reporting host
4 exitCode the run's exit code
5 failReason why cronstable judged the run failed
6 runId the durable run ID, when a state: store is configured
7 startedAt ISO 8601 start instant
8 schedule the job's crontab line
9 detail per-outcome extras (SLA numbers, the event name, resource usage)
10 output the run's captured stderr (or stdout when stderr is empty), cut to its first 8000 characters, when includeOutput is on

Field 2 repeats the outcome as text so a consumer that only rule-matches event ID 1001 can still tell a failure from a permanent failure.

Fields are capped so that a record can never exceed what the API accepts. Everything except output is capped at 1024 characters, and output at 8000. A field cut short ends with ...[truncated]. The cap is arithmetic rather than a runtime trim because of what happens otherwise: when the combined vector is too long, ReportEventW writes no record at all, so an uncapped reporter would drop precisely the alerts for the jobs that produced the most output.

The "description not found" preamble

Because cronstable does not register its event source, Event Viewer has no message table to render and shows each record with a preamble like:

The description for Event ID 1001 from source cronstable cannot be found.
Either the component that raises this event is not installed on your local
computer or the installation is corrupted. You can install or repair the
component on the local computer.

followed by the insertion strings themselves. That preamble costs the rendered prose and nothing else. The provider name, the event ID, the level, the timestamp, and every insertion string are all present and unchanged, so the XML view, wevtutil, Get-WinEvent, a Windows Event Forwarding subscription, and every SIEM connector read the record exactly as they would a registered one.

Registering a source is an HKLM write, which needs Administrator. Without a message DLL to point EventMessageFile at, registering provides no benefit. Building one means an mc.exe and rc.exe step per architecture, shipping a second binary beside the executable, and the code-signing question that binary would raise. Therefore, cronstable writes as an unregistered source and documents the preamble rather than asking every install for Administrator to remove a cosmetic line.

If you have your own message DLL, you can register the source yourself. The reporter writes the same records either way:

reg add "HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Application\cronstable" /v EventMessageFile /t REG_EXPAND_SZ /d "C:\path\to\your-messages.dll" /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Application\cronstable" /v TypesSupported /t REG_DWORD /d 7 /f

This is optional and unsupported: the message file is yours, and its message IDs have to match the earlier table.

Reading the records

An unregistered source writes into the Application log. To read only cronstable's records:

Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = 'cronstable' } -MaxEvents 20
wevtutil qe Application /q:"*[System[Provider[@Name='cronstable']]]" /f:text /c:20 /rd:true

For only the failures, filter on the event IDs:

Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = 'cronstable'; ID = 1001, 1002 }

The same XPath works as an Event Viewer custom view and as the <Select> clause of a Windows Event Forwarding subscription.

Things to know before turning it on

The Application log is readable by every local account. That is why includeOutput is off by default, and it is the opposite default from push's includeLogTail. A push payload is sealed to a paired device's key, but anything written here is visible to anyone who can open Event Viewer on the host. Turning includeOutput on is a decision to publish that job's output locally.

Queued records are not durable. Writes are handed to a background thread so a busy Event Log service can never delay a job's completion handling. A graceful shutdown drains that queue with a five-second bound. An abrupt process exit drops whatever had not yet reached the service.

It complements a log file; it does not replace one. The logging: section captures the daemon's own log (see logging configuration). This reporter publishes per-run outcomes. An unattended deployment usually benefits from both. The standard library's logging.handlers.NTEventLogHandler is not an alternative, because it needs pywin32, which the frozen executable does not carry.

On POSIX it does nothing, and cronstable logs a warning on every configuration load, including --validate-config, naming every job, DAG task, or notify block that enabled it:

report.eventlog is enabled (job nightly-backup), but Windows Event Log is unavailable on this platform. Event Log reports will be skipped; other configured reporters still run normally

A warning rather than a refusal, so one configuration directory can serve a mixed fleet. There is nothing an operator could install on Linux to satisfy a refusal, which is what separates this from the user/group rejection on Windows.

Related pages

⚠️ **GitHub.com Fallback** ⚠️