Naming and shaping your 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:

  1. Does an operator care? If only the tool's developers would read it, write a log line instead.
  2. 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 kacs and peinit are reserved to platform components.
  • Add a noun only where someone would write a policy on the set beneath it. org.example.backup.snapshot groups 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, carrying outcome.success = false. Don't add a snapshot.failed type.
  • Use lowercase and kebab-case within a segment: copied-up, never copied_up or copiedUp.

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:

FieldValue in this event
object.kindfile
object.file.pathThe snapshot archive written
object.file.sizeIts size in bytes
operation.durationHow long the snapshot took
outcome.successWhether it was written
outcome.reasonWhy not, when it wasn't
outcome.errnoThe 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.sid or similar. Consumers resolve names when they read the record.
  • A field you have no value for. Leave the key out rather than writing nil, "" or 0.

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 valueWrite it as
A SIDThe binary SID, never an SDDL string
A GUID16 raw bytes, never a hyphenated string
A timeNanoseconds since the Unix epoch, as an unsigned integer
A durationNanoseconds, as an unsigned integer
A yes or noA MessagePack boolean, never the string "true"
One of a fixed setA 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 = peios_mp_writer_new();

peios_mp_write_map(w, 3);                       /* object, operation, outcome */

peios_mp_write_str(w, "object", 6);
peios_mp_write_map(w, 2);                       /* kind, file */
peios_mp_write_str(w, "kind", 4);
peios_mp_write_str(w, "file", 4);
peios_mp_write_str(w, "file", 4);
peios_mp_write_map(w, 2);                       /* path, size */
peios_mp_write_str(w, "path", 4);
peios_mp_write_str(w, path, path_len);
peios_mp_write_str(w, "size", 4);
peios_mp_write_uint(w, size_bytes);

peios_mp_write_str(w, "operation", 9);
peios_mp_write_map(w, 1);
peios_mp_write_str(w, "duration", 8);
peios_mp_write_uint(w, elapsed_ns);

peios_mp_write_str(w, "outcome", 7);
peios_mp_write_map(w, 1);
peios_mp_write_str(w, "success", 7);
peios_mp_write_bool(w, true);

When the snapshot fails, the same type carries the reason instead of the size:

peios_mp_write_str(w, "outcome", 7);
peios_mp_write_map(w, 3);
peios_mp_write_str(w, "success", 7);
peios_mp_write_bool(w, false);
peios_mp_write_str(w, "reason", 6);
peios_mp_write_str(w, "disk-full", 9);
peios_mp_write_str(w, "errno", 5);
peios_mp_write_int(w, -ENOSPC);

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:

TierUse it for
standardWhat an administrator reviews: lifecycle, configuration, failures. Most events.
verboseDetail for an active investigation. Off unless switched on.
debugFor your own developers. Off unless switched on.
essentialExtremely 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 #

Edit this page