Writing evman pages
An event record carries a type name and a MessagePack payload, but never its meaning — it does not say that outcome.reason is only present on failure, or which of two masks a field holds. That knowledge ships with the software that writes the event, as documentation evman reads. From the operator's side this is the event manual; from yours, the package author's, it is a file you write and install.
This page is how you write one. It assumes you have already designed your events — their types, fields and tiers — as Naming and shaping your events describes. The normative rules are PGSS §6.10; this page applies them.
Where the documentation lives #
evman reads a drop-in directory, /usr/share/evman/. Each *.evman file in it is one fragment, named for the component that ships it:
/usr/share/evman/
kernel.evman # the platform fragment: the shared field vocabulary
kacs.evman
org.example.backup.evman # yours
A package ships one fragment per component, describing every event type that component writes and every field it defines. There is no central index to register with and no install-time hook: dropping the file in is the whole act, and removing it on uninstall is the whole undo.
Ship it the way you ship any other file. A hand-written fragment kept beside the recipe maps straight into the payload from the package file's [files] table:
[]
= "usr/share/evman/org.example.backup.evman"
See Packages for ref resolution.
The shape of a fragment #
A fragment is a sequence of records. Each begins with an anchor line — three hyphens, a space, the record kind, a space and the name — then header lines, a blank line, and prose:
--- field object.org.example.backup.snapshot.id
type: bin.guid
The snapshot an event concerns, by the GUID the backup tool assigned it.
There are three kinds of record:
| Kind | Defines | Header keys |
|---|---|---|
field | One field path | type, and for an enumeration values and closed |
group | A named set of fields that travel together | include: lines, one field each |
event | One event type | tier, gating if anything decides which occurrences are recorded, cardinality if one occurrence makes more than one record |
The prose's first sentence is the summary evman shows in its indexes and -k results, so make it a complete sentence that stands on its own. Inline markup is **bold** and `code` only. The full key tables, including the rarely needed asserted and carried, are in §6.10.
Event records list their fields #
After an event's prose come the fields it carries, one per line, each saying when it is present:
field: object.file.path required
field: object.file.size when outcome.success == true
field: outcome.reason when outcome.success == false
values: disk-full | source-unreadable | cancelled
closed: false
Why the snapshot was not written.
- Presence is
required,optional, orwhenand a condition on another field the event carries. - An indented paragraph after a
field:line is a gloss: what the field means in this event. Use it wherever the general definition is broader than what this event puts there. A gloss may narrow a definition and must not contradict it. - Indented
values:andclosed:lines declare the values of a field whose definition leaves them to each event.outcome.reasonis the common case: every event has its own reasons. include: <group>carries every field of a group at once.
Define your own fields; reference everyone else's #
This is the rule that matters most, and the one a new author most often gets wrong.
A --- field record defines a field. A field: line on an event carries it. You carry any field defined anywhere — subject.token.sid, object.file.path, outcome.success are all defined in the platform fragment, and your events reference them freely. You define a field only when no existing one has the meaning you need, and then only beneath your own package name, under the role it describes:
--- field object.org.example.backup.snapshot.id
Two fragments defining the same path is an error, and so is redefining a standard field "to be safe": one value with two names is a value a security descriptor can deny under one and leak under the other. Before defining anything, look for it:
If you find a field every package like yours would want and nobody defines, that is a gap in the shared vocabulary worth reporting rather than working around.
Enumerations declare their values #
A field whose type is str.enum or uint.enum lists its values and says whether the list is closed:
--- field object.org.example.backup.snapshot.kind
type: str.enum
values: full | incremental | differential
closed: true
Which kind of snapshot was taken. ...
- A
str.enumvalue is a kebab-case string. Auint.enumvalue is a number and the name it renders as:0 None | 512 Protected. closed: truepromises that no value is added without a new version of the field. Consumers rely on it, so only claim it when the set is genuinely complete; otherwise sayfalse, and consumers will cope with a value they have not seen.- Take the values from your code, not from memory. Nothing checks that the list matches what your program writes.
A complete fragment #
--- field object.org.example.backup.snapshot.id
type: bin.guid
The snapshot an event concerns, by the GUID the backup tool assigned it.
Stable for the life of the snapshot, so it joins a snapshot's creation to
its later verification and deletion.
--- event org.example.backup.snapshot.created
tier: standard
A snapshot was written, or an attempt to write one failed.
field: object.kind required
Always `file`.
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
Only object.org.example.backup.snapshot.id is defined here. Every other field is the platform's, and the event references it.
The lint workflow #
Lint your fragment together with the installed ones. The rules that matter most — every referenced field is defined somewhere, no path is defined twice, no path is both a value and a map — can only be checked across the whole catalogue, and your fragment references fields defined in others. evman lint checks every rule of §6.10, prints each finding as file:line: rule N: message, and exits non-zero if there is any. A clean catalogue prints nothing.
Wire it into your build so a broken fragment fails the build rather than shipping.
Preview before you ship #
evman honours EVMAN_DIR, so you can see exactly what an operator will see without installing anything. Copy the installed fragments and yours into one directory and point it there:
&&
EVMAN_DIR=/tmp/evman
EVMAN_DIR=/tmp/evman
Rules the tools won't catch #
evman lint checks structure, names, definitions and references. These it does not check, and they are on you:
- That a value list is right. Lint checks that an enumeration has values, not that they are the ones your program writes.
- That a condition is true.
when outcome.success == falseis checked for naming a field the event carries, not for matching when your program actually writes the field. - That the gloss agrees with the definition. A gloss may narrow a field's meaning, never change it. If you find yourself writing a gloss that redefines a field, you want a different field.
- That the tier is honest.
essentialis for events that are extremely important and rare, or already governed by their own configuration; it cannot be switched off. Packages submitted to the Peios package repository are checked for it by hand. - Never start a prose line with
---and a word. It is an anchor: it starts a new record and silently splits yours in two.
See also #
- evman — the operator's view: reading an event card, a field card, and the
-ksearch. - Naming and shaping your events — designing the events this page documents.
- The Catalogue — the normative format and rules.
- Writing regman pages — the same job for your registry keys.