channel_monitor_v2_aggregation.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "time"
  7. )
  8. // Platform is derived from group/account (usage_logs has no provider column on upstream schema).
  9. const channelMonitorV2PlatformSQL = `lower(` + usageLogEffectivePlatformExpr + `)`
  10. const channelMonitorV2ModelSQL = `COALESCE(NULLIF(TRIM(ul.requested_model), ''), NULLIF(TRIM(ul.model), ''), 'unknown')`
  11. // Tiered retention balances UI windows against storage:
  12. //
  13. // 1m facts → short (late writes + rebuild rollups)
  14. // 5m/1h/12h/1d rollups → longer, aligned to 90m / 24h / 7d / 30d(+audit)
  15. //
  16. // Backfill may still write short-lived 1m rows for old windows so rollups can be
  17. // built; prune at end of each recompute drops them past their TTL while rollups remain.
  18. const (
  19. channelMonitorV2RetentionUser1m = 3 * 24 * time.Hour
  20. channelMonitorV2RetentionMetrics1m = 7 * 24 * time.Hour
  21. channelMonitorV2RetentionError1m = 7 * 24 * time.Hour
  22. channelMonitorV2RetentionHistogram1m = 7 * 24 * time.Hour
  23. channelMonitorV2RetentionRollup5m = 7 * 24 * time.Hour // bucket_seconds=300
  24. channelMonitorV2RetentionRollup1h = 30 * 24 * time.Hour // 3600
  25. channelMonitorV2RetentionRollup12h = 45 * 24 * time.Hour // 43200
  26. channelMonitorV2RetentionRollup1d = 90 * 24 * time.Hour // 86400
  27. channelMonitorV2RetentionMax = channelMonitorV2RetentionRollup1d
  28. )
  29. // channelMonitorV2MaxRetention is the longest stored window (1d rollup). Used to
  30. // clamp recompute/backfill so we never scan older than product history needs.
  31. func channelMonitorV2MaxRetention() time.Duration {
  32. return channelMonitorV2RetentionMax
  33. }
  34. func channelMonitorV2RetentionCutoff(now time.Time, retention time.Duration) time.Time {
  35. return now.UTC().Truncate(time.Minute).Add(-retention)
  36. }
  37. type channelMonitorV2RetentionRule struct {
  38. table string
  39. retention time.Duration
  40. bucketSeconds int // 0 = fact table (no bucket_seconds column)
  41. }
  42. // channelMonitorV2RetentionRules is ordered coarse→fine for predictable prune plans.
  43. var channelMonitorV2RetentionRules = []channelMonitorV2RetentionRule{
  44. {table: "channel_monitor_v2_user_metrics_1m", retention: channelMonitorV2RetentionUser1m},
  45. {table: "channel_monitor_v2_metrics_1m", retention: channelMonitorV2RetentionMetrics1m},
  46. {table: "channel_monitor_v2_error_metrics_1m", retention: channelMonitorV2RetentionError1m},
  47. {table: "channel_monitor_v2_latency_histograms_1m", retention: channelMonitorV2RetentionHistogram1m},
  48. {table: "channel_monitor_v2_metrics_rollup", retention: channelMonitorV2RetentionRollup5m, bucketSeconds: 300},
  49. {table: "channel_monitor_v2_user_metrics_rollup", retention: channelMonitorV2RetentionRollup5m, bucketSeconds: 300},
  50. {table: "channel_monitor_v2_error_metrics_rollup", retention: channelMonitorV2RetentionRollup5m, bucketSeconds: 300},
  51. {table: "channel_monitor_v2_latency_histograms_rollup", retention: channelMonitorV2RetentionRollup5m, bucketSeconds: 300},
  52. {table: "channel_monitor_v2_metrics_rollup", retention: channelMonitorV2RetentionRollup1h, bucketSeconds: 3600},
  53. {table: "channel_monitor_v2_user_metrics_rollup", retention: channelMonitorV2RetentionRollup1h, bucketSeconds: 3600},
  54. {table: "channel_monitor_v2_error_metrics_rollup", retention: channelMonitorV2RetentionRollup1h, bucketSeconds: 3600},
  55. {table: "channel_monitor_v2_latency_histograms_rollup", retention: channelMonitorV2RetentionRollup1h, bucketSeconds: 3600},
  56. {table: "channel_monitor_v2_metrics_rollup", retention: channelMonitorV2RetentionRollup12h, bucketSeconds: 43200},
  57. {table: "channel_monitor_v2_user_metrics_rollup", retention: channelMonitorV2RetentionRollup12h, bucketSeconds: 43200},
  58. {table: "channel_monitor_v2_error_metrics_rollup", retention: channelMonitorV2RetentionRollup12h, bucketSeconds: 43200},
  59. {table: "channel_monitor_v2_latency_histograms_rollup", retention: channelMonitorV2RetentionRollup12h, bucketSeconds: 43200},
  60. {table: "channel_monitor_v2_metrics_rollup", retention: channelMonitorV2RetentionRollup1d, bucketSeconds: 86400},
  61. {table: "channel_monitor_v2_user_metrics_rollup", retention: channelMonitorV2RetentionRollup1d, bucketSeconds: 86400},
  62. {table: "channel_monitor_v2_error_metrics_rollup", retention: channelMonitorV2RetentionRollup1d, bucketSeconds: 86400},
  63. {table: "channel_monitor_v2_latency_histograms_rollup", retention: channelMonitorV2RetentionRollup1d, bucketSeconds: 86400},
  64. }
  65. func (r *channelMonitorV2Repository) pruneChannelMonitorV2Retention(ctx context.Context, tx *sql.Tx, now time.Time) error {
  66. // During historical bootstrap, retain all 1m facts until the cursor reaches
  67. // the oldest rollup boundary. Otherwise adjacent chunks would rebuild the
  68. // same daily bucket from source rows already pruned by the prior chunk.
  69. var backfillCursor time.Time
  70. if err := tx.QueryRowContext(ctx, `SELECT backfill_cursor FROM channel_monitor_v2_watermarks WHERE id = 1`).Scan(&backfillCursor); err == nil && backfillCursor.After(channelMonitorV2RetentionCutoff(now, channelMonitorV2RetentionMax)) {
  71. return nil
  72. }
  73. for _, rule := range channelMonitorV2RetentionRules {
  74. cutoff := channelMonitorV2RetentionCutoff(now, rule.retention)
  75. var err error
  76. if rule.bucketSeconds == 0 {
  77. _, err = tx.ExecContext(ctx, fmt.Sprintf(`DELETE FROM %s WHERE bucket_start < $1`, rule.table), cutoff)
  78. } else {
  79. _, err = tx.ExecContext(ctx,
  80. fmt.Sprintf(`DELETE FROM %s WHERE bucket_seconds = $1 AND bucket_start < $2`, rule.table),
  81. rule.bucketSeconds, cutoff,
  82. )
  83. }
  84. if err != nil {
  85. return fmt.Errorf("prune %s (bucket_seconds=%d): %w", rule.table, rule.bucketSeconds, err)
  86. }
  87. }
  88. return nil
  89. }
  90. func (r *channelMonitorV2Repository) RecomputeRange(ctx context.Context, start, end time.Time) (err error) {
  91. start = start.UTC().Truncate(time.Minute)
  92. end = end.UTC().Truncate(time.Minute)
  93. now := time.Now().UTC().Truncate(time.Minute)
  94. // Clamp to longest rollup TTL so backfill does not scan beyond product history.
  95. maxCutoff := channelMonitorV2RetentionCutoff(now, channelMonitorV2MaxRetention())
  96. if start.Before(maxCutoff) {
  97. start = maxCutoff
  98. }
  99. if !start.Before(end) {
  100. return nil
  101. }
  102. tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
  103. if err != nil {
  104. return err
  105. }
  106. defer func() {
  107. if err != nil {
  108. _ = tx.Rollback()
  109. }
  110. }()
  111. // Idempotent window rewrite: drop existing facts/rollups in [start,end) then re-insert.
  112. for _, table := range []string{
  113. "channel_monitor_v2_latency_histograms_rollup",
  114. "channel_monitor_v2_error_metrics_rollup",
  115. "channel_monitor_v2_user_metrics_rollup",
  116. "channel_monitor_v2_metrics_rollup",
  117. "channel_monitor_v2_latency_histograms_1m",
  118. "channel_monitor_v2_error_metrics_1m",
  119. "channel_monitor_v2_user_metrics_1m",
  120. "channel_monitor_v2_metrics_1m",
  121. } {
  122. if _, err = tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM %s WHERE bucket_start >= $1 AND bucket_start < $2", table), start, end); err != nil {
  123. return err
  124. }
  125. }
  126. if _, err = tx.ExecContext(ctx, fmt.Sprintf(channelMonitorV2UsageMetricsSQL, channelMonitorV2PlatformSQL, channelMonitorV2ModelSQL), start, end); err != nil {
  127. return fmt.Errorf("aggregate channel monitor v2 usage: %w", err)
  128. }
  129. if _, err = tx.ExecContext(ctx, fmt.Sprintf(channelMonitorV2UserMetricsSQL, channelMonitorV2PlatformSQL, channelMonitorV2ModelSQL), start, end); err != nil {
  130. return fmt.Errorf("aggregate channel monitor v2 users: %w", err)
  131. }
  132. if _, err = tx.ExecContext(ctx, fmt.Sprintf(channelMonitorV2HistogramSQL, channelMonitorV2PlatformSQL, channelMonitorV2ModelSQL, channelMonitorV2HistogramBoundSQL("latency.value_ms")), start, end); err != nil {
  133. return fmt.Errorf("aggregate channel monitor v2 histograms: %w", err)
  134. }
  135. if _, err = tx.ExecContext(ctx, channelMonitorV2ErrorAggregationSQL, start, end); err != nil {
  136. return fmt.Errorf("aggregate channel monitor v2 errors: %w", err)
  137. }
  138. if err = r.recomputeFixedRollups(ctx, tx, start, end); err != nil {
  139. return err
  140. }
  141. // Drop rows past per-tier TTL (1m short, coarse rollups long). Safe after rollup
  142. // so a backfill chunk can build 1d rollups from temporary 1m rows then discard 1m.
  143. if err = r.pruneChannelMonitorV2Retention(ctx, tx, now); err != nil {
  144. return err
  145. }
  146. if _, err = tx.ExecContext(ctx, channelMonitorV2WatermarkSQL, start, end); err != nil {
  147. return err
  148. }
  149. if err = tx.Commit(); err != nil {
  150. return err
  151. }
  152. return nil
  153. }
  154. const channelMonitorV2UsageMetricsSQL = `
  155. INSERT INTO channel_monitor_v2_metrics_1m (
  156. bucket_start, platform, group_id, model, success_requests,
  157. input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
  158. ttft_sum_ms, ttft_count, duration_sum_ms, duration_count, computed_at
  159. )
  160. SELECT date_trunc('minute', ul.created_at), %s, COALESCE(ul.group_id, 0), %s,
  161. COUNT(DISTINCT COALESCE(NULLIF(ul.request_id, ''), 'usage:' || ul.id::text))
  162. FILTER (WHERE COALESCE(ul.request_type, 0) NOT IN (4, 6) AND ` + usageLogSuccessFilterUL + `),
  163. COALESCE(SUM(ul.input_tokens) FILTER (WHERE ` + usageLogSuccessFilterUL + `), 0),
  164. COALESCE(SUM(ul.output_tokens) FILTER (WHERE ` + usageLogSuccessFilterUL + `), 0),
  165. COALESCE(SUM(ul.cache_creation_tokens) FILTER (WHERE ` + usageLogSuccessFilterUL + `), 0),
  166. COALESCE(SUM(ul.cache_read_tokens) FILTER (WHERE ` + usageLogSuccessFilterUL + `), 0),
  167. COALESCE(SUM(ul.first_token_ms) FILTER (WHERE ul.first_token_ms IS NOT NULL AND ` + usageLogSuccessFilterUL + `), 0),
  168. COUNT(ul.first_token_ms) FILTER (WHERE ` + usageLogSuccessFilterUL + `),
  169. COALESCE(SUM(ul.duration_ms) FILTER (WHERE ul.duration_ms IS NOT NULL AND ` + usageLogSuccessFilterUL + `), 0),
  170. COUNT(ul.duration_ms) FILTER (WHERE ` + usageLogSuccessFilterUL + `), NOW()
  171. FROM usage_logs ul
  172. LEFT JOIN groups g ON g.id = ul.group_id
  173. LEFT JOIN accounts a ON a.id = ul.account_id
  174. WHERE ul.created_at >= $1 AND ul.created_at < $2
  175. GROUP BY 1, 2, 3, 4`
  176. const channelMonitorV2UserMetricsSQL = `
  177. INSERT INTO channel_monitor_v2_user_metrics_1m (
  178. bucket_start, platform, group_id, model, user_id, success_requests,
  179. input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
  180. ttft_sum_ms, ttft_count, duration_sum_ms, duration_count, computed_at
  181. )
  182. SELECT date_trunc('minute', ul.created_at), %s, COALESCE(ul.group_id, 0), %s, ul.user_id,
  183. COUNT(DISTINCT COALESCE(NULLIF(ul.request_id, ''), 'usage:' || ul.id::text))
  184. FILTER (WHERE COALESCE(ul.request_type, 0) NOT IN (4, 6) AND ` + usageLogSuccessFilterUL + `),
  185. COALESCE(SUM(ul.input_tokens) FILTER (WHERE ` + usageLogSuccessFilterUL + `), 0),
  186. COALESCE(SUM(ul.output_tokens) FILTER (WHERE ` + usageLogSuccessFilterUL + `), 0),
  187. COALESCE(SUM(ul.cache_creation_tokens) FILTER (WHERE ` + usageLogSuccessFilterUL + `), 0),
  188. COALESCE(SUM(ul.cache_read_tokens) FILTER (WHERE ` + usageLogSuccessFilterUL + `), 0),
  189. COALESCE(SUM(ul.first_token_ms) FILTER (WHERE ul.first_token_ms IS NOT NULL AND ` + usageLogSuccessFilterUL + `), 0),
  190. COUNT(ul.first_token_ms) FILTER (WHERE ` + usageLogSuccessFilterUL + `),
  191. COALESCE(SUM(ul.duration_ms) FILTER (WHERE ul.duration_ms IS NOT NULL AND ` + usageLogSuccessFilterUL + `), 0),
  192. COUNT(ul.duration_ms) FILTER (WHERE ` + usageLogSuccessFilterUL + `), NOW()
  193. FROM usage_logs ul
  194. LEFT JOIN groups g ON g.id = ul.group_id
  195. LEFT JOIN accounts a ON a.id = ul.account_id
  196. WHERE ul.created_at >= $1 AND ul.created_at < $2 AND ul.user_id IS NOT NULL
  197. GROUP BY 1, 2, 3, 4, 5`
  198. const channelMonitorV2HistogramSQL = `
  199. INSERT INTO channel_monitor_v2_latency_histograms_1m (
  200. bucket_start, platform, group_id, model, user_id, metric, upper_bound_ms, sample_count
  201. )
  202. SELECT date_trunc('minute', ul.created_at), %s, COALESCE(ul.group_id, 0), %s,
  203. audience.user_id, latency.metric, %s, COUNT(*)
  204. FROM usage_logs ul
  205. LEFT JOIN groups g ON g.id = ul.group_id
  206. LEFT JOIN accounts a ON a.id = ul.account_id
  207. CROSS JOIN LATERAL (VALUES (0::bigint), (ul.user_id)) audience(user_id)
  208. CROSS JOIN LATERAL (VALUES ('ttft'::text, ul.first_token_ms), ('duration'::text, ul.duration_ms)) latency(metric, value_ms)
  209. WHERE ul.created_at >= $1 AND ul.created_at < $2
  210. AND audience.user_id IS NOT NULL AND latency.value_ms IS NOT NULL AND latency.value_ms >= 0
  211. AND ` + usageLogSuccessFilterUL + `
  212. GROUP BY 1, 2, 3, 4, 5, 6, 7`
  213. func channelMonitorV2HistogramBoundSQL(column string) string {
  214. return `CASE
  215. WHEN ` + column + ` <= 50 THEN 50 WHEN ` + column + ` <= 100 THEN 100
  216. WHEN ` + column + ` <= 250 THEN 250 WHEN ` + column + ` <= 500 THEN 500
  217. WHEN ` + column + ` <= 1000 THEN 1000 WHEN ` + column + ` <= 2000 THEN 2000
  218. WHEN ` + column + ` <= 3000 THEN 3000 WHEN ` + column + ` <= 5000 THEN 5000
  219. WHEN ` + column + ` <= 8000 THEN 8000 WHEN ` + column + ` <= 10000 THEN 10000
  220. WHEN ` + column + ` <= 15000 THEN 15000 WHEN ` + column + ` <= 30000 THEN 30000
  221. WHEN ` + column + ` <= 60000 THEN 60000 WHEN ` + column + ` <= 120000 THEN 120000
  222. WHEN ` + column + ` <= 300000 THEN 300000 WHEN ` + column + ` <= 600000 THEN 600000
  223. ELSE 2147483647 END`
  224. }
  225. // Error dedup lookback: request_id branch is bounded by chunk start minus 90
  226. // minutes so candidate_ids never forces a full-history scan of ops_error_logs.
  227. const channelMonitorV2ErrorAggregationSQL = `
  228. WITH dedup AS (
  229. WITH candidate_ids AS MATERIALIZED (
  230. SELECT DISTINCT request_id
  231. FROM ops_error_logs
  232. WHERE created_at >= $1 AND created_at < $2 AND NULLIF(request_id, '') IS NOT NULL
  233. )
  234. SELECT DISTINCT ON (COALESCE(NULLIF(current_error.request_id, ''), 'error:' || current_error.id::text))
  235. date_trunc('minute', current_error.created_at) AS bucket_start,
  236. -- Composite groups are a routing layer: resolve the concrete account
  237. -- platform (mirrors usageLogEffectivePlatformExpr on the usage side) so
  238. -- error facts share the usage facts' platform key. Without this, composite
  239. -- group errors aggregate under platform 'composite', which is never an
  240. -- enabled config platform, and are filtered out of every monitor v2 query.
  241. lower(CASE
  242. WHEN g.platform = 'composite' THEN COALESCE(NULLIF(TRIM(a.platform), ''), NULLIF(NULLIF(lower(TRIM(current_error.platform)), ''), 'composite'), 'unknown')
  243. ELSE COALESCE(NULLIF(TRIM(current_error.platform), ''), 'unknown')
  244. END) AS platform,
  245. COALESCE(current_error.group_id, 0) AS group_id,
  246. COALESCE(NULLIF(TRIM(current_error.requested_model), ''), NULLIF(TRIM(current_error.model), ''), 'unknown') AS model,
  247. current_error.user_id, current_error.error_type, current_error.error_owner, COALESCE(current_error.status_code, 0) AS status_code,
  248. COALESCE(current_error.upstream_status_code, 0) AS upstream_status_code,
  249. lower(CONCAT_WS(' ', current_error.error_type, current_error.error_source, current_error.error_message, current_error.upstream_error_message, current_error.upstream_error_detail, current_error.error_body)) AS text,
  250. (CASE WHEN jsonb_typeof(current_error.upstream_errors) = 'array' THEN jsonb_array_length(current_error.upstream_errors) > 0 ELSE FALSE END
  251. OR current_error.error_owner = 'provider' OR current_error.upstream_status_code IS NOT NULL) AS upstream_affected,
  252. CASE WHEN jsonb_typeof(current_error.upstream_errors) = 'array' THEN jsonb_array_length(current_error.upstream_errors) ELSE 0 END AS upstream_attempts
  253. FROM ops_error_logs current_error
  254. LEFT JOIN groups g ON g.id = current_error.group_id
  255. LEFT JOIN accounts a ON a.id = current_error.account_id
  256. WHERE (
  257. (NULLIF(current_error.request_id, '') IS NULL AND current_error.created_at >= $1 AND current_error.created_at < $2)
  258. OR (
  259. current_error.request_id IN (SELECT request_id FROM candidate_ids)
  260. AND current_error.created_at >= $1 - INTERVAL '90 minutes'
  261. AND current_error.created_at < $2
  262. )
  263. )
  264. AND NOT current_error.is_count_tokens
  265. AND (COALESCE(current_error.status_code, 0) >= 400 OR current_error.error_type = 'cyber_policy')
  266. ORDER BY COALESCE(NULLIF(current_error.request_id, ''), 'error:' || current_error.id::text), current_error.created_at DESC, current_error.id DESC
  267. ), classified AS (
  268. SELECT *, CASE
  269. -- Keep in lockstep with service.ClassifyChannelMonitorV2Error needles.
  270. WHEN error_type = 'cyber_policy' OR text LIKE ANY(ARRAY['%content policy%','%content_policy%','%safety policy%','%moderation%','%blocked keyword%']) THEN 'content_policy'
  271. WHEN status_code = 401 OR upstream_status_code = 401 OR text LIKE ANY(ARRAY['%unauthorized%','%invalid api key%','%invalid_api_key%','%authentication%','%api_key_disabled%']) THEN 'authentication'
  272. WHEN text LIKE ANY(ARRAY['%context window%','%context length%','%maximum prompt length%','%too many tokens%','%max_tokens%']) THEN 'context_limit'
  273. WHEN text LIKE ANY(ARRAY['%failed to deserialize%','%missing required parameter%','%invalid request%','%invalid_request%','%tool_choice%']) THEN 'invalid_request'
  274. WHEN text LIKE ANY(ARRAY['%does not support the requested model%','%not supported by any configured account%','%model not supported%','%unsupported model%']) THEN 'model_unsupported'
  275. WHEN text LIKE ANY(ARRAY['%group not allowed%','%group_not_allowed%','%group access%']) THEN 'group_access'
  276. WHEN text LIKE ANY(ARRAY['%run out of credits%','%insufficient balance%','%insufficient quota%','%subscription%','%quota exceeded%','%billing hard limit%']) THEN 'quota_or_balance'
  277. WHEN text LIKE ANY(ARRAY['%no available accounts%','%no healthy account%','%no healthy upstream account%','%failover budget exhausted%','%account pool%']) THEN 'account_pool_unavailable'
  278. WHEN status_code = 429 OR upstream_status_code = 429 OR text LIKE ANY(ARRAY['%rate limit%','%rate_limit%','%high demand%','%overloaded%','%concurrency limit%','%capacity%']) THEN 'rate_or_capacity'
  279. WHEN status_code IN (408,504) OR text LIKE ANY(ARRAY['%timeout%','%deadline exceeded%','%error code: 524%','%gateway time-out%','%gateway timeout%']) THEN 'timeout'
  280. WHEN text LIKE ANY(ARRAY['%transport%','%stream_read_error%','%connection reset%','%connection refused%','%tls%','%http2%','%missing terminal event%','%unexpected eof%']) THEN 'transport_or_stream'
  281. WHEN status_code = 403 OR upstream_status_code = 403 THEN 'upstream_forbidden'
  282. WHEN status_code = 404 OR upstream_status_code = 404 THEN 'not_found'
  283. WHEN status_code = 499 OR text LIKE ANY(ARRAY['%client cancelled%','%client canceled%','%context canceled%']) THEN 'client_cancelled'
  284. WHEN upstream_status_code >= 500 OR (error_owner = 'provider' AND status_code >= 500) THEN 'upstream_5xx'
  285. WHEN status_code >= 500 OR error_type = 'internal' OR error_owner = 'system' THEN 'internal'
  286. ELSE 'other' END AS category
  287. FROM dedup
  288. WHERE bucket_start >= $1 AND bucket_start < $2
  289. ), metric_rows AS (
  290. INSERT INTO channel_monitor_v2_metrics_1m (bucket_start, platform, group_id, model, error_requests, upstream_affected_requests, upstream_attempt_count, computed_at)
  291. SELECT bucket_start, platform, group_id, model, COUNT(*), COUNT(*) FILTER (WHERE upstream_affected), SUM(upstream_attempts), NOW()
  292. FROM classified GROUP BY 1,2,3,4
  293. ON CONFLICT (bucket_start, platform, group_id, model) DO UPDATE SET
  294. error_requests = EXCLUDED.error_requests, upstream_affected_requests = EXCLUDED.upstream_affected_requests,
  295. upstream_attempt_count = EXCLUDED.upstream_attempt_count, computed_at = NOW()
  296. ), user_rows AS (
  297. INSERT INTO channel_monitor_v2_user_metrics_1m (bucket_start, platform, group_id, model, user_id, error_requests, computed_at)
  298. SELECT bucket_start, platform, group_id, model, user_id, COUNT(*), NOW()
  299. FROM classified WHERE user_id IS NOT NULL GROUP BY 1,2,3,4,5
  300. ON CONFLICT (bucket_start, platform, group_id, model, user_id) DO UPDATE SET error_requests = EXCLUDED.error_requests, computed_at = NOW()
  301. )
  302. INSERT INTO channel_monitor_v2_error_metrics_1m (bucket_start, platform, group_id, model, error_category, taxonomy_version, error_requests)
  303. SELECT bucket_start, platform, group_id, model, category, 1, COUNT(*) FROM classified GROUP BY 1,2,3,4,5
  304. ON CONFLICT (bucket_start, platform, group_id, model, error_category, taxonomy_version)
  305. DO UPDATE SET error_requests = EXCLUDED.error_requests`
  306. // Floor matches channelMonitorV2RetentionMax (90d). Keep the INTERVAL literal in
  307. // sync when changing channelMonitorV2RetentionRollup1d.
  308. //
  309. // Coverage starts track how far back recompute has walked ($1 = chunk start), not
  310. // "min(source_log.created_at)". Using global min(ops_error_logs) pins
  311. // error_coverage_start to the first real error forever and collapses UI windows
  312. // when errors only exist in a recent slice (common on first upgrade).
  313. const channelMonitorV2WatermarkSQL = `
  314. INSERT INTO channel_monitor_v2_watermarks (id, usage_coverage_start, error_coverage_start, data_through, last_successful_at, backfill_cursor, updated_at)
  315. VALUES (
  316. 1,
  317. $1,
  318. $1,
  319. $2, NOW(), $1, NOW()
  320. )
  321. ON CONFLICT (id) DO UPDATE SET
  322. usage_coverage_start = GREATEST(
  323. date_trunc('minute', NOW()) - INTERVAL '90 days',
  324. LEAST(COALESCE(channel_monitor_v2_watermarks.usage_coverage_start, EXCLUDED.usage_coverage_start), EXCLUDED.usage_coverage_start)
  325. ),
  326. error_coverage_start = GREATEST(
  327. date_trunc('minute', NOW()) - INTERVAL '90 days',
  328. LEAST(COALESCE(channel_monitor_v2_watermarks.error_coverage_start, EXCLUDED.error_coverage_start), EXCLUDED.error_coverage_start)
  329. ),
  330. data_through = GREATEST(COALESCE(channel_monitor_v2_watermarks.data_through, EXCLUDED.data_through), EXCLUDED.data_through),
  331. last_successful_at = NOW(),
  332. backfill_cursor = LEAST(COALESCE(channel_monitor_v2_watermarks.backfill_cursor, EXCLUDED.backfill_cursor), EXCLUDED.backfill_cursor),
  333. updated_at = NOW()`
  334. var channelMonitorV2FixedRollupSeconds = []int{300, 3600, 43200, 86400}
  335. func (r *channelMonitorV2Repository) recomputeFixedRollups(ctx context.Context, tx *sql.Tx, start, end time.Time) error {
  336. for _, seconds := range channelMonitorV2FixedRollupSeconds {
  337. // Coarse buckets are immutable between boundaries during the normal
  338. // trailing refresh. Historical backfills and boundary-crossing windows
  339. // still rebuild them; this avoids repeatedly regrouping the full current
  340. // day/user table every few minutes.
  341. if seconds >= 43200 && sameFixedRollupBucket(start, end, seconds) {
  342. continue
  343. }
  344. interval := fmt.Sprintf("%d seconds", seconds)
  345. for _, table := range []string{
  346. "channel_monitor_v2_latency_histograms_rollup",
  347. "channel_monitor_v2_error_metrics_rollup",
  348. "channel_monitor_v2_user_metrics_rollup",
  349. "channel_monitor_v2_metrics_rollup",
  350. } {
  351. if _, err := tx.ExecContext(ctx, fmt.Sprintf(channelMonitorV2FixedRollupDeleteSQL, table), interval, seconds, start, end); err != nil {
  352. return err
  353. }
  354. }
  355. if _, err := tx.ExecContext(ctx, channelMonitorV2MetricsRollupSQL, interval, seconds, start, end); err != nil {
  356. return fmt.Errorf("roll up channel monitor v2 metrics %ds: %w", seconds, err)
  357. }
  358. if _, err := tx.ExecContext(ctx, channelMonitorV2UserMetricsRollupSQL, interval, seconds, start, end); err != nil {
  359. return fmt.Errorf("roll up channel monitor v2 user metrics %ds: %w", seconds, err)
  360. }
  361. if _, err := tx.ExecContext(ctx, channelMonitorV2HistogramRollupSQL, interval, seconds, start, end); err != nil {
  362. return fmt.Errorf("roll up channel monitor v2 histograms %ds: %w", seconds, err)
  363. }
  364. if _, err := tx.ExecContext(ctx, channelMonitorV2ErrorRollupSQL, interval, seconds, start, end); err != nil {
  365. return fmt.Errorf("roll up channel monitor v2 errors %ds: %w", seconds, err)
  366. }
  367. }
  368. return nil
  369. }
  370. func sameFixedRollupBucket(start, end time.Time, seconds int) bool {
  371. if !end.After(start) {
  372. return true
  373. }
  374. interval := time.Duration(seconds) * time.Second
  375. return start.Truncate(interval).Equal(end.Add(-time.Nanosecond).Truncate(interval))
  376. }
  377. const channelMonitorV2FixedRollupBoundsSQL = `
  378. WITH bounds AS (
  379. SELECT
  380. date_bin($1::interval, $3::timestamptz, TIMESTAMPTZ '1970-01-01') AS start_at,
  381. date_bin($1::interval, $4::timestamptz - INTERVAL '1 microsecond', TIMESTAMPTZ '1970-01-01') + $1::interval AS end_at
  382. )`
  383. const channelMonitorV2FixedRollupDeleteSQL = channelMonitorV2FixedRollupBoundsSQL + `
  384. DELETE FROM %s
  385. USING bounds
  386. WHERE bucket_seconds = $2::integer
  387. AND bucket_start >= bounds.start_at
  388. AND bucket_start < bounds.end_at`
  389. const channelMonitorV2MetricsRollupSQL = `
  390. INSERT INTO channel_monitor_v2_metrics_rollup (
  391. bucket_start, bucket_seconds, platform, group_id, model, success_requests, error_requests,
  392. upstream_affected_requests, upstream_attempt_count, input_tokens, output_tokens,
  393. cache_creation_tokens, cache_read_tokens, ttft_sum_ms, ttft_count, duration_sum_ms,
  394. duration_count, computed_at
  395. )
  396. ` + channelMonitorV2FixedRollupBoundsSQL + `
  397. SELECT date_bin($1::interval, m.bucket_start, TIMESTAMPTZ '1970-01-01'), $2::integer,
  398. platform, group_id, model, SUM(success_requests), SUM(error_requests),
  399. SUM(upstream_affected_requests), SUM(upstream_attempt_count), SUM(input_tokens),
  400. SUM(output_tokens), SUM(cache_creation_tokens), SUM(cache_read_tokens),
  401. SUM(ttft_sum_ms), SUM(ttft_count), SUM(duration_sum_ms), SUM(duration_count), NOW()
  402. FROM channel_monitor_v2_metrics_1m m, bounds
  403. WHERE m.bucket_start >= bounds.start_at AND m.bucket_start < bounds.end_at
  404. GROUP BY 1, 2, 3, 4, 5`
  405. const channelMonitorV2UserMetricsRollupSQL = `
  406. INSERT INTO channel_monitor_v2_user_metrics_rollup (
  407. bucket_start, bucket_seconds, platform, group_id, model, user_id, success_requests,
  408. error_requests, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
  409. ttft_sum_ms, ttft_count, duration_sum_ms, duration_count, computed_at
  410. )
  411. ` + channelMonitorV2FixedRollupBoundsSQL + `
  412. SELECT date_bin($1::interval, m.bucket_start, TIMESTAMPTZ '1970-01-01'), $2::integer,
  413. platform, group_id, model, user_id, SUM(success_requests), SUM(error_requests),
  414. SUM(input_tokens), SUM(output_tokens), SUM(cache_creation_tokens), SUM(cache_read_tokens),
  415. SUM(ttft_sum_ms), SUM(ttft_count), SUM(duration_sum_ms), SUM(duration_count), NOW()
  416. FROM channel_monitor_v2_user_metrics_1m m, bounds
  417. WHERE m.bucket_start >= bounds.start_at AND m.bucket_start < bounds.end_at
  418. GROUP BY 1, 2, 3, 4, 5, 6`
  419. const channelMonitorV2HistogramRollupSQL = `
  420. INSERT INTO channel_monitor_v2_latency_histograms_rollup (
  421. bucket_start, bucket_seconds, platform, group_id, model, user_id, metric, upper_bound_ms, sample_count
  422. )
  423. ` + channelMonitorV2FixedRollupBoundsSQL + `
  424. SELECT date_bin($1::interval, h.bucket_start, TIMESTAMPTZ '1970-01-01'), $2::integer,
  425. platform, group_id, model, user_id, metric, upper_bound_ms, SUM(sample_count)
  426. FROM channel_monitor_v2_latency_histograms_1m h, bounds
  427. WHERE h.bucket_start >= bounds.start_at AND h.bucket_start < bounds.end_at
  428. GROUP BY 1, 2, 3, 4, 5, 6, 7, 8`
  429. const channelMonitorV2ErrorRollupSQL = `
  430. INSERT INTO channel_monitor_v2_error_metrics_rollup (
  431. bucket_start, bucket_seconds, platform, group_id, model, error_category, taxonomy_version, error_requests
  432. )
  433. ` + channelMonitorV2FixedRollupBoundsSQL + `
  434. SELECT date_bin($1::interval, e.bucket_start, TIMESTAMPTZ '1970-01-01'), $2::integer,
  435. platform, group_id, model, error_category, taxonomy_version, SUM(error_requests)
  436. FROM channel_monitor_v2_error_metrics_1m e, bounds
  437. WHERE e.bucket_start >= bounds.start_at AND e.bucket_start < bounds.end_at
  438. GROUP BY 1, 2, 3, 4, 5, 6, 7`