20260731_410_data_observability.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. """Add canonical SLO policies, alerts and evidence-bound data incidents."""
  2. from alembic import op
  3. revision = "20260731_410"
  4. down_revision = "20260731_400"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. """
  10. CREATE TABLE public.data_slo_policies (
  11. uid UUID PRIMARY KEY,
  12. code VARCHAR(120) NOT NULL UNIQUE,
  13. name VARCHAR(300) NOT NULL,
  14. sli_type VARCHAR(30) NOT NULL CHECK (
  15. sli_type IN ('freshness','completeness','quality','delivery')
  16. ),
  17. scope_type VARCHAR(40) NOT NULL,
  18. scope_uid VARCHAR(500) NOT NULL,
  19. operator VARCHAR(2) NOT NULL CHECK (operator IN ('>=','<=')),
  20. target NUMERIC NOT NULL,
  21. window_seconds INTEGER NOT NULL CHECK (
  22. window_seconds BETWEEN 60 AND 31536000
  23. ),
  24. owner_uid UUID NOT NULL REFERENCES public.users(id),
  25. status VARCHAR(20) NOT NULL CHECK (
  26. status IN ('active','retired')
  27. ),
  28. created_by UUID NOT NULL REFERENCES public.users(id),
  29. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  30. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  31. UNIQUE (sli_type, scope_type, scope_uid)
  32. );
  33. CREATE TABLE public.data_incidents (
  34. uid UUID PRIMARY KEY,
  35. code VARCHAR(40) NOT NULL UNIQUE,
  36. dedup_key CHAR(64) NOT NULL,
  37. title VARCHAR(300) NOT NULL,
  38. severity VARCHAR(20) NOT NULL CHECK (
  39. severity IN ('info','warning','error','critical')
  40. ),
  41. status VARCHAR(20) NOT NULL CHECK (
  42. status IN ('open','monitoring','closed')
  43. ),
  44. owner_uid UUID NOT NULL REFERENCES public.users(id),
  45. escalation_level INTEGER NOT NULL CHECK (
  46. escalation_level BETWEEN 1 AND 3
  47. ),
  48. first_detected_at TIMESTAMPTZ NOT NULL,
  49. last_observed_at TIMESTAMPTZ NOT NULL,
  50. created_by UUID NOT NULL REFERENCES public.users(id),
  51. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  52. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  53. closed_by UUID REFERENCES public.users(id),
  54. closed_at TIMESTAMPTZ
  55. );
  56. CREATE UNIQUE INDEX uq_data_incident_open_dedup
  57. ON public.data_incidents(dedup_key)
  58. WHERE status <> 'closed';
  59. CREATE TABLE public.data_observability_alerts (
  60. uid UUID PRIMARY KEY,
  61. dedup_key CHAR(64) NOT NULL,
  62. incident_uid UUID NOT NULL REFERENCES public.data_incidents(uid),
  63. layer VARCHAR(20) NOT NULL CHECK (
  64. layer IN ('service','task','data','capacity')
  65. ),
  66. sli_type VARCHAR(30) NOT NULL CHECK (
  67. sli_type IN ('freshness','completeness','quality','delivery')
  68. ),
  69. title VARCHAR(300) NOT NULL,
  70. severity VARCHAR(20) NOT NULL CHECK (
  71. severity IN ('info','warning','error','critical')
  72. ),
  73. status VARCHAR(20) NOT NULL CHECK (
  74. status IN ('open','suppressed','recovered')
  75. ),
  76. occurrence_count INTEGER NOT NULL CHECK (occurrence_count > 0),
  77. escalation_level INTEGER NOT NULL CHECK (
  78. escalation_level BETWEEN 1 AND 3
  79. ),
  80. owner_uid UUID NOT NULL REFERENCES public.users(id),
  81. first_observed_at TIMESTAMPTZ NOT NULL,
  82. last_observed_at TIMESTAMPTZ NOT NULL,
  83. suppressed_until TIMESTAMPTZ,
  84. suppression_reason VARCHAR(500),
  85. delivery_status VARCHAR(30) NOT NULL CHECK (
  86. delivery_status IN (
  87. 'pending','suppressed','acknowledged','recovery_pending'
  88. )
  89. ),
  90. delivery_receipt JSONB NOT NULL DEFAULT '{}'::jsonb,
  91. evidence JSONB NOT NULL,
  92. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  93. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  94. CHECK (jsonb_typeof(delivery_receipt) = 'object'),
  95. CHECK (jsonb_typeof(evidence) = 'object')
  96. );
  97. CREATE UNIQUE INDEX uq_data_alert_open_dedup
  98. ON public.data_observability_alerts(dedup_key)
  99. WHERE status <> 'recovered';
  100. CREATE TABLE public.data_observability_source_events (
  101. uid UUID PRIMARY KEY,
  102. source_event_key VARCHAR(500) NOT NULL UNIQUE,
  103. source_type VARCHAR(80) NOT NULL,
  104. source_uid VARCHAR(500) NOT NULL,
  105. status VARCHAR(20) NOT NULL CHECK (
  106. status IN ('violated','recovered')
  107. ),
  108. signal JSONB NOT NULL,
  109. processed_at TIMESTAMPTZ NOT NULL,
  110. CHECK (jsonb_typeof(signal) = 'object')
  111. );
  112. CREATE TABLE public.data_incident_impacts (
  113. uid BIGSERIAL PRIMARY KEY,
  114. incident_uid UUID NOT NULL
  115. REFERENCES public.data_incidents(uid) ON DELETE CASCADE,
  116. target_type VARCHAR(40) NOT NULL CHECK (
  117. target_type IN (
  118. 'asset','data_product','business_domain',
  119. 'user_group','service'
  120. )
  121. ),
  122. target_uid VARCHAR(500) NOT NULL,
  123. label VARCHAR(300) NOT NULL,
  124. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  125. UNIQUE (incident_uid, target_type, target_uid)
  126. );
  127. CREATE TABLE public.data_incident_timeline (
  128. uid UUID PRIMARY KEY,
  129. incident_uid UUID NOT NULL
  130. REFERENCES public.data_incidents(uid) ON DELETE CASCADE,
  131. action VARCHAR(40) NOT NULL,
  132. actor_uid UUID NOT NULL REFERENCES public.users(id),
  133. evidence JSONB NOT NULL,
  134. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  135. CHECK (jsonb_typeof(evidence) = 'object')
  136. );
  137. CREATE TABLE public.data_incident_postmortems (
  138. uid UUID PRIMARY KEY,
  139. incident_uid UUID NOT NULL UNIQUE
  140. REFERENCES public.data_incidents(uid) ON DELETE CASCADE,
  141. root_cause TEXT NOT NULL,
  142. user_impact TEXT NOT NULL,
  143. corrective_actions JSONB NOT NULL,
  144. closure_evidence JSONB NOT NULL,
  145. created_by UUID NOT NULL REFERENCES public.users(id),
  146. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  147. CHECK (jsonb_typeof(corrective_actions) = 'array'),
  148. CHECK (jsonb_array_length(corrective_actions) > 0),
  149. CHECK (jsonb_typeof(closure_evidence) = 'array'),
  150. CHECK (jsonb_array_length(closure_evidence) > 0)
  151. );
  152. CREATE INDEX idx_data_alert_layer_status
  153. ON public.data_observability_alerts(
  154. layer, status, severity, last_observed_at DESC
  155. );
  156. CREATE INDEX idx_data_incident_status_updated
  157. ON public.data_incidents(status, updated_at DESC);
  158. CREATE INDEX idx_data_incident_timeline_created
  159. ON public.data_incident_timeline(
  160. incident_uid, created_at, uid
  161. );
  162. """
  163. )
  164. def downgrade() -> None:
  165. raise RuntimeError(
  166. "data incident evidence is append-only; "
  167. "downgrade requires an approved archival migration"
  168. )