Files
oikos/migrations/006_observability.up.sql
dtoro aa2ca0ae6f phase 1: Go foundation — module, migrations, domain, seed ingest
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
2026-07-07 01:07:26 +02:00

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;