Core deliverables: - Go module github.com/dtoro/oikos (Go 1.26.3) - cmd/oikos: single binary with role subcommands (migrate, seed, export) - 6 SQL migrations: ontology meta-schema, entity instances (UUID+slug, blast_radius recursive function), operations (signals/checks/approvals), cognition (classifications/executions/feedback/patterns/skills), policy, observability (TimescaleDB hypertables + CAGGs + retention) - Domain layer: entity, signal, execution, classification, pattern, skill, approval, check types + 11 sentinel errors + lifecycle state machines - DB layer: pgx pool, SQL splitter (handles 94436 and -- comments), migration runner, seed ingest (ontology+inventory+policy) with content-hash dedup - Config: env-based with defaults, secrets redaction - Observability: slog JSON logger with debug mode - Infrastructure: Makefile, docker-compose.yml, multi-stage Dockerfile (distroless, CGO_ENABLED=0) Verified end-to-end against timescale/timescaledb:2.17.2-pg16: - 6 migrations applied (65 SQL statements) - Seeds ingested: 6 lifecycles, 59 entity types, 46 relationship types, 111 entities, 144 relationships, 4 risk classes, 27 approval rules, 9 autonomy settings - Idempotent: second seed run is a no-op (content hash matches) Bugs fixed during implementation: - TimescaleDB CAGGs can't run in a transaction -> splitSQL() executes statements individually - Semicolons in -- comments treated as separators -> comment handling - YAML keys source/target didn't match code's source_type/target_type - yaml.Marshal produced YAML for JSONB columns -> json.Marshal
139 lines
5.9 KiB
SQL
139 lines
5.9 KiB
SQL
-- 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;
|