033_ops_monitoring_vnext.sql 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. -- Ops Monitoring (vNext): squashed migration (030)
  2. --
  3. -- This repository originally planned Ops vNext as migrations 030-036:
  4. -- 030 drop legacy ops tables
  5. -- 031 core schema
  6. -- 032 pre-aggregation tables
  7. -- 033 indexes + optional extensions
  8. -- 034 add avg/max to preagg
  9. -- 035 add notify_email to alert rules
  10. -- 036 seed default alert rules
  11. --
  12. -- Since these migrations have NOT been applied to any environment yet, we squash them
  13. -- into a single 030 migration for easier review and a cleaner migration history.
  14. --
  15. -- Notes:
  16. -- - This is intentionally destructive for ops_* data (error logs / metrics / alerts).
  17. -- - It is idempotent (DROP/CREATE/ALTER IF EXISTS/IF NOT EXISTS), but will wipe ops_* data if re-run.
  18. -- =====================================================================
  19. -- 030_ops_drop_legacy_ops_tables.sql
  20. -- =====================================================================
  21. SET LOCAL lock_timeout = '5s';
  22. SET LOCAL statement_timeout = '10min';
  23. -- Legacy pre-aggregation tables (from 026 and/or previous branches)
  24. DROP TABLE IF EXISTS ops_metrics_daily CASCADE;
  25. DROP TABLE IF EXISTS ops_metrics_hourly CASCADE;
  26. -- Core ops tables that may exist in some deployments / branches
  27. DROP TABLE IF EXISTS ops_system_metrics CASCADE;
  28. DROP TABLE IF EXISTS ops_error_logs CASCADE;
  29. DROP TABLE IF EXISTS ops_alert_events CASCADE;
  30. DROP TABLE IF EXISTS ops_alert_rules CASCADE;
  31. DROP TABLE IF EXISTS ops_job_heartbeats CASCADE;
  32. DROP TABLE IF EXISTS ops_retry_attempts CASCADE;
  33. -- Optional legacy tables (best-effort cleanup)
  34. DROP TABLE IF EXISTS ops_scheduled_reports CASCADE;
  35. DROP TABLE IF EXISTS ops_group_availability_configs CASCADE;
  36. DROP TABLE IF EXISTS ops_group_availability_events CASCADE;
  37. -- Optional legacy views/indexes
  38. DROP VIEW IF EXISTS ops_latest_metrics CASCADE;
  39. -- =====================================================================
  40. -- 031_ops_core_schema.sql
  41. -- =====================================================================
  42. -- Ops Monitoring (vNext): core schema (errors / retries / metrics / jobs / alerts)
  43. --
  44. -- Design goals:
  45. -- - Support global filtering (time/platform/group) across all ops modules.
  46. -- - Persist enough context for two retry modes (client retry / pinned upstream retry).
  47. -- - Make ops background jobs observable via job heartbeats.
  48. -- - Keep schema stable and indexes targeted (high-write tables).
  49. --
  50. -- Notes:
  51. -- - This migration is idempotent.
  52. -- - ops_* tables intentionally avoid strict foreign keys to reduce write amplification/locks.
  53. SET LOCAL lock_timeout = '5s';
  54. SET LOCAL statement_timeout = '10min';
  55. -- ============================================
  56. -- 1) ops_error_logs: error log details (high-write)
  57. -- ============================================
  58. CREATE TABLE IF NOT EXISTS ops_error_logs (
  59. id BIGSERIAL PRIMARY KEY,
  60. -- Correlation / identities
  61. request_id VARCHAR(64),
  62. client_request_id VARCHAR(64),
  63. user_id BIGINT,
  64. api_key_id BIGINT,
  65. account_id BIGINT,
  66. group_id BIGINT,
  67. client_ip inet,
  68. -- Dimensions for global filtering
  69. platform VARCHAR(32),
  70. -- Request metadata
  71. model VARCHAR(100),
  72. request_path VARCHAR(256),
  73. stream BOOLEAN NOT NULL DEFAULT false,
  74. user_agent TEXT,
  75. -- Core error classification
  76. error_phase VARCHAR(32) NOT NULL,
  77. error_type VARCHAR(64) NOT NULL,
  78. severity VARCHAR(8) NOT NULL DEFAULT 'P2',
  79. status_code INT,
  80. -- vNext metric semantics
  81. is_business_limited BOOLEAN NOT NULL DEFAULT false,
  82. -- Error details (sanitized/truncated at ingest time)
  83. error_message TEXT,
  84. error_body TEXT,
  85. -- Provider/upstream details (optional; useful for trends & account health)
  86. error_source VARCHAR(64),
  87. error_owner VARCHAR(32),
  88. account_status VARCHAR(50),
  89. upstream_status_code INT,
  90. upstream_error_message TEXT,
  91. upstream_error_detail TEXT,
  92. provider_error_code VARCHAR(64),
  93. provider_error_type VARCHAR(64),
  94. network_error_type VARCHAR(50),
  95. retry_after_seconds INT,
  96. -- Timings (ms) - optional
  97. duration_ms INT,
  98. time_to_first_token_ms BIGINT,
  99. auth_latency_ms BIGINT,
  100. routing_latency_ms BIGINT,
  101. upstream_latency_ms BIGINT,
  102. response_latency_ms BIGINT,
  103. -- Retry context (only stored for error requests)
  104. request_body JSONB,
  105. request_headers JSONB,
  106. request_body_truncated BOOLEAN NOT NULL DEFAULT false,
  107. request_body_bytes INT,
  108. -- Retryability flags (best-effort classification)
  109. is_retryable BOOLEAN NOT NULL DEFAULT false,
  110. retry_count INT NOT NULL DEFAULT 0,
  111. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  112. );
  113. COMMENT ON TABLE ops_error_logs IS 'Ops error logs (vNext). Stores sanitized error details and request_body for retries (errors only).';
  114. -- ============================================
  115. -- 2) ops_retry_attempts: audit log for retries
  116. -- ============================================
  117. CREATE TABLE IF NOT EXISTS ops_retry_attempts (
  118. id BIGSERIAL PRIMARY KEY,
  119. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  120. requested_by_user_id BIGINT,
  121. source_error_id BIGINT,
  122. -- client|upstream
  123. mode VARCHAR(16) NOT NULL,
  124. pinned_account_id BIGINT,
  125. -- queued|running|succeeded|failed
  126. status VARCHAR(16) NOT NULL DEFAULT 'queued',
  127. started_at TIMESTAMPTZ,
  128. finished_at TIMESTAMPTZ,
  129. duration_ms BIGINT,
  130. -- Optional result correlation
  131. result_request_id VARCHAR(64),
  132. result_error_id BIGINT,
  133. result_usage_request_id VARCHAR(64),
  134. error_message TEXT
  135. );
  136. COMMENT ON TABLE ops_retry_attempts IS 'Audit table for ops retries (client retry / pinned upstream retry).';
  137. -- ============================================
  138. -- 3) ops_system_metrics: system + request window snapshots
  139. -- ============================================
  140. CREATE TABLE IF NOT EXISTS ops_system_metrics (
  141. id BIGSERIAL PRIMARY KEY,
  142. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  143. window_minutes INT NOT NULL DEFAULT 1,
  144. -- Optional dimensions (only if collector chooses to write per-dimension snapshots)
  145. platform VARCHAR(32),
  146. group_id BIGINT,
  147. -- Core counts
  148. success_count BIGINT NOT NULL DEFAULT 0,
  149. error_count_total BIGINT NOT NULL DEFAULT 0,
  150. business_limited_count BIGINT NOT NULL DEFAULT 0,
  151. error_count_sla BIGINT NOT NULL DEFAULT 0,
  152. upstream_error_count_excl_429_529 BIGINT NOT NULL DEFAULT 0,
  153. upstream_429_count BIGINT NOT NULL DEFAULT 0,
  154. upstream_529_count BIGINT NOT NULL DEFAULT 0,
  155. token_consumed BIGINT NOT NULL DEFAULT 0,
  156. -- Rates
  157. qps DOUBLE PRECISION,
  158. tps DOUBLE PRECISION,
  159. -- Duration percentiles (ms) - success requests
  160. duration_p50_ms INT,
  161. duration_p90_ms INT,
  162. duration_p95_ms INT,
  163. duration_p99_ms INT,
  164. duration_avg_ms DOUBLE PRECISION,
  165. duration_max_ms INT,
  166. -- TTFT percentiles (ms) - success requests (streaming)
  167. ttft_p50_ms INT,
  168. ttft_p90_ms INT,
  169. ttft_p95_ms INT,
  170. ttft_p99_ms INT,
  171. ttft_avg_ms DOUBLE PRECISION,
  172. ttft_max_ms INT,
  173. -- System resources
  174. cpu_usage_percent DOUBLE PRECISION,
  175. memory_used_mb BIGINT,
  176. memory_total_mb BIGINT,
  177. memory_usage_percent DOUBLE PRECISION,
  178. -- Dependency health (best-effort)
  179. db_ok BOOLEAN,
  180. redis_ok BOOLEAN,
  181. -- DB pool & runtime
  182. db_conn_active INT,
  183. db_conn_idle INT,
  184. db_conn_waiting INT,
  185. goroutine_count INT,
  186. -- Queue / concurrency
  187. concurrency_queue_depth INT
  188. );
  189. COMMENT ON TABLE ops_system_metrics IS 'Ops system/request metrics snapshots (vNext). Used for dashboard overview and realtime rates.';
  190. -- ============================================
  191. -- 4) ops_job_heartbeats: background jobs health
  192. -- ============================================
  193. CREATE TABLE IF NOT EXISTS ops_job_heartbeats (
  194. job_name VARCHAR(64) PRIMARY KEY,
  195. last_run_at TIMESTAMPTZ,
  196. last_success_at TIMESTAMPTZ,
  197. last_error_at TIMESTAMPTZ,
  198. last_error TEXT,
  199. last_duration_ms BIGINT,
  200. updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  201. );
  202. COMMENT ON TABLE ops_job_heartbeats IS 'Ops background jobs heartbeats (vNext).';
  203. -- ============================================
  204. -- 5) ops_alert_rules / ops_alert_events
  205. -- ============================================
  206. CREATE TABLE IF NOT EXISTS ops_alert_rules (
  207. id BIGSERIAL PRIMARY KEY,
  208. name VARCHAR(128) NOT NULL,
  209. description TEXT,
  210. enabled BOOLEAN NOT NULL DEFAULT true,
  211. severity VARCHAR(16) NOT NULL DEFAULT 'warning',
  212. -- Metric definition
  213. -- Metric definition
  214. metric_type VARCHAR(64) NOT NULL,
  215. operator VARCHAR(8) NOT NULL,
  216. threshold DOUBLE PRECISION NOT NULL,
  217. window_minutes INT NOT NULL DEFAULT 5,
  218. sustained_minutes INT NOT NULL DEFAULT 5,
  219. cooldown_minutes INT NOT NULL DEFAULT 10,
  220. -- Optional scoping: platform/group filters etc.
  221. filters JSONB,
  222. last_triggered_at TIMESTAMPTZ,
  223. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  224. updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  225. );
  226. CREATE UNIQUE INDEX IF NOT EXISTS idx_ops_alert_rules_name_unique
  227. ON ops_alert_rules (name);
  228. CREATE INDEX IF NOT EXISTS idx_ops_alert_rules_enabled
  229. ON ops_alert_rules (enabled);
  230. CREATE TABLE IF NOT EXISTS ops_alert_events (
  231. id BIGSERIAL PRIMARY KEY,
  232. rule_id BIGINT,
  233. severity VARCHAR(16) NOT NULL,
  234. status VARCHAR(16) NOT NULL DEFAULT 'firing',
  235. title VARCHAR(200),
  236. description TEXT,
  237. metric_value DOUBLE PRECISION,
  238. threshold_value DOUBLE PRECISION,
  239. dimensions JSONB,
  240. fired_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  241. resolved_at TIMESTAMPTZ,
  242. email_sent BOOLEAN NOT NULL DEFAULT false,
  243. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  244. );
  245. CREATE INDEX IF NOT EXISTS idx_ops_alert_events_rule_status
  246. ON ops_alert_events (rule_id, status);
  247. CREATE INDEX IF NOT EXISTS idx_ops_alert_events_fired_at
  248. ON ops_alert_events (fired_at DESC);
  249. -- =====================================================================
  250. -- 032_ops_preaggregation_tables.sql
  251. -- =====================================================================
  252. -- Ops Monitoring (vNext): pre-aggregation tables
  253. --
  254. -- Purpose:
  255. -- - Provide stable query performance for 1–24h windows (and beyond), avoiding expensive
  256. -- percentile_cont scans on raw logs for every dashboard refresh.
  257. -- - Support global filter dimensions: overall / platform / group.
  258. --
  259. -- Design note:
  260. -- - We keep a single table with nullable platform/group_id, and enforce uniqueness via a
  261. -- COALESCE-based unique index (because UNIQUE with NULLs allows duplicates in Postgres).
  262. SET LOCAL lock_timeout = '5s';
  263. SET LOCAL statement_timeout = '10min';
  264. -- ============================================
  265. -- 1) ops_metrics_hourly
  266. -- ============================================
  267. CREATE TABLE IF NOT EXISTS ops_metrics_hourly (
  268. id BIGSERIAL PRIMARY KEY,
  269. bucket_start TIMESTAMPTZ NOT NULL,
  270. platform VARCHAR(32),
  271. group_id BIGINT,
  272. success_count BIGINT NOT NULL DEFAULT 0,
  273. error_count_total BIGINT NOT NULL DEFAULT 0,
  274. business_limited_count BIGINT NOT NULL DEFAULT 0,
  275. error_count_sla BIGINT NOT NULL DEFAULT 0,
  276. upstream_error_count_excl_429_529 BIGINT NOT NULL DEFAULT 0,
  277. upstream_429_count BIGINT NOT NULL DEFAULT 0,
  278. upstream_529_count BIGINT NOT NULL DEFAULT 0,
  279. token_consumed BIGINT NOT NULL DEFAULT 0,
  280. -- Duration percentiles (ms)
  281. duration_p50_ms INT,
  282. duration_p90_ms INT,
  283. duration_p95_ms INT,
  284. duration_p99_ms INT,
  285. -- TTFT percentiles (ms)
  286. ttft_p50_ms INT,
  287. ttft_p90_ms INT,
  288. ttft_p95_ms INT,
  289. ttft_p99_ms INT,
  290. computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  291. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  292. );
  293. -- Uniqueness across three “dimension modes” (overall / platform / group).
  294. -- Postgres UNIQUE treats NULLs as distinct, so we enforce uniqueness via COALESCE.
  295. CREATE UNIQUE INDEX IF NOT EXISTS idx_ops_metrics_hourly_unique_dim
  296. ON ops_metrics_hourly (
  297. bucket_start,
  298. COALESCE(platform, ''),
  299. COALESCE(group_id, 0)
  300. );
  301. CREATE INDEX IF NOT EXISTS idx_ops_metrics_hourly_bucket
  302. ON ops_metrics_hourly (bucket_start DESC);
  303. CREATE INDEX IF NOT EXISTS idx_ops_metrics_hourly_platform_bucket
  304. ON ops_metrics_hourly (platform, bucket_start DESC)
  305. WHERE platform IS NOT NULL AND platform <> '' AND group_id IS NULL;
  306. CREATE INDEX IF NOT EXISTS idx_ops_metrics_hourly_group_bucket
  307. ON ops_metrics_hourly (group_id, bucket_start DESC)
  308. WHERE group_id IS NOT NULL AND group_id <> 0;
  309. COMMENT ON TABLE ops_metrics_hourly IS 'vNext hourly pre-aggregated ops metrics (overall/platform/group).';
  310. -- ============================================
  311. -- 2) ops_metrics_daily (optional; for longer windows)
  312. -- ============================================
  313. CREATE TABLE IF NOT EXISTS ops_metrics_daily (
  314. id BIGSERIAL PRIMARY KEY,
  315. bucket_date DATE NOT NULL,
  316. platform VARCHAR(32),
  317. group_id BIGINT,
  318. success_count BIGINT NOT NULL DEFAULT 0,
  319. error_count_total BIGINT NOT NULL DEFAULT 0,
  320. business_limited_count BIGINT NOT NULL DEFAULT 0,
  321. error_count_sla BIGINT NOT NULL DEFAULT 0,
  322. upstream_error_count_excl_429_529 BIGINT NOT NULL DEFAULT 0,
  323. upstream_429_count BIGINT NOT NULL DEFAULT 0,
  324. upstream_529_count BIGINT NOT NULL DEFAULT 0,
  325. token_consumed BIGINT NOT NULL DEFAULT 0,
  326. duration_p50_ms INT,
  327. duration_p90_ms INT,
  328. duration_p95_ms INT,
  329. duration_p99_ms INT,
  330. ttft_p50_ms INT,
  331. ttft_p90_ms INT,
  332. ttft_p95_ms INT,
  333. ttft_p99_ms INT,
  334. computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  335. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  336. );
  337. CREATE UNIQUE INDEX IF NOT EXISTS idx_ops_metrics_daily_unique_dim
  338. ON ops_metrics_daily (
  339. bucket_date,
  340. COALESCE(platform, ''),
  341. COALESCE(group_id, 0)
  342. );
  343. CREATE INDEX IF NOT EXISTS idx_ops_metrics_daily_bucket
  344. ON ops_metrics_daily (bucket_date DESC);
  345. CREATE INDEX IF NOT EXISTS idx_ops_metrics_daily_platform_bucket
  346. ON ops_metrics_daily (platform, bucket_date DESC)
  347. WHERE platform IS NOT NULL AND platform <> '' AND group_id IS NULL;
  348. CREATE INDEX IF NOT EXISTS idx_ops_metrics_daily_group_bucket
  349. ON ops_metrics_daily (group_id, bucket_date DESC)
  350. WHERE group_id IS NOT NULL AND group_id <> 0;
  351. COMMENT ON TABLE ops_metrics_daily IS 'vNext daily pre-aggregated ops metrics (overall/platform/group).';
  352. -- =====================================================================
  353. -- 033_ops_indexes_and_extensions.sql
  354. -- =====================================================================
  355. -- Ops Monitoring (vNext): indexes and optional extensions
  356. --
  357. -- This migration intentionally keeps "optional" objects (like pg_trgm) best-effort,
  358. -- so environments without extension privileges won't fail the whole migration chain.
  359. SET LOCAL lock_timeout = '5s';
  360. SET LOCAL statement_timeout = '10min';
  361. -- ============================================
  362. -- 1) Core btree indexes (always safe)
  363. -- ============================================
  364. -- ops_error_logs
  365. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_created_at
  366. ON ops_error_logs (created_at DESC);
  367. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_platform_time
  368. ON ops_error_logs (platform, created_at DESC);
  369. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_group_time
  370. ON ops_error_logs (group_id, created_at DESC)
  371. WHERE group_id IS NOT NULL;
  372. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_account_time
  373. ON ops_error_logs (account_id, created_at DESC)
  374. WHERE account_id IS NOT NULL;
  375. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_status_time
  376. ON ops_error_logs (status_code, created_at DESC);
  377. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_phase_time
  378. ON ops_error_logs (error_phase, created_at DESC);
  379. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_type_time
  380. ON ops_error_logs (error_type, created_at DESC);
  381. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_request_id
  382. ON ops_error_logs (request_id);
  383. CREATE INDEX IF NOT EXISTS idx_ops_error_logs_client_request_id
  384. ON ops_error_logs (client_request_id);
  385. -- ops_system_metrics
  386. CREATE INDEX IF NOT EXISTS idx_ops_system_metrics_created_at
  387. ON ops_system_metrics (created_at DESC);
  388. CREATE INDEX IF NOT EXISTS idx_ops_system_metrics_window_time
  389. ON ops_system_metrics (window_minutes, created_at DESC);
  390. CREATE INDEX IF NOT EXISTS idx_ops_system_metrics_platform_time
  391. ON ops_system_metrics (platform, created_at DESC)
  392. WHERE platform IS NOT NULL AND platform <> '' AND group_id IS NULL;
  393. CREATE INDEX IF NOT EXISTS idx_ops_system_metrics_group_time
  394. ON ops_system_metrics (group_id, created_at DESC)
  395. WHERE group_id IS NOT NULL;
  396. -- ops_retry_attempts
  397. CREATE INDEX IF NOT EXISTS idx_ops_retry_attempts_created_at
  398. ON ops_retry_attempts (created_at DESC);
  399. CREATE INDEX IF NOT EXISTS idx_ops_retry_attempts_source_error
  400. ON ops_retry_attempts (source_error_id, created_at DESC)
  401. WHERE source_error_id IS NOT NULL;
  402. -- Prevent concurrent retries for the same ops_error_logs row (race-free, multi-instance safe).
  403. CREATE UNIQUE INDEX IF NOT EXISTS idx_ops_retry_attempts_unique_active
  404. ON ops_retry_attempts (source_error_id)
  405. WHERE source_error_id IS NOT NULL AND status IN ('queued', 'running');
  406. -- ============================================
  407. -- 2) Optional: pg_trgm + trigram indexes for fuzzy search
  408. -- ============================================
  409. DO $$
  410. BEGIN
  411. BEGIN
  412. CREATE EXTENSION IF NOT EXISTS pg_trgm;
  413. EXCEPTION WHEN OTHERS THEN
  414. -- Missing privileges or extension package should not block migrations.
  415. RAISE NOTICE 'pg_trgm extension not created: %', SQLERRM;
  416. END;
  417. IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_trgm') THEN
  418. -- request_id / client_request_id fuzzy search
  419. EXECUTE 'CREATE INDEX IF NOT EXISTS idx_ops_error_logs_request_id_trgm
  420. ON ops_error_logs USING gin (request_id gin_trgm_ops)';
  421. EXECUTE 'CREATE INDEX IF NOT EXISTS idx_ops_error_logs_client_request_id_trgm
  422. ON ops_error_logs USING gin (client_request_id gin_trgm_ops)';
  423. -- error_message fuzzy search
  424. EXECUTE 'CREATE INDEX IF NOT EXISTS idx_ops_error_logs_error_message_trgm
  425. ON ops_error_logs USING gin (error_message gin_trgm_ops)';
  426. END IF;
  427. END $$;
  428. -- =====================================================================
  429. -- 034_ops_preaggregation_add_avg_max.sql
  430. -- =====================================================================
  431. -- Ops Monitoring (vNext): extend pre-aggregation tables with avg/max latency fields
  432. --
  433. -- Why:
  434. -- - The dashboard overview returns avg/max for duration/TTFT.
  435. -- - Hourly/daily pre-aggregation tables originally stored only p50/p90/p95/p99, which makes
  436. -- it impossible to answer avg/max in preagg mode without falling back to raw scans.
  437. --
  438. -- This migration is idempotent and safe to run multiple times.
  439. --
  440. -- NOTE: We keep the existing p50/p90/p95/p99 columns as-is; these are still used for
  441. -- approximate long-window summaries.
  442. SET LOCAL lock_timeout = '5s';
  443. SET LOCAL statement_timeout = '10min';
  444. -- Hourly table
  445. ALTER TABLE ops_metrics_hourly
  446. ADD COLUMN IF NOT EXISTS duration_avg_ms DOUBLE PRECISION,
  447. ADD COLUMN IF NOT EXISTS duration_max_ms INT,
  448. ADD COLUMN IF NOT EXISTS ttft_avg_ms DOUBLE PRECISION,
  449. ADD COLUMN IF NOT EXISTS ttft_max_ms INT;
  450. -- Daily table
  451. ALTER TABLE ops_metrics_daily
  452. ADD COLUMN IF NOT EXISTS duration_avg_ms DOUBLE PRECISION,
  453. ADD COLUMN IF NOT EXISTS duration_max_ms INT,
  454. ADD COLUMN IF NOT EXISTS ttft_avg_ms DOUBLE PRECISION,
  455. ADD COLUMN IF NOT EXISTS ttft_max_ms INT;
  456. -- =====================================================================
  457. -- 035_ops_alert_rules_notify_email.sql
  458. -- =====================================================================
  459. -- Ops Monitoring (vNext): alert rule notify settings
  460. --
  461. -- Adds notify_email flag to ops_alert_rules to keep UI parity with the backup Ops dashboard.
  462. -- Migration is idempotent.
  463. SET LOCAL lock_timeout = '5s';
  464. SET LOCAL statement_timeout = '10min';
  465. ALTER TABLE ops_alert_rules
  466. ADD COLUMN IF NOT EXISTS notify_email BOOLEAN NOT NULL DEFAULT true;
  467. -- =====================================================================
  468. -- 036_ops_seed_default_alert_rules.sql
  469. -- =====================================================================
  470. -- Ops Monitoring (vNext): seed default alert rules (idempotent)
  471. --
  472. -- Goal:
  473. -- - Provide "out of the box" alert rules so the Ops dashboard can immediately show alert events.
  474. -- - Keep inserts idempotent via ON CONFLICT (name) DO NOTHING.
  475. --
  476. -- Notes:
  477. -- - Thresholds are intentionally conservative defaults and should be tuned per deployment.
  478. -- - Metric semantics follow vNext:
  479. -- - success_rate / error_rate are based on SLA-scope counts (exclude is_business_limited).
  480. -- - upstream_error_rate excludes 429/529.
  481. SET LOCAL lock_timeout = '5s';
  482. SET LOCAL statement_timeout = '10min';
  483. -- 1) High error rate (P1)
  484. INSERT INTO ops_alert_rules (
  485. name, description, enabled, metric_type, operator, threshold,
  486. window_minutes, sustained_minutes, severity, notify_email, cooldown_minutes,
  487. created_at, updated_at
  488. ) VALUES (
  489. '错误率过高',
  490. '当错误率超过 5% 且持续 5 分钟时触发告警',
  491. true, 'error_rate', '>', 5.0, 5, 5, 'P1', true, 20, NOW(), NOW()
  492. ) ON CONFLICT (name) DO NOTHING;
  493. -- 2) Low success rate (P0)
  494. INSERT INTO ops_alert_rules (
  495. name, description, enabled, metric_type, operator, threshold,
  496. window_minutes, sustained_minutes, severity, notify_email, cooldown_minutes,
  497. created_at, updated_at
  498. ) VALUES (
  499. '成功率过低',
  500. '当成功率低于 95% 且持续 5 分钟时触发告警(服务可用性下降)',
  501. true, 'success_rate', '<', 95.0, 5, 5, 'P0', true, 15, NOW(), NOW()
  502. ) ON CONFLICT (name) DO NOTHING;
  503. -- 3) P99 latency too high (P2)
  504. INSERT INTO ops_alert_rules (
  505. name, description, enabled, metric_type, operator, threshold,
  506. window_minutes, sustained_minutes, severity, notify_email, cooldown_minutes,
  507. created_at, updated_at
  508. ) VALUES (
  509. 'P99延迟过高',
  510. '当 P99 延迟超过 3000ms 且持续 10 分钟时触发告警',
  511. true, 'p99_latency_ms', '>', 3000.0, 5, 10, 'P2', true, 30, NOW(), NOW()
  512. ) ON CONFLICT (name) DO NOTHING;
  513. -- 4) P95 latency too high (P2)
  514. INSERT INTO ops_alert_rules (
  515. name, description, enabled, metric_type, operator, threshold,
  516. window_minutes, sustained_minutes, severity, notify_email, cooldown_minutes,
  517. created_at, updated_at
  518. ) VALUES (
  519. 'P95延迟过高',
  520. '当 P95 延迟超过 2000ms 且持续 10 分钟时触发告警',
  521. true, 'p95_latency_ms', '>', 2000.0, 5, 10, 'P2', true, 30, NOW(), NOW()
  522. ) ON CONFLICT (name) DO NOTHING;
  523. -- 5) CPU usage too high (P2)
  524. INSERT INTO ops_alert_rules (
  525. name, description, enabled, metric_type, operator, threshold,
  526. window_minutes, sustained_minutes, severity, notify_email, cooldown_minutes,
  527. created_at, updated_at
  528. ) VALUES (
  529. 'CPU使用率过高',
  530. '当 CPU 使用率超过 85% 且持续 10 分钟时触发告警',
  531. true, 'cpu_usage_percent', '>', 85.0, 5, 10, 'P2', true, 30, NOW(), NOW()
  532. ) ON CONFLICT (name) DO NOTHING;
  533. -- 6) Memory usage too high (P1)
  534. INSERT INTO ops_alert_rules (
  535. name, description, enabled, metric_type, operator, threshold,
  536. window_minutes, sustained_minutes, severity, notify_email, cooldown_minutes,
  537. created_at, updated_at
  538. ) VALUES (
  539. '内存使用率过高',
  540. '当内存使用率超过 90% 且持续 10 分钟时触发告警(可能导致 OOM)',
  541. true, 'memory_usage_percent', '>', 90.0, 5, 10, 'P1', true, 20, NOW(), NOW()
  542. ) ON CONFLICT (name) DO NOTHING;
  543. -- 7) Concurrency queue buildup (P1)
  544. INSERT INTO ops_alert_rules (
  545. name, description, enabled, metric_type, operator, threshold,
  546. window_minutes, sustained_minutes, severity, notify_email, cooldown_minutes,
  547. created_at, updated_at
  548. ) VALUES (
  549. '并发队列积压',
  550. '当并发队列深度超过 100 且持续 5 分钟时触发告警(系统处理能力不足)',
  551. true, 'concurrency_queue_depth', '>', 100.0, 5, 5, 'P1', true, 20, NOW(), NOW()
  552. ) ON CONFLICT (name) DO NOTHING;
  553. -- 8) Extremely high error rate (P0)
  554. INSERT INTO ops_alert_rules (
  555. name, description, enabled, metric_type, operator, threshold,
  556. window_minutes, sustained_minutes, severity, notify_email, cooldown_minutes,
  557. created_at, updated_at
  558. ) VALUES (
  559. '错误率极高',
  560. '当错误率超过 20% 且持续 1 分钟时触发告警(服务严重异常)',
  561. true, 'error_rate', '>', 20.0, 1, 1, 'P0', true, 15, NOW(), NOW()
  562. ) ON CONFLICT (name) DO NOTHING;
  563. -- Ops Monitoring vNext: add Redis pool stats fields to system metrics snapshots.
  564. -- This migration is intentionally idempotent.
  565. ALTER TABLE ops_system_metrics
  566. ADD COLUMN IF NOT EXISTS redis_conn_total INT,
  567. ADD COLUMN IF NOT EXISTS redis_conn_idle INT;
  568. COMMENT ON COLUMN ops_system_metrics.redis_conn_total IS 'Redis pool total connections (go-redis PoolStats.TotalConns).';
  569. COMMENT ON COLUMN ops_system_metrics.redis_conn_idle IS 'Redis pool idle connections (go-redis PoolStats.IdleConns).';