---
module: agent.win-app-events
title: Windows App Event Log Monitor — forwards Windows Application-log errors as HiveIQ incidents
tab: Architect
order: 30
audience: dev
---

> **Internal · how it's built.** Confirmed against `hiveops-module-win-app-events` source (`com.hiveops.winevents`) 2026-07-01. See [technical.agent] for agent-wide module/event context.

## Maven module

`hiveops-module-win-app-events` (artifact `hiveops-module-win-app-events`, own version `1.0.1`, parented by `hiveops-agent-parent` `4.4.3`). Packaged as a standalone jar (`<finalName>${project.artifactId}</finalName>` → `hiveops-module-win-app-events.jar`). Dependencies are all `provided` (supplied by the fat JAR at runtime):

- `hiveops-core` — the `AgentModule` SPI, `ModuleContext`, `HttpClientHelper`/`HttpClientSettings`, `PatternReloadRegistry`.
- `hiveops-agent-journal` (`1.0.0`) — `IncidentEventClient`, `EventType`, `CreateJournalEventRequest` (the same event path `journal-events` uses).
- `log4j-api`, `commons-lang3`.

Registered via ServiceLoader SPI: `src/main/resources/META-INF/services/com.hiveops.core.module.AgentModule` → `com.hiveops.winevents.WinAppEventModule`.

## Classes and flow

Four classes, no sub-packages:

| Class | Role |
|---|---|
| `WinAppEventModule` | `AgentModule` impl — OS/enable gating, config parse, scheduler, hot-reload registration |
| `WinAppEventPoller` | `Runnable` — the anchor/poll loop; runs `wevtutil`, applies filters, emits events |
| `WinAppEventParser` | Turns raw `wevtutil` XML into `WinAppEvent` records |
| `WinAppEvent` | Immutable holder: `recordId, provider, eventId, level, timeCreated, message`; builds `matchText()` and `toDetails()` |

**Poll-based** cycle (not log-tail or ETW hook — it repeatedly shells `wevtutil` and diffs by RecordID):

1. **`initialize(ModuleContext)`** — bails (leaves `ready=false`) if not Windows, if `winevt.events.enabled != true`, or if no `agent.endpoint`/`incident.endpoint`. Otherwise builds an `IncidentEventClient` from `HttpClientHelper.loadSettingsFromProperties(props, prefix)` (prefix `"agent"` if `agent.endpoint` set, else `"incident"`), reads `levels`/`batch.size`/`sources`/pattern, constructs the `WinAppEventPoller`, resolves the `hiveops.properties` path for later reloads, and sets `ready=true`.
2. **`isEnabled()`** returns `ready`.
3. **`start()`** — creates a single daemon thread (`newSingleThreadScheduledExecutor`, thread name `win-app-events`) and `scheduleAtFixedRate(poller, 0, intervalSec, SECONDS)`. Then registers `this::reloadPatterns` with `PatternReloadRegistry`.
4. **`WinAppEventPoller.run()`** — if `suspended`, skip. If `lastEventRecordId < 0`, call `anchor()` and return (the very first tick). Otherwise `poll()`.
   - **`anchor()`** runs `wevtutil qe Application /rd:true /f:XML /c:1 /q:*[System[Level>=1 and Level<=<maxLevel>]]`, sets `lastEventRecordId` to that newest RecordID (or `0` if the log is empty). This is why no history is replayed.
   - **`poll()`** runs `wevtutil qe Application /rd:false /f:XML /LNIFO:Message /c:<batchSize> /q:*[System[(Level>=1 and Level<=<maxLevel>) and (EventRecordID > <lastId>)]]`. `/rd:false` = oldest-first (in-order processing); `/LNIFO:Message` renders human-readable message text from the provider DLL.
5. **Filtering** (in order, per event): the **level** filter is baked into the `wevtutil` XPath; then **source** filter (`sourceFilter` non-empty → Provider name must case-insensitively equal one entry); then **pattern** filter (`eventPattern != null` → regex `.find()` on `matchText()` = `"<provider> <eventId> <message>"`).
6. **Bookmark advance** — `lastEventRecordId` is set to the max RecordID seen this poll **even if every event was filtered out**, so filtered RecordIDs are never re-queried.
7. **Emit** — survivors become `CreateJournalEventRequest`s (`agentAtmId=atmName`, `eventType=WIN_APP_EVENT_ERROR`, `eventDetails=WinAppEvent.toDetails()`, `eventSource=HIVEOPS_AGENT_WINEVT`) and are sent via `client.sendEvents(list)`.

`WinAppEventParser` wraps the `wevtutil` output in a synthetic `<Events>…</Events>` root (handles both single- and multi-event output), DOM-parses each `<Event>`, and reads `EventRecordID`, `Provider@Name`, `Level` (mapped 1→Critical, 2→Error, 3→Warning, 4→Information), `EventID`, `TimeCreated@SystemTime`, and the message. Message extraction **prefers** `RenderingInfo/Message` (present because of `/LNIFO:Message`) and **falls back** to joining `EventData/Data` values with `" | "`. Unparseable individual events are skipped at DEBUG; a totally unparseable payload yields an empty list.

## Hot reload

`WinAppEventModule.reloadPatterns()` (registered with `PatternReloadRegistry`) re-reads `hiveops.properties` **from disk** (`startedFromDir/hiveops.properties`, cwd fallback) and calls `poller.setPattern(...)` and `poller.setSuspended(...)`. Both poller fields are `volatile`. `ProcessConfigUpdate` (the `CONFIG_UPDATE` fleet-task handler in `hiveops-file-collection`) writes the new properties then calls `PatternReloadRegistry.trigger()` — that's the entire hot-reload path. Level/sources/interval/batch are **not** re-read here (captured at `initialize()` time only).

## How events reach the platform

This module rides the **same journal-event path** as the `journal-events` module — there is no separate protocol:

```
WinAppEventPoller
   │  IncidentEventClient.sendEvents(List<CreateJournalEventRequest>)
   │  (endpoint from agent.endpoint | incident.endpoint; auth via HttpClientHelper)
   ▼
POST {endpoint}/api/journal-events      ← one HTTP POST per event (sendEvents loops sendEvent)
   ▼
agent-proxy  (agent-facing gateway, X-Institution-Key / token auth)
   ▼
hiveops-incident  → journal-event ingest → incident pipeline / device_summary
```

Note: `sendEvents` is **not** a batch POST — it iterates and POSTs each event individually to `/api/journal-events`; `winevt.events.batch.size` only bounds how many events one `wevtutil` call pulls, not the HTTP shape.

## Platform abstraction

The module does **not** use `com.hiveops.platform.PlatformServiceFactory` / `PlatformService`. OS detection is done locally (`System.getProperty("os.name")…contains("windows")`) and it shells `wevtutil` directly via `ProcessBuilder`. The runner (`runWevtutil`) decodes stdout with the charset from the `file.encoding` system property (a comment notes `wevtutil` emits UTF-16LE and relies on the JVM's active code-page resolution), reads stdout fully, then `waitFor()`s; a non-zero exit is logged at DEBUG and yields an empty string (→ empty parse → no events that tick).
