Events
Single-page view · as markdown
Events overview
Peios / Developing for Peios / Events
KMES is Peios's event system, and it is the sole event path on the system. Audit records, subsystem events, and your own application events all travel the same way: the kernel stamps each event with trusted metadata and writes it into a per-CPU, lock-free ring buffer, and consumers drain those rings. This section teaches both sides — producing and consuming — via event.h and msgpack.h.
What an event is #
An event has two parts:
- Kernel-stamped metadata you cannot forge — a
CLOCK_REALTIMEtimestamp, a per-CPU monotonic sequence number, the CPU id, an origin class, and identity GUIDs (the effective token, the true token, and the process). This is the trustworthy skeleton: when you consume an event, you know who emitted it and when, because the kernel wrote that, not the emitter. - A payload — a single MessagePack value that you define. This is your event's actual content.
The event type names the kind of event. It's a dotted name made of your package name, what the event is about, and what happened, such as org.example.backup.snapshot.created. Its fields follow one vocabulary shared by every emitter on the system. Naming and shaping your events shows how to choose both.
Payloads are MessagePack, and you own them #
The kernel does not build or interpret payloads — it only structurally validates them on emit (one well-formed MessagePack value, within size and nesting limits). So userspace owns encoding and decoding, and the SDK ships a MessagePack codec whose validator's acceptance is matched to the kernel's check. Build a payload with the writer, and a successful peios_mp_writer_bytes (or peios_mp_validate) means the emit call will accept it.
Per-CPU rings and lost events #
Events live in per-CPU ring buffers — one ring per logical CPU, lock-free so producers never block on consumers. Two consequences shape how you consume:
- You drain per CPU. To see everything, run a reader per CPU (discover the count by attaching upward from CPU 0 until it fails).
- Rings can lap. If you don't drain fast enough, new events overwrite old ones you haven't read. The per-CPU
sequencenumbers are contiguous, so a gap in the sequence means events were lost — and the reader tracks that count for you.
Privileges #
The two sides are gated separately:
- Emitting requires
SeAuditPrivilege. - Consuming (attaching to a ring) requires
SeSecurityPrivilege.
Two ways to consume #
The SDK offers a high-level reader that hides the entire lock-free drain — barriers, lapping recovery, lost-event accounting, buffer-resize handling, and the wait — behind a simple next/wait loop. That's what almost everyone should use. There is also a low-level ring API for callers who need to drive the drain inside their own event loop. Both are covered in consuming events.
Where to go in this section #
- Naming and shaping your events — design an event before you emit it: its type, its fields, its tier and its catalogue entry.
- Emitting events — build a payload and emit, singly or in batches.
- Consuming events — drain the rings with the high-level reader (and, briefly, the low-level ring).
event.handmsgpack.h— the exhaustive reference.
Naming and shaping your events
Peios / Developing for Peios / Events
Every event on Peios follows one naming scheme and one field vocabulary, so that an operator can find your events with the same queries and security descriptors they use for everyone else's. This guide walks through designing one event, from deciding that it should be an event to describing it for the catalogue. The rules themselves are in PGSS Events; this page applies them.
The running example is a backup tool, packaged as org.example.backup,
that writes a snapshot archive and wants to record each one.
Decide that it is an event #
Ask two questions, in order:
- Does an operator care? If only the tool's developers would read it, write a log line instead.
- Does one instance matter on its own? If it does, it's an event. If only the trend matters — snapshots per hour, bytes per day — it's a metric.
An operator wants to know that last night's snapshot failed, so each snapshot is an event. See Events, logs and tiers.
Name the event type #
An event type is your package name, then one or more nouns from general to specific, then a past-tense verb:
org.example.backup.snapshot.created
- The root is your package name, split at its dots. Short roots such
as
kacsandpeinitare reserved to platform components. - Add a noun only where someone would write a policy on the set
beneath it.
org.example.backup.snapshotgroups everything about snapshots, which an administrator might plausibly switch off or grant access to as a whole. - End with what happened, in the past tense:
created,started,ended,deleted. - Use one type for success and failure. A failed snapshot is still
org.example.backup.snapshot.created, carryingoutcome.success = false. Don't add asnapshot.failedtype. - Use lowercase and kebab-case within a segment:
copied-up, nevercopied_uporcopiedUp.
See Event types for the full grammar.
Choose the fields #
A field is named for the part it plays, then what it is, then which property:
| Field | Value in this event |
|---|---|
object.kind | file |
object.file.path | The snapshot archive written |
object.file.size | Its size in bytes |
operation.duration | How long the snapshot took |
outcome.success | Whether it was written |
outcome.reason | Why not, when it wasn't |
outcome.errno | The error number, when there is one |
Look a field up before you invent one. The standard fields cover
almost everything an event carries: who acted (subject.*), what was
acted on (object.*), where something came from or went to (source.*
and destination.*), and how it turned out (outcome.*). Every defined
field is described in /usr/share/evman/, and
evman looks them up: evman object.file
lists what is defined under object.file, and evman -k size searches.
A few things never go in your payload:
- Anything the header already carries. The kernel stamps the time,
the sequence number and your token and process identities on every
event. Don't write a
timestamp. - A principal's name. Carry its SID in
subject.token.sidor similar. Consumers resolve names when they read the record. - A field you have no value for. Leave the key out rather than
writing nil,
""or0.
If your event genuinely needs a field the catalogue doesn't have, put it beneath your package name, under the role it describes:
object.org.example.backup.snapshot.id
This should be rare. If you find a field that every backup tool would want, that's a gap in the standard set worth reporting.
Encode the values #
Each field's type fixes its wire form. The ones you'll meet most:
| Kind of value | Write it as |
|---|---|
| A SID | The binary SID, never an SDDL string |
| A GUID | 16 raw bytes, never a hyphenated string |
| A time | Nanoseconds since the Unix epoch, as an unsigned integer |
| A duration | Nanoseconds, as an unsigned integer |
| A yes or no | A MessagePack boolean, never the string "true" |
| One of a fixed set | A kebab-case string: disk-full, not 2 or DISK_FULL |
The full table is in Values.
Build the payload #
A dotted field path is a chain of nested maps: object.file.path is
{object: {file: {path: …}}}. Never write a key containing a dot; it
becomes a different field that no query can reach.
peios_mp_writer *w = ;
; /* object, operation, outcome */
;
; /* kind, file */
;
;
;
; /* path, size */
;
;
;
;
;
;
;
;
;
;
;
;
When the snapshot fails, the same type carries the reason instead of the size:
;
;
;
;
;
;
;
;
outcome.reason is for filtering, so it's a value from a fixed set. If
you want to tell a person more, add outcome.detail as free text; nothing
parses it. See Outcomes.
Emitting events covers sending the finished payload.
Choose a tier #
Every event type declares how much it matters to an operator:
| Tier | Use it for |
|---|---|
standard | What an administrator reviews: lifecycle, configuration, failures. Most events. |
verbose | Detail for an active investigation. Off unless switched on. |
debug | For your own developers. Off unless switched on. |
essential | Extremely important and rare, or already governed by its own configuration. Never switched off. |
A snapshot record is standard. Reserve essential for the two reasons
in its row; a package submitted to the Peios package repository is
checked for it.
Administrators switch event types on and off in the emission policy,
under Machine\Generic\Events. Your types live under
Machine\Generic\Events\org\example\backup, and an event type that is
switched off is never written. See
Emission policy.
Describe it in a fragment #
Ship a fragment describing every event type you write and every field
you define, installed as /usr/share/evman/org.example.backup.evman:
--- field object.org.example.backup.snapshot.id
type: bin.guid
The snapshot an event concerns, by the GUID the backup tool assigned it.
--- event org.example.backup.snapshot.created
tier: standard
A snapshot was written, or an attempt to write one failed.
field: object.kind required
field: object.file.path required
The snapshot archive.
field: object.file.size when outcome.success == true
field: object.org.example.backup.snapshot.id required
field: operation.duration required
field: outcome.success required
field: outcome.reason when outcome.success == false
values: disk-full | source-unreadable | cancelled
closed: false
Why the snapshot was not written.
field: outcome.errno optional
You define only your own field. The standard ones are referenced, never
redefined: two fragments defining the same field is an error, because one
value with two names can be denied under one and read under the other.
Writing evman pages
covers the format, checking a fragment with evman lint, and installing
it; the normative rules are in
The catalogue.
If your program guards access to something #
A daemon that decides who may use its own objects doesn't emit its own "access denied" event. It makes the decision through the kernel's access check and passes the object's identity along, so that the kernel's audit records the decision like any other. See Access decisions.
Where to go next #
- Emitting events: send the payload, singly or in batches.
- PGSS Events: the rules this page applies, in full.
Emitting events
Peios / Developing for Peios / Events
Emitting an event is two steps: build a MessagePack payload, then hand it and an event type to the kernel. This guide shows both; event.h and msgpack.h are the full references. Emitting requires SeAuditPrivilege.
Choose the event type and fields first: Naming and shaping your events covers that, and the examples below use its backup-tool event.
Ask whether the type is on #
An administrator can switch event types on and off in the registry, and an emitter must not write a type that is switched off (PGSS §6.9). Open a policy handle once, and ask it before building each payload. The tier is the one your fragment declares for the type:
peios_event_policy *policy = ; /* once, at startup */
static const char type = "org.example.backup.snapshot.created";
if
return; /* off: build nothing */
The handle caches its answers and watches the registry, so asking costs little. A change the administrator commits applies to your next decision. Essential types are always on. See the emission policy for the details.
Build the payload #
Use the MessagePack writer to encode a single top-level value: a map whose nested maps spell the field paths. This payload carries outcome.success:
peios_mp_writer *w = ;
; /* {outcome: {success: true}} */
;
;
; ;
const void *payload;
ssize_t plen = ; /* validates as it borrows */
if
peios_mp_writer_bytes validates that what you built is exactly one well-formed value, so a non-negative return means the payload is emit-ready. (Remember a map of n needs 2*n values — one per key and value.)
Emit it #
static const char type = "org.example.backup.snapshot.created";
int rc = ;
;
if
The event type is length-counted UTF-8 and must be non-zero length. It isn't NUL-terminated on the wire, which is why the example passes sizeof type - 1. On success the kernel stamps the trusted metadata (timestamp, sequence, identity GUIDs) and sets origin_class = userspace, which queries see as emitter.class; you don't provide any of that, and your payload must not repeat it.
Validating untrusted payloads first #
If a payload's shape comes from dynamic or untrusted input, validate it in userspace before emitting so you handle the failure on your terms rather than as an EINVAL from the kernel:
if
The validator's acceptance matches the kernel's emit-time check at that depth bound.
Emitting in batches #
A high-rate producer should batch. peios_event_emit_batch emits many events in one call, so a single timestamp capture, identity capture, and consumer wake cover the whole set:
struct peios_event_entry entries = ;
uint32_t emitted = 0;
int rc = ;
if
count must be in [1, KMES_BATCH_MAX_ENTRIES]. On failure, errno is the reason the first failing entry failed and emitted tells you how many succeeded before it, so you know exactly where to resume. One caveat: rate-limiting is all-or-nothing for a batch — an EAGAIN emits none of it, so on EAGAIN back off and retry the whole batch.
Next #
- Consuming events — the other side of the pipe.
msgpack.hreference — the full encoder, including containers, extensions, and raw splicing.
Consuming events
Peios / Developing for Peios / Events
Consuming events means draining the per-CPU ring buffers. The SDK's high-level reader hides all the hard parts, so most consumers are a short loop. This guide shows that loop and how to parse what you read; event.h has the full API. Consuming requires SeSecurityPrivilege.
The reader loop #
Open a reader for a CPU, then loop next/wait:
peios_event_reader *r = ;
if
for
;
peios_event_reader_next returns 1 (event filled), 0 (nothing available — call wait), or -1 (error). peios_event_reader_wait blocks until events arrive or the timeout elapses (negative = forever). The reader handles the memory barriers, lapping recovery, buffer-resize handling, and futex wait internally — you just alternate the two calls.
Parsing an event #
Each struct peios_event gives you the trusted metadata by value and the payload as a MessagePack value. Parse it with a reader:
void
The lifetime rule #
ev->event_type and ev->payload point into the ring mapping. They are valid only until your next peios_event_reader_next call, and only while the slot hasn't been overwritten. So copy out anything you need to keep before continuing the loop — don't stash the raw pointers.
Draining every CPU #
Rings are per-CPU, so one reader sees only one CPU's events. To consume the whole machine, run a reader per CPU — typically one thread each. Discover the CPU count by attaching upward until it fails:
uint32_t ncpu = 0;
for
/* now spawn one peios_event_reader per cpu in [0, ncpu) */
Watching for loss #
If a consumer falls behind, the producer laps it and events are lost. Poll peios_event_reader_lost to see the cumulative count of lost events (derived from sequence gaps). A rising number means you aren't draining fast enough — process events more cheaply, hand off to a worker, or accept the loss deliberately.
The low-level ring #
If the reader's loop doesn't fit your event model — you want the ring integrated into an existing epoll/state-machine loop, driving the read position yourself — the low-level ring API exposes the mapping directly: peios_event_ring_map, the position accessors (write_pos/tail_pos/generation), peios_event_ring_event_at to parse a slot, and peios_event_ring_wait to sleep. It's more bookkeeping (you own the read position and the empty/lapping/generation checks) for more control. Reach for it only when you need to; the high-level reader is the right default.
Next #
event.hreference — the full reader and ring APIs, and everystruct peios_eventfield.- Auditing — the operator-side view of the event and audit stream.