ADR-012: Alerting Rules Storage and Evaluation¶
Status: Accepted, amended 2026-07-08
Date: 2026-06-30
Last Updated: 2026-07-08
Decision: Store alerting rules as control-plane records; evaluate them in-process on the gateway via the existing cached-config path (amended from the original "defer to a future stream evaluator" decision)
Context¶
Operators need to define threshold-based conditions that should trigger alarms, for example, "temperature above 80°C for 5 consecutive minutes on pump_skid_01". The system must record these rules, make them visible and editable via the UI, and eventually evaluate them against live telemetry to produce Alarm records.
Two concerns are separable: rule storage (where and how rules are persisted) and rule evaluation (how and when the system checks incoming data against rules).
Options Considered¶
Option A: Rules stored at the gateway runtime; evaluated in-process¶
Rules are pushed to the gateway as configuration. The gateway runtime evaluates each reading against applicable rules as data flows through the validator module.
Pros: Low latency, works offline, no round-trip to control plane
Cons: Rules are not operator-visible in the control plane UI without syncing; hard to update rules without restarting the gateway runtime; duplicated rule logic with the validation-rules configuration in deployments
Option B: Rules stored in control plane; evaluation by a dedicated stream processor¶
Rules are persisted in the control-plane database. A separate Kafka Streams or Faust application subscribes to telemetry.clean, evaluates each record against active rules, and publishes to an alarms.raw topic.
Pros: Centralised management, independent scaling of the evaluation engine
Cons: Additional component with its own deployment and failure mode; overkill for the current deployment scale; not yet implemented
Option C: Rules stored in control plane; evaluation deferred¶
Rules are persisted in the control-plane PostgreSQL database (alerting_rules table). Full CRUD is exposed via GET/POST/PUT/DELETE /api/v1/alerting-rules. Evaluation is an explicit future step, rules currently serve as operator-visible policy documents consulted by the alert_router sink and any future evaluator. The Alarm table stores runtime alarm instances raised by the gateway validator (via deployment validation rules) or by future rule evaluation.
Pros: Consistent data model, immediate operator visibility, no premature component complexity, auditable
Cons: Rules are not evaluated automatically until a stream evaluator is built; operators must currently rely on deployment validation rules for real-time checking
Decision¶
Option C: Control-plane storage. Evaluation, however, did not end up deferred to a future stream evaluator, see the Amendment below for what actually shipped and why.
Amendment (2026-07-08): Evaluation happens in-process, not via a stream evaluator¶
When it came time to make alerting rules actually evaluate, the system did
not build the "future stream evaluator" this ADR originally proposed to
defer to (Option B). Instead, AlertingRule records are attached directly to
a Deployment (via a deployment_alarm_rules join table) and are shipped to
the gateway inside the same deployment-config payload the gateway already
polls/hot-reloads for deployment validation rules. The gateway runtime
validator evaluates them in-process, exactly as Option A ("rules stored at
the gateway runtime; evaluated in-process") originally described, the
option this ADR explicitly rejected.
This is a deliberate amendment, not silent drift, recorded here because the three downsides Option A was rejected for turned out not to apply against how the rest of the platform had already evolved:
- "Not operator-visible without syncing", doesn't apply. Rules are full
CRUD with a dedicated UI page (
AlertingRulesPage), same visibility Option C was chosen for. - "Hard to update without restarting the gateway runtime", doesn't
apply. The gateway's config-sync/hot-reload path (ADR-006) already diffs
and reloads
alarm_ruleson every config sync; no restart is needed. - "Duplicated rule logic with the validation-rules configuration in
deployments", doesn't apply in practice.
alarm_rulesis evaluated as its own distinct rule list by the validator, separate from range/rate-of- change/gap-detection validation logic.
In short: storage stayed centralized (the reason Option A was rejected), while evaluation moved to the edge (the reason Option A was attractive) by reusing infrastructure ADR-006 had already built, rather than standing up the dedicated stream-processing component Option B itself called "overkill for the current deployment scale." The dedicated stream evaluator described in Option B is no longer planned. If a future scale point genuinely requires independent evaluation-engine scaling, that would need its own ADR revisiting this decision; it is not presently on the roadmap.
Rationale¶
- Single source of truth for operator intent: Alerting rules belong with other operator-managed configuration (adapters, sinks, deployments) in the control plane. Operators should not need to look in multiple places.
- Separation of concerns: Storage and evaluation are independent problems. Solving storage correctly now does not foreclose any evaluation approach later.
- Avoids premature architecture: A dedicated stream evaluation engine introduces a new operational component. It should be introduced when the workload justifies it, not speculatively.
- Audit trail: All rule create/update/delete actions are recorded to
audit_eventsviarecord_audit_event(), which is consistent with all other control-plane mutations.
Data Model¶
The schema below reflects the shipped AlertingRule model, not the original
proposal, every column changed name or shape at least once during
implementation. See the Amendment above for why evaluation-related fields
(on_delay_seconds, deadband) exist instead of duration_minutes.
alerting_rules
├── rule_id VARCHAR(128) unique, indexed
├── name VARCHAR(256)
├── description VARCHAR(1024) nullable
├── status VARCHAR(16) default "enabled" -- "enabled" | "disabled"
├── parameter VARCHAR(255) indexed -- e.g. "temperature"
├── asset_id VARCHAR(255) nullable, indexed -- scope to one asset
├── adapter_id VARCHAR(128) nullable, indexed -- scope to one adapter
├── operator VARCHAR(4) -- ">", "<", ">=", "<=", "==", "!="
├── threshold FLOAT
├── unit VARCHAR(64) nullable
├── deadband FLOAT nullable
├── on_delay_seconds INTEGER nullable -- replaces duration_minutes
├── severity VARCHAR(16) default "MEDIUM", indexed
├── alarm_type VARCHAR(255) unique, indexed -- operator-defined, not a fixed sentinel
├── active_message VARCHAR(1024)
├── clear_message VARCHAR(1024)
├── requires_acknowledgement BOOLEAN default true
├── escalation_policy_id VARCHAR(128) nullable FK → escalation_policies(policy_id) ON DELETE SET NULL
├── created_by VARCHAR(128)
├── updated_by VARCHAR(128)
├── created_at TIMESTAMPTZ
└── updated_at TIMESTAMPTZ
AlertingRule also joins to Deployment via a deployment_alarm_rules
association table, a rule is only evaluated once it is attached to at least
one deployment (this is how it reaches the gateway).
Relationship to Deployment Validation Rules¶
Deployment validation rules (range, rate-of-change, gap-detection,
configured per-deployment in DeploymentCreatePage) and alerting rules (this
ADR) are both evaluated inside the gateway runtime, in-process, per the
Amendment above. They remain distinct rule lists with different trigger
conditions and different current output completeness:
| Trigger path | Configured where | Evaluated where | Output |
|---|---|---|---|
| Deployment validation rule (range/rate/gap) | Per-deployment form | Gateway runtime (in-process) | DLQ message only, does not yet raise an Alarm row (tracked gap, see ADR-014) |
| Alerting rule (this ADR) | Alerting Rules page, attached to a deployment | Gateway runtime (in-process, via deployment config sync) | Alarm |
| alert_router sink | Sink config | Sink container consuming alarms.raw |
External delivery |
Consequences¶
Positive¶
- Alerting rules are immediately manageable via UI and API
- Rule lifecycle is audited
- No new runtime component required, now or planned, evaluation reuses the existing gateway config-sync/validator path
- Rules evaluate automatically once attached to a deployment, with no separate stream-processing component to operate
Negative¶
- Deployment validation rules (range/rate/gap) still only produce DLQ entries, not
Alarmrows, an inconsistency with alerting rules (this ADR), which do raiseAlarmrows. Tracked as an open gap in ADR-014. escalation_policy_idFK is implemented (see ADR-013);alert_routerdoes not yet resolve or walk the linked policy's steps, tracked as an open gap in ADR-013/014.