146 lines
6.6 KiB
SQL
146 lines
6.6 KiB
SQL
-- Bell v6 append-only rule evaluation, Alert identity and state transitions.
|
|||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS bell.rule_versions (
|
||
|
|
id text PRIMARY KEY,
|
||
|
|
tenant_id bigint NOT NULL,
|
||
|
|
site_id bigint,
|
||
|
|
rule_key text NOT NULL,
|
||
|
|
version integer NOT NULL,
|
||
|
|
display_name text NOT NULL,
|
||
|
|
event_kind text NOT NULL,
|
||
|
|
minimum_severity text NOT NULL,
|
||
|
|
enabled boolean NOT NULL,
|
||
|
|
effective_from timestamptz NOT NULL,
|
||
|
|
config_hash bytea NOT NULL,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||
|
|
CONSTRAINT bell_rule_version_id CHECK (id ~ '^ruv_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||
|
|
CONSTRAINT bell_rule_version_scope CHECK (tenant_id >= 1 AND (site_id IS NULL OR site_id >= 1)),
|
||
|
|
CONSTRAINT bell_rule_version_key CHECK (rule_key ~ '^[a-z][a-z0-9_-]{2,63}$'),
|
||
|
|
CONSTRAINT bell_rule_version_number CHECK (version >= 1),
|
||
|
|
CONSTRAINT bell_rule_version_name CHECK (char_length(btrim(display_name)) BETWEEN 1 AND 120),
|
||
|
|
CONSTRAINT bell_rule_version_kind CHECK (event_kind ~ '^[a-z][a-z0-9_]{2,63}$'),
|
||
|
|
CONSTRAINT bell_rule_version_severity CHECK (minimum_severity IN ('low','medium','high','critical')),
|
||
|
|
CONSTRAINT bell_rule_version_hash CHECK (octet_length(config_hash) = 32),
|
||
|
|
UNIQUE (tenant_id, rule_key, version),
|
||
|
|
UNIQUE (tenant_id, rule_key, config_hash)
|
||
|
|
);
|
||
|
|
ALTER TABLE bell.rule_versions OWNER TO bell_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS bell.event_rule_sweeps (
|
||
|
|
event_id text PRIMARY KEY REFERENCES bell.events(id),
|
||
|
|
swept_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||
|
|
);
|
||
|
|
ALTER TABLE bell.event_rule_sweeps OWNER TO bell_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS bell.rule_evaluations (
|
||
|
|
id text PRIMARY KEY,
|
||
|
|
event_id text NOT NULL REFERENCES bell.events(id),
|
||
|
|
rule_version_id text NOT NULL REFERENCES bell.rule_versions(id),
|
||
|
|
result text NOT NULL,
|
||
|
|
reason text NOT NULL,
|
||
|
|
evaluated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||
|
|
CONSTRAINT bell_rule_evaluation_id CHECK (id ~ '^eva_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||
|
|
CONSTRAINT bell_rule_evaluation_result CHECK (result IN ('matched','no_match')),
|
||
|
|
CONSTRAINT bell_rule_evaluation_reason CHECK (reason IN ('matched','disabled','event_kind','severity')),
|
||
|
|
UNIQUE (event_id, rule_version_id)
|
||
|
|
);
|
||
|
|
ALTER TABLE bell.rule_evaluations OWNER TO bell_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS bell.alerts (
|
||
|
|
id text PRIMARY KEY,
|
||
|
|
tenant_id bigint NOT NULL,
|
||
|
|
site_id bigint NOT NULL,
|
||
|
|
rule_evaluation_id text NOT NULL UNIQUE REFERENCES bell.rule_evaluations(id),
|
||
|
|
rule_version_id text NOT NULL REFERENCES bell.rule_versions(id),
|
||
|
|
severity text NOT NULL,
|
||
|
|
title text NOT NULL,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||
|
|
CONSTRAINT bell_alert_id CHECK (id ~ '^alt_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||
|
|
CONSTRAINT bell_alert_scope CHECK (tenant_id >= 1 AND site_id >= 1),
|
||
|
|
CONSTRAINT bell_alert_severity CHECK (severity IN ('low','medium','high','critical')),
|
||
|
|
CONSTRAINT bell_alert_title CHECK (char_length(btrim(title)) BETWEEN 1 AND 120)
|
||
|
|
);
|
||
|
|
ALTER TABLE bell.alerts OWNER TO bell_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS bell.alert_events (
|
||
|
|
alert_id text NOT NULL REFERENCES bell.alerts(id),
|
||
|
|
event_id text NOT NULL REFERENCES bell.events(id),
|
||
|
|
linked_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||
|
|
PRIMARY KEY (alert_id, event_id),
|
||
|
|
UNIQUE (event_id, alert_id)
|
||
|
|
);
|
||
|
|
ALTER TABLE bell.alert_events OWNER TO bell_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS bell.alert_transitions (
|
||
|
|
id text PRIMARY KEY,
|
||
|
|
alert_id text NOT NULL REFERENCES bell.alerts(id),
|
||
|
|
sequence integer NOT NULL,
|
||
|
|
from_state text,
|
||
|
|
to_state text NOT NULL,
|
||
|
|
actor_ref text NOT NULL,
|
||
|
|
note text,
|
||
|
|
occurred_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||
|
|
CONSTRAINT bell_alert_transition_id CHECK (id ~ '^trn_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||
|
|
CONSTRAINT bell_alert_transition_sequence CHECK (sequence >= 1),
|
||
|
|
CONSTRAINT bell_alert_transition_from CHECK (from_state IS NULL OR from_state IN ('open','acknowledged')),
|
||
|
|
CONSTRAINT bell_alert_transition_to CHECK (to_state IN ('open','acknowledged','closed')),
|
||
|
|
CONSTRAINT bell_alert_transition_edge CHECK (
|
||
|
|
(sequence = 1 AND from_state IS NULL AND to_state = 'open') OR
|
||
|
|
(sequence > 1 AND from_state = 'open' AND to_state = 'acknowledged') OR
|
||
|
|
(sequence > 1 AND from_state = 'acknowledged' AND to_state = 'closed')
|
||
|
|
),
|
||
|
|
CONSTRAINT bell_alert_transition_actor CHECK (
|
||
|
|
char_length(actor_ref) BETWEEN 1 AND 80 AND actor_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:@/-]*$'
|
||
|
|
),
|
||
|
|
CONSTRAINT bell_alert_transition_note CHECK (note IS NULL OR char_length(note) <= 500),
|
||
|
|
UNIQUE (alert_id, sequence)
|
||
|
|
);
|
||
|
|
ALTER TABLE bell.alert_transitions OWNER TO bell_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS bell.alert_command_receipts (
|
||
|
|
tenant_id bigint NOT NULL,
|
||
|
|
idempotency_key text NOT NULL,
|
||
|
|
command_hash bytea NOT NULL,
|
||
|
|
response_status integer NOT NULL,
|
||
|
|
response_body jsonb NOT NULL,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||
|
|
PRIMARY KEY (tenant_id, idempotency_key),
|
||
|
|
CONSTRAINT bell_alert_command_tenant CHECK (tenant_id >= 1),
|
||
|
|
CONSTRAINT bell_alert_command_key CHECK (
|
||
|
|
char_length(idempotency_key) BETWEEN 8 AND 128
|
||
|
|
AND idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]*$'
|
||
|
|
),
|
||
|
|
CONSTRAINT bell_alert_command_hash CHECK (octet_length(command_hash) = 32),
|
||
|
|
CONSTRAINT bell_alert_command_status CHECK (response_status IN (200,409)),
|
||
|
|
CONSTRAINT bell_alert_command_body CHECK (jsonb_typeof(response_body) = 'object')
|
||
|
|
);
|
||
|
|
ALTER TABLE bell.alert_command_receipts OWNER TO bell_app;
|
||
|
|
|
||
|
|
DO $immutable$
|
||
|
|
DECLARE
|
||
|
|
table_name text;
|
||
|
|
BEGIN
|
||
|
|
FOREACH table_name IN ARRAY ARRAY[
|
||
|
|
'rule_versions','event_rule_sweeps','rule_evaluations','alerts',
|
||
|
|
'alert_events','alert_transitions','alert_command_receipts'
|
||
|
|
] LOOP
|
||
|
|
EXECUTE format('DROP TRIGGER IF EXISTS %I ON bell.%I', 'bell_' || table_name || '_immutable', table_name);
|
||
|
|
EXECUTE format(
|
||
|
|
'CREATE TRIGGER %I BEFORE UPDATE OR DELETE ON bell.%I FOR EACH ROW EXECUTE FUNCTION bell.reject_immutable_change()',
|
||
|
|
'bell_' || table_name || '_immutable', table_name
|
||
|
|
);
|
||
|
|
END LOOP;
|
||
|
|
END
|
||
|
|
$immutable$;
|
||
|
|
|
||
|
|
CREATE INDEX IF NOT EXISTS bell_rule_versions_effective_idx
|
||
|
|
ON bell.rule_versions(tenant_id, rule_key, effective_from DESC, version DESC);
|
||
|
|
CREATE INDEX IF NOT EXISTS bell_alerts_scope_time_idx
|
||
|
|
ON bell.alerts(tenant_id, site_id, created_at DESC, id DESC);
|
||
|
|
CREATE INDEX IF NOT EXISTS bell_alert_events_event_idx ON bell.alert_events(event_id, alert_id);
|
||
|
|
CREATE INDEX IF NOT EXISTS bell_alert_transitions_latest_idx
|
||
|
|
ON bell.alert_transitions(alert_id, sequence DESC);
|
||
|
|
|
||
|
|
INSERT INTO bell.schema_migrations(version) VALUES (6)
|
||
|
|
ON CONFLICT (version) DO NOTHING;
|