Event Pipeline Architecture
The Event Pipeline ensures reliable event delivery through a persistent, file-based queue with per-sink delivery tracking. Events are written to an NDJSON file and processed in batches. Each sink maintains independent retry state with exponential backoff, and permanently failed events are moved to a dead-letter queue for manual resolution.
The ProfileSafeEvent Envelope
Every event dispatched through the pipeline is wrapped in a canonical ProfileSafeEvent envelope:
| Field | Description |
|---|---|
EventId |
Deterministic GUID derived from the audit sequence number (used for deduplication) |
TimestampUtc |
Event timestamp in UTC |
SchemaVersion |
Always 1.0.0 |
Severity |
Info, Warning, Error, or Critical (see Audit Logging — Severity Mapping) |
Source |
GUI, CLI, or Core |
OperationId |
GUID correlating all events from a single export/import/simulation run |
Category |
Export, Import, Simulation, Security, Privacy, or System |
Action |
The AuditAction name (e.g. ExportCompleted) |
Context |
MachineName, UserName, Domain, Tenant |
Payload |
Action-specific data (target, success, duration, file count, etc.) |
ErrorCode |
APS error code on failure (e.g. APS-2103), null on success |
Extensions |
Open key-value map for custom metadata |
For the full JSON schema, see Event Schema Reference.
Persistent Queue
Events are persisted to rotating NDJSON files in the EventPipeline folder:
| File | Purpose |
|---|---|
queue.jsonl |
Active events (pending, partially delivered) |
deadletter.jsonl |
Permanently failed events awaiting manual resolution |
The queue is cross-process safe via a Named Mutex and uses atomic write operations (temp file + move). Each queue entry stores the event payload (immutable) alongside per-sink delivery state (mutable).
Per-Sink Delivery Tracking
Each queue entry maintains an independent SinkDeliveryState for every active sink:
| Field | Description |
|---|---|
SinkId |
Identifies the target sink (e.g. siem-http, webhook-primary) |
Status |
Pending, Delivered, FailedPermanent, or Skipped |
IsCritical |
Whether this sink affects the overall event delivery state |
AttemptCount |
Number of delivery attempts so far |
NextAttemptUtc |
Earliest time for the next retry (backoff-controlled) |
LastError |
Truncated error message from the last failed attempt (max 500 characters) |
LastHttpStatus |
HTTP status code from the last failed attempt (if applicable) |
The overall event state is computed from critical sinks only: Delivered (all critical sinks succeeded or were skipped), PartiallyDelivered (at least one critical sink delivered), Pending (retries outstanding), or DeadLettered (all critical sinks permanently failed).
Failure Classification
When a sink delivery fails, the error is classified to determine whether the event should be retried:
| Class | Behavior | Examples |
|---|---|---|
| Transient | Retry with exponential backoff | HTTP 429 (rate limit), HTTP 5xx (server error), timeout, DNS failure, connection refused |
| Permanent | Dead-letter immediately | HTTP 400/401/403/404/405/409/422, certificate errors, authentication failures |
Unknown HTTP status codes default to transient (retry). Events also move to dead-letter when MaxRetries is reached (default: 10).
Exponential Backoff
Transient failures are retried with exponential backoff plus random jitter (±25%):
delay = BaseSeconds × 2^attemptCount (capped at MaxSeconds)
jitter = delay × 0.25 × random(-1..+1)
nextAttempt = now + max(BaseSeconds, delay + jitter)
The backoff policy is configured in eventpipeline.xml:
<Backoff>
<BaseSeconds>5</BaseSeconds>
<MaxSeconds>600</MaxSeconds>
</Backoff>
With the defaults, retries start at ~5 seconds and cap at 10 minutes. After 10 attempts over approximately 20 minutes, the event is dead-lettered.
eventpipeline.xml Reference
<EventPipeline xmlns="urn:appprofilesafe:eventpipeline:v1" SchemaVersion="1.0.0">
<QueueFolder>%ProgramData%\...\EventPipeline</QueueFolder>
<MaxRetries>10</MaxRetries>
<SendTimeoutSeconds>3</SendTimeoutSeconds>
<Backoff>
<BaseSeconds>5</BaseSeconds>
<MaxSeconds>600</MaxSeconds>
</Backoff>
<Sinks>
<Sink id="siem-http" critical="true" />
<Sink id="siem-syslog" critical="false" />
<Sink id="webhook-primary" critical="true" />
<Sink id="eventlog" critical="false" enabled="true"
sourceName="AppProfileSafe" logName="Application" />
</Sinks>
</EventPipeline>
Whether a sink is active is controlled by its own configuration file (<Active> in siem.xml, <Enabled> in webhook.xml). The <Sinks> section in eventpipeline.xml only declares routing metadata such as critical.
Queue Flushing
Queue processing is triggered in two ways:
- GUI:
DispatchAndFlushAsyncis called asynchronously (fire-and-forget) each time an audit entry is written. - CLI: Events are dispatched synchronously to the persistent queue during operations.
FlushQueueis called at the start and end of each CLI run to deliver all pending events to sinks. There is no background thread — all deliveries complete before the process exits.
Each flush processes up to FlushBatchSize (default: 100) due entries — only events where at least one sink has a pending delivery with NextAttemptUtc ≤ now.