Skip to content

Events

Partial

An event is our semantic assertion that something happened, in our vocabulary: a discrete, point-in-time occurrence the action layer reacts to, owned through the same exclusive-arc as a sample. It is not a sample (a sample records a value; an event records an occurrence, see the has-a-value-now razor). Samples are what rules read; events are what event rules produce. The rules that produce events live on calculations; the alarms paired events drive, and the actions that respond, live on alarms and actions.

A sample and an event are different shapes (a sample has a value; an event is an occurrence), so each gets a registry named for what it holds. The sample half is property_type; the event half is event_type. We do not force them into one universal registry, that would be the false unification the rest of the model avoids.

event_type describes every event key: (name, display_name, payload_schema, official, ...). The built registry carries the official boolean (shipped-canonical versus org-local), like the sample registry; the fuller scope ladder (template / org / official, where a template can define a template-local event) is future design. Declaring event types (call.started, cable.unplugged, command.issued) is first-class and valuable: it gives events a known schema, makes them inspectable, and is what a derivation rule targets when it turns a raw log line into a registered event. Raw log lines live in a separate ingest lane (the log_line table), untyped, until a rule derives a registered event from one; most never become events at all.

The naming convention is consistent: a _type registry defines what a thing is, named for the thing (property_type, event_type, like location_type, interface_type). Events get their own registry because an event is a different shape from a sample. The designed scope axis would work the same way as for samples: see key scope.

Events: caught, caused, derived, scheduled

Section titled “Events: caught, caused, derived, scheduled”

An event arrives one of four ways; none is auto-manufactured from a state flip (a transition is already two consecutive sample rows, derivable by query).

  1. caught: a component publishes a structured occurrence natively (an xAPI Event channel, a webhook, an SNMP trap).
  2. caused: we issued a command, recorded as an event; this is what opens an intended sample.
  3. derived: an event rule fuses signals into an operator-meaningful fact (“codec in-call + traffic spike + room booked, so meeting started”), or turns a raw log line from the log_line lane into a registered event, inferred without instrumenting the control system.
  4. scheduled: the clock fired a schedule. A schedule fire is an event with origin=scheduled, manufactured by the clock (a leader-elected singleton held via a NATS KV CAS lock, exactly one active, failing over on death); there is no separate schedule log table. So action_rule subscribes to events uniformly (schedule to event to action: digests, synthetic checks, SLA resets are all schedule fires an action subscribes to).

Caught/caused/derived/scheduled is the event’s origin, a small vocabulary on the event table; it is not the same enum as sample provenance. The discipline that keeps an event-driven system from rotting is that events are declared (registered event keys) and rules are inspectable (the blast-radius preview in the UI).

The event row is the semantic-occurrence log; event_type is its key registry. The physical layout (partitioning, the owner arc, lineage) lives on storage.

An event is born in a Postgres transaction, on the record lane. When an event_rule fires, the consumer writes the event row and its paired alarm transition to PG in one transaction (the alarm edge is serialized per (event_rule, owner)); the event is the durable record, the alarm is the stateful edge. The event is not published directly from the rule (no dual-write): a leader-elected CDC publisher (logical decoding of the WAL) fans the committed change out to JetStream, where the action_rule consumers react. Postgres is the system of record; JetStream carries the committed event onward. This is the opposite lane from samples, which live on NATS and sink to PG asynchronously (see samples).

The event row is the semantic-occurrence log. There is no alarm_id column today; the event-carries-its-alarm edge is design direction. The lineage columns (ADR-0066) name what produced the event, while the flat correlation_id threads the causal chain. A schedule fire is an event with origin=scheduled; there is no separate schedule table.

event (telemetry): generated from the migrated schema by make gen
Column Type Constraints Notes
id bigint PK
ts timestamp with time zone not null, default now()
owner_kind text not null
instance text not null, default ''::text The sub-entity the occurrence is about, when the owner is not enough
message text not null, default ''::text
attributes jsonb
provenance text not null, default 'observed'::text
source text not null, default ''::text
source_rule text
source_rule_version bigint
component_id uuid FK → component.id
system_id uuid FK → system.id
location_id uuid FK → location.id
node_id uuid FK → node.principal_id
event_type_id uuid FK → event_type.id, not null The registered key this occurrence asserts
origin text not null, default 'caught'::text How the event arrived: caught, caused, derived, or scheduled
source_event_id bigint FK → event.id Lineage: the parent event, when one event caused another
correlation_id text Threads the causal chain flat, across lineage hops
source_log_line_id bigint FK → log_line.id Lineage: the raw log line a derivation rule promoted
derived_by_rule_id uuid Lineage: the rule that did the deriving; a natively-caught event has none
CHECK constraints and unique indexes on event
  • event_origin_check: CHECK ((origin = ANY (ARRAY['caught'::text, 'caused'::text, 'derived'::text, 'scheduled'::text])))
  • event_owner_arc_check: CHECK ((((owner_kind = 'component'::text) AND (component_id IS NOT NULL) AND (system_id IS NULL) AND (location_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'system'::text) AND (system_id IS NOT NULL) AND (component_id IS NULL) AND (location_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'location'::text) AND (location_id IS NOT NULL) AND (component_id IS NULL) AND (system_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'node'::text) AND (node_id IS NOT NULL) AND (component_id IS NULL) AND (system_id IS NULL) AND (location_id IS NULL))))
  • event_owner_kind_check: CHECK ((owner_kind = ANY (ARRAY['component'::text, 'system'::text, 'location'::text, 'node'::text])))
  • event_provenance_check: CHECK ((provenance = ANY (ARRAY['observed'::text, 'calculated'::text, 'intended'::text, 'declared'::text])))

event_type is the event-key registry: what a derivation rule targets when turning a raw log line (in the separate log_line lane) into a registered event. The built governance column is the official boolean (shipped-canonical versus org-local); the scope ladder (template / org / official) is future design, shared with property_type.

event_type (telemetry): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
name text not null
display_name text
description text not null, default ''::text
payload_schema jsonb The JSON Schema an occurrence of this key promises its attributes satisfy
official boolean not null, default false Shipped-canonical versus org-local; seed-owned rows are read-only
registered_at timestamp with time zone not null, default now()
CHECK constraints and unique indexes on event_type
  • event_type_name_key: CREATE UNIQUE INDEX event_type_name_key ON public.event_type USING btree (name)

Related: calculations (the event_rule that produces events), alarms and actions (alarms and the response layer), samples (the data events read), and the glossary.