Files
Max Mayfield ccc4cd1c32 Scaffold dd0c/alert: ingestion, correlation engine, HMAC validation, tests
- Webhook ingestion: HMAC validation for Datadog/PagerDuty/OpsGenie with 5-min timestamp freshness
- Payload normalizers: canonical alert schema with severity mapping per provider
- Correlation engine: time-window grouping, late-alert attachment (2x window), FakeClock for testing
- InMemoryWindowStore for unit tests
- Tests: 12 HMAC validation cases, 5 normalizer cases, 7 correlation engine cases
- PostgreSQL schema with RLS: tenants, incidents, alerts, webhook_secrets, notification_configs
- Free tier enforcement columns (alert_count_month, reset_at)
- Fly.io config, Dockerfile, Gitea Actions CI
2026-03-01 02:49:14 +00:00

91 lines
3.5 KiB
SQL

-- dd0c/alert V1 schema — DynamoDB-style in PostgreSQL (Neon)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Tenants
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
tier TEXT NOT NULL DEFAULT 'free' CHECK (tier IN ('free', 'pro')),
alert_count_month INT NOT NULL DEFAULT 0,
alert_count_reset_at TIMESTAMPTZ NOT NULL DEFAULT date_trunc('month', now()) + interval '1 month',
stripe_customer_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Webhook secrets per provider per tenant
CREATE TABLE webhook_secrets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
provider TEXT NOT NULL CHECK (provider IN ('datadog', 'pagerduty', 'opsgenie', 'grafana', 'custom')),
secret TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, provider)
);
-- Incidents (correlation output)
CREATE TABLE incidents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
incident_key TEXT NOT NULL,
fingerprint TEXT NOT NULL,
service TEXT,
title TEXT NOT NULL,
severity TEXT NOT NULL CHECK (severity IN ('critical', 'high', 'medium', 'low', 'info')),
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'acknowledged', 'resolved', 'suppressed')),
alert_count INT NOT NULL DEFAULT 1,
first_alert_at TIMESTAMPTZ NOT NULL,
last_alert_at TIMESTAMPTZ NOT NULL,
resolved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_incidents_tenant ON incidents(tenant_id, status, created_at DESC);
CREATE INDEX idx_incidents_fingerprint ON incidents(tenant_id, fingerprint);
-- Alerts (raw, linked to incidents)
CREATE TABLE alerts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
incident_id UUID REFERENCES incidents(id) ON DELETE SET NULL,
source_provider TEXT NOT NULL,
source_id TEXT NOT NULL,
fingerprint TEXT NOT NULL,
title TEXT NOT NULL,
severity TEXT NOT NULL,
status TEXT NOT NULL,
service TEXT,
environment TEXT,
tags JSONB NOT NULL DEFAULT '{}',
raw_payload JSONB NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_alerts_tenant ON alerts(tenant_id, received_at DESC);
CREATE INDEX idx_alerts_incident ON alerts(incident_id);
-- Notification configs
CREATE TABLE notification_configs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
channel TEXT NOT NULL CHECK (channel IN ('slack', 'email', 'webhook')),
config JSONB NOT NULL DEFAULT '{}',
min_severity TEXT NOT NULL DEFAULT 'medium',
enabled BOOLEAN NOT NULL DEFAULT true,
UNIQUE(tenant_id, channel)
);
-- Free tier enforcement: 10K alerts/month, 7-day retention
-- Pro tier: unlimited alerts, 90-day retention
-- RLS
ALTER TABLE incidents ENABLE ROW LEVEL SECURITY;
ALTER TABLE alerts ENABLE ROW LEVEL SECURITY;
ALTER TABLE notification_configs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_iso_incidents ON incidents
USING (tenant_id::text = current_setting('app.tenant_id', true));
CREATE POLICY tenant_iso_alerts ON alerts
USING (tenant_id::text = current_setting('app.tenant_id', true));
CREATE POLICY tenant_iso_notif ON notification_configs
USING (tenant_id::text = current_setting('app.tenant_id', true));