-- Migration 006: Observability (TimescaleDB) -- Hypertable PKs include time column (SG1); idempotent DDL (SG3); no array_agg in CAGGs (SG2). -- Enable TimescaleDB extension CREATE EXTENSION IF NOT EXISTS timescaledb; -- ─── Time-series metrics ────────────────────────────────────────────── CREATE TABLE metric_samples ( ts TIMESTAMPTZ NOT NULL, entity_id UUID NOT NULL, metric TEXT NOT NULL, value DOUBLE PRECISION NOT NULL, tags JSONB NOT NULL DEFAULT '{}' ); SELECT create_hypertable('metric_samples', 'ts', chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE); CREATE INDEX idx_metrics_entity_ts ON metric_samples(entity_id, ts DESC); CREATE INDEX idx_metrics_metric_ts ON metric_samples(metric, ts DESC); DO $$ BEGIN PERFORM add_retention_policy('metric_samples', INTERVAL '90 days'); EXCEPTION WHEN OTHERS THEN NULL; END $$; -- 1-hour rollups CREATE MATERIALIZED VIEW metric_rollups_1h WITH (timescaledb.continuous) AS SELECT time_bucket('1 hour', ts) AS bucket, entity_id, metric, avg(value) AS avg_value, min(value) AS min_value, max(value) AS max_value, count(*) AS sample_count FROM metric_samples GROUP BY bucket, entity_id, metric; DO $$ BEGIN PERFORM add_continuous_aggregate_policy('metric_rollups_1h', start_offset => INTERVAL '2 hours', end_offset => INTERVAL '5 minutes', schedule_interval => INTERVAL '1 hour'); EXCEPTION WHEN OTHERS THEN NULL; END $$; -- 1-day rollups CREATE MATERIALIZED VIEW metric_rollups_1d WITH (timescaledb.continuous) AS SELECT time_bucket('1 day', ts) AS bucket, entity_id, metric, avg(value) AS avg_value, min(value) AS min_value, max(value) AS max_value, count(*) AS sample_count FROM metric_samples GROUP BY bucket, entity_id, metric; DO $$ BEGIN PERFORM add_continuous_aggregate_policy('metric_rollups_1d', start_offset => INTERVAL '2 days', end_offset => INTERVAL '1 hour', schedule_interval => INTERVAL '1 day'); EXCEPTION WHEN OTHERS THEN NULL; END $$; -- ─── Audit log ──────────────────────────────────────────────────────── CREATE TABLE audit_log ( id BIGINT GENERATED ALWAYS AS IDENTITY, ts TIMESTAMPTZ NOT NULL DEFAULT now(), actor_type TEXT NOT NULL, actor_id UUID, action TEXT NOT NULL, entity_id UUID, method TEXT, path TEXT, status_code INTEGER, detail JSONB NOT NULL DEFAULT '{}', source_ip TEXT, correlation_id TEXT, PRIMARY KEY (id, ts) ); SELECT create_hypertable('audit_log', 'ts', chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE); CREATE INDEX idx_audit_actor ON audit_log(actor_type, actor_id, ts DESC); CREATE INDEX idx_audit_entity ON audit_log(entity_id, ts DESC); CREATE INDEX idx_audit_action ON audit_log(action, ts DESC); CREATE INDEX idx_audit_correlation ON audit_log(correlation_id); DO $$ BEGIN PERFORM add_retention_policy('audit_log', INTERVAL '365 days'); EXCEPTION WHEN OTHERS THEN NULL; END $$; -- ─── Event log ──────────────────────────────────────────────────────── CREATE TABLE events ( id BIGINT GENERATED ALWAYS AS IDENTITY, ts TIMESTAMPTZ NOT NULL DEFAULT now(), type TEXT NOT NULL, entity_id UUID, severity TEXT NOT NULL DEFAULT 'info', source TEXT NOT NULL, data JSONB NOT NULL DEFAULT '{}', correlation_id TEXT, PRIMARY KEY (id, ts) ); SELECT create_hypertable('events', 'ts', chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE); CREATE INDEX idx_events_type_ts ON events(type, ts DESC); CREATE INDEX idx_events_entity_ts ON events(entity_id, ts DESC); CREATE INDEX idx_events_severity_ts ON events(severity, ts DESC); CREATE INDEX idx_events_correlation ON events(correlation_id); DO $$ BEGIN PERFORM add_retention_policy('events', INTERVAL '90 days'); EXCEPTION WHEN OTHERS THEN NULL; END $$; -- ─── Agent activity ────────────────────────────────────────────────── CREATE TABLE agent_activity ( id BIGINT GENERATED ALWAYS AS IDENTITY, ts TIMESTAMPTZ NOT NULL DEFAULT now(), agent_id UUID NOT NULL, session_id TEXT, activity_type TEXT NOT NULL, tool_name TEXT, entity_id UUID, input_summary TEXT, output_summary TEXT, duration_ms INTEGER, token_count INTEGER, success BOOLEAN, correlation_id TEXT, PRIMARY KEY (id, ts) ); SELECT create_hypertable('agent_activity', 'ts', chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE); CREATE INDEX idx_agent_activity_agent_ts ON agent_activity(agent_id, ts DESC); CREATE INDEX idx_agent_activity_type_ts ON agent_activity(activity_type, ts DESC); CREATE INDEX idx_agent_activity_entity ON agent_activity(entity_id, ts DESC); CREATE INDEX idx_agent_activity_correlation ON agent_activity(correlation_id); DO $$ BEGIN PERFORM add_retention_policy('agent_activity', INTERVAL '90 days'); EXCEPTION WHEN OTHERS THEN NULL; END $$; -- ─── Ledger view (R3-11: not a fourth write path) ──────────────────── CREATE VIEW ledger AS SELECT e.created_at AS ts, e.entity_id AS execution_id, e.target_entity_id, e.action, e.risk_class, e.status, e.verified, c.route, c.reasoning, a.status AS approval_status, a.decided_by, e.agent_id, e.correlation_id FROM executions e LEFT JOIN classifications c ON c.entity_id = e.classification_id LEFT JOIN approvals a ON a.entity_id = e.approval_id;