railwatch 0.9.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +37 -33
  3. data/CHANGELOG.md +50 -0
  4. data/README.md +51 -60
  5. data/config/routes.rb +0 -59
  6. data/docs/ai-and-mcp.md +9 -8
  7. data/docs/configuration.md +35 -66
  8. data/docs/faq.md +17 -25
  9. data/docs/getting-started.md +105 -117
  10. data/docs/replacing-nightwatch.md +8 -7
  11. data/docs/replacing-sentry.md +12 -16
  12. data/docs/security.md +14 -20
  13. data/docs/self-hosting.md +24 -22
  14. data/docs/testing.md +6 -14
  15. data/docs/troubleshooting.md +12 -46
  16. data/lib/generators/railwatch/install/install_generator.rb +12 -240
  17. data/lib/generators/railwatch/install/templates/initializer.rb.tt +10 -40
  18. data/lib/generators/railwatch/install/templates/post-deploy +0 -7
  19. data/lib/railwatch/authentication.rb +6 -5
  20. data/lib/railwatch/configuration.rb +21 -151
  21. data/lib/railwatch/engine.rb +9 -118
  22. data/lib/railwatch/patches.rb +0 -11
  23. data/lib/railwatch/reporter.rb +3 -19
  24. data/lib/railwatch/subscribers/process_info.rb +1 -2
  25. data/lib/railwatch/version.rb +1 -1
  26. data/lib/railwatch.rb +4 -62
  27. data/lib/tasks/railwatch_tasks.rake +3 -325
  28. data/llms.txt +23 -25
  29. metadata +3 -453
  30. data/app/channels/railwatch/environment_channel.rb +0 -62
  31. data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
  32. data/app/controllers/railwatch/alerts_controller.rb +0 -48
  33. data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
  34. data/app/controllers/railwatch/attachments_controller.rb +0 -22
  35. data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
  36. data/app/controllers/railwatch/cache_events_controller.rb +0 -30
  37. data/app/controllers/railwatch/commands_controller.rb +0 -16
  38. data/app/controllers/railwatch/comments_controller.rb +0 -11
  39. data/app/controllers/railwatch/dashboard_controller.rb +0 -70
  40. data/app/controllers/railwatch/deploys_controller.rb +0 -34
  41. data/app/controllers/railwatch/deprecations_controller.rb +0 -54
  42. data/app/controllers/railwatch/environment_scoped.rb +0 -81
  43. data/app/controllers/railwatch/exceptions_controller.rb +0 -52
  44. data/app/controllers/railwatch/executions_controller.rb +0 -13
  45. data/app/controllers/railwatch/issues_controller.rb +0 -258
  46. data/app/controllers/railwatch/jobs_controller.rb +0 -63
  47. data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
  48. data/app/controllers/railwatch/logs_controller.rb +0 -40
  49. data/app/controllers/railwatch/mails_controller.rb +0 -19
  50. data/app/controllers/railwatch/notifications_controller.rb +0 -11
  51. data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
  52. data/app/controllers/railwatch/overview_controller.rb +0 -38
  53. data/app/controllers/railwatch/people_controller.rb +0 -25
  54. data/app/controllers/railwatch/processes_controller.rb +0 -33
  55. data/app/controllers/railwatch/profiles_controller.rb +0 -65
  56. data/app/controllers/railwatch/queries_controller.rb +0 -59
  57. data/app/controllers/railwatch/releases_controller.rb +0 -75
  58. data/app/controllers/railwatch/requests_controller.rb +0 -37
  59. data/app/controllers/railwatch/saved_views_controller.rb +0 -53
  60. data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
  61. data/app/controllers/railwatch/spans_controller.rb +0 -47
  62. data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
  63. data/app/controllers/railwatch/tenants_controller.rb +0 -44
  64. data/app/controllers/railwatch/thresholds_controller.rb +0 -47
  65. data/app/controllers/railwatch/traces_controller.rb +0 -72
  66. data/app/controllers/railwatch/transactions_controller.rb +0 -21
  67. data/app/controllers/railwatch/view_renders_controller.rb +0 -28
  68. data/app/controllers/railwatch/visits_controller.rb +0 -53
  69. data/app/frontend/components/app-content.tsx +0 -26
  70. data/app/frontend/components/app-logo-icon.tsx +0 -23
  71. data/app/frontend/components/app-logo.tsx +0 -14
  72. data/app/frontend/components/app-shell.tsx +0 -38
  73. data/app/frontend/components/app-sidebar-header.tsx +0 -54
  74. data/app/frontend/components/app-sidebar.tsx +0 -94
  75. data/app/frontend/components/app-wordmark.tsx +0 -29
  76. data/app/frontend/components/breadcrumbs.tsx +0 -46
  77. data/app/frontend/components/icon.tsx +0 -16
  78. data/app/frontend/components/input-error.tsx +0 -18
  79. data/app/frontend/components/nav-footer.tsx +0 -46
  80. data/app/frontend/components/nav-main.tsx +0 -41
  81. data/app/frontend/components/nav-user.tsx +0 -50
  82. data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
  83. data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
  84. data/app/frontend/components/railwatch/code.tsx +0 -28
  85. data/app/frontend/components/railwatch/command-palette.tsx +0 -132
  86. data/app/frontend/components/railwatch/copy-block.tsx +0 -52
  87. data/app/frontend/components/railwatch/copy-id.tsx +0 -49
  88. data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
  89. data/app/frontend/components/railwatch/data-table.tsx +0 -195
  90. data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
  91. data/app/frontend/components/railwatch/empty-state.tsx +0 -159
  92. data/app/frontend/components/railwatch/env-nav.tsx +0 -130
  93. data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
  94. data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
  95. data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
  96. data/app/frontend/components/railwatch/frames.tsx +0 -295
  97. data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
  98. data/app/frontend/components/railwatch/live-dot.tsx +0 -78
  99. data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
  100. data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
  101. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
  102. data/app/frontend/components/railwatch/onboarding.tsx +0 -91
  103. data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
  104. data/app/frontend/components/railwatch/page-header.tsx +0 -42
  105. data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
  106. data/app/frontend/components/railwatch/relative-time.tsx +0 -18
  107. data/app/frontend/components/railwatch/saved-views.tsx +0 -232
  108. data/app/frontend/components/railwatch/segmented.tsx +0 -64
  109. data/app/frontend/components/railwatch/series-chart.tsx +0 -400
  110. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
  111. data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
  112. data/app/frontend/components/railwatch/sort-header.tsx +0 -37
  113. data/app/frontend/components/railwatch/source-link.tsx +0 -57
  114. data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
  115. data/app/frontend/components/railwatch/sql-block.tsx +0 -116
  116. data/app/frontend/components/railwatch/stat.tsx +0 -185
  117. data/app/frontend/components/railwatch/status-badge.tsx +0 -79
  118. data/app/frontend/components/railwatch/step-picker.tsx +0 -57
  119. data/app/frontend/components/railwatch/timeline.tsx +0 -405
  120. data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
  121. data/app/frontend/components/railwatch/window-picker.tsx +0 -147
  122. data/app/frontend/components/ui/avatar.tsx +0 -51
  123. data/app/frontend/components/ui/badge.tsx +0 -46
  124. data/app/frontend/components/ui/breadcrumb.tsx +0 -109
  125. data/app/frontend/components/ui/button.tsx +0 -60
  126. data/app/frontend/components/ui/card.tsx +0 -92
  127. data/app/frontend/components/ui/chart.tsx +0 -374
  128. data/app/frontend/components/ui/checkbox.tsx +0 -32
  129. data/app/frontend/components/ui/collapsible.tsx +0 -31
  130. data/app/frontend/components/ui/command.tsx +0 -182
  131. data/app/frontend/components/ui/dialog.tsx +0 -158
  132. data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
  133. data/app/frontend/components/ui/hover-card.tsx +0 -42
  134. data/app/frontend/components/ui/input.tsx +0 -21
  135. data/app/frontend/components/ui/label.tsx +0 -24
  136. data/app/frontend/components/ui/popover.tsx +0 -87
  137. data/app/frontend/components/ui/select.tsx +0 -185
  138. data/app/frontend/components/ui/separator.tsx +0 -28
  139. data/app/frontend/components/ui/sheet.tsx +0 -137
  140. data/app/frontend/components/ui/sidebar.tsx +0 -726
  141. data/app/frontend/components/ui/skeleton.tsx +0 -13
  142. data/app/frontend/components/ui/sonner.tsx +0 -41
  143. data/app/frontend/components/ui/table.tsx +0 -114
  144. data/app/frontend/components/ui/tabs.tsx +0 -91
  145. data/app/frontend/components/ui/textarea.tsx +0 -18
  146. data/app/frontend/components/ui/tooltip.tsx +0 -61
  147. data/app/frontend/components/user-info.tsx +0 -32
  148. data/app/frontend/components/user-menu-content.tsx +0 -94
  149. data/app/frontend/entrypoints/application.css +0 -244
  150. data/app/frontend/entrypoints/inertia.tsx +0 -92
  151. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  152. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  153. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  154. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  155. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  156. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  157. data/app/frontend/hooks/use-appearance.tsx +0 -67
  158. data/app/frontend/hooks/use-clipboard.ts +0 -32
  159. data/app/frontend/hooks/use-flash.tsx +0 -23
  160. data/app/frontend/hooks/use-hotkeys.ts +0 -63
  161. data/app/frontend/hooks/use-initials.tsx +0 -15
  162. data/app/frontend/hooks/use-live.ts +0 -205
  163. data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
  164. data/app/frontend/hooks/use-mobile.ts +0 -21
  165. data/app/frontend/hooks/use-percentile.ts +0 -24
  166. data/app/frontend/hooks/use-reduced-motion.ts +0 -25
  167. data/app/frontend/hooks/use-row-nav.ts +0 -106
  168. data/app/frontend/hooks/use-step.ts +0 -27
  169. data/app/frontend/hooks/use-ticker.ts +0 -12
  170. data/app/frontend/hooks/use-window.ts +0 -58
  171. data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
  172. data/app/frontend/layouts/app-layout.tsx +0 -21
  173. data/app/frontend/layouts/env-layout.tsx +0 -53
  174. data/app/frontend/layouts/persistent-layout.tsx +0 -18
  175. data/app/frontend/lib/cable.ts +0 -10
  176. data/app/frontend/lib/env-pages.ts +0 -236
  177. data/app/frontend/lib/execution-path.ts +0 -55
  178. data/app/frontend/lib/explain.ts +0 -50
  179. data/app/frontend/lib/filter.ts +0 -25
  180. data/app/frontend/lib/flamegraph.ts +0 -155
  181. data/app/frontend/lib/format.ts +0 -71
  182. data/app/frontend/lib/keyboard.ts +0 -18
  183. data/app/frontend/lib/mount.ts +0 -23
  184. data/app/frontend/lib/source-link.ts +0 -217
  185. data/app/frontend/lib/utils.ts +0 -6
  186. data/app/frontend/pages/alerts/index.tsx +0 -171
  187. data/app/frontend/pages/broadcasts/index.tsx +0 -171
  188. data/app/frontend/pages/cache_events/index.tsx +0 -127
  189. data/app/frontend/pages/commands/index.tsx +0 -143
  190. data/app/frontend/pages/deploys/index.tsx +0 -94
  191. data/app/frontend/pages/deploys/show.tsx +0 -309
  192. data/app/frontend/pages/deprecations/index.tsx +0 -176
  193. data/app/frontend/pages/exceptions/index.tsx +0 -271
  194. data/app/frontend/pages/executions/show.tsx +0 -821
  195. data/app/frontend/pages/issues/index.tsx +0 -399
  196. data/app/frontend/pages/issues/show.tsx +0 -1367
  197. data/app/frontend/pages/jobs/index.tsx +0 -381
  198. data/app/frontend/pages/jobs/klass.tsx +0 -215
  199. data/app/frontend/pages/llm_calls/index.tsx +0 -664
  200. data/app/frontend/pages/logs/index.tsx +0 -303
  201. data/app/frontend/pages/mails/index.tsx +0 -218
  202. data/app/frontend/pages/notifications/index.tsx +0 -161
  203. data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
  204. data/app/frontend/pages/overview/show.tsx +0 -333
  205. data/app/frontend/pages/people/index.tsx +0 -102
  206. data/app/frontend/pages/people/show.tsx +0 -149
  207. data/app/frontend/pages/processes/index.tsx +0 -335
  208. data/app/frontend/pages/profiles/index.tsx +0 -110
  209. data/app/frontend/pages/profiles/show.tsx +0 -105
  210. data/app/frontend/pages/queries/index.tsx +0 -492
  211. data/app/frontend/pages/queries/show.tsx +0 -276
  212. data/app/frontend/pages/releases/index.tsx +0 -190
  213. data/app/frontend/pages/releases/release-health.tsx +0 -68
  214. data/app/frontend/pages/releases/release-path.ts +0 -25
  215. data/app/frontend/pages/releases/show.tsx +0 -262
  216. data/app/frontend/pages/requests/index.tsx +0 -235
  217. data/app/frontend/pages/requests/route.tsx +0 -159
  218. data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
  219. data/app/frontend/pages/spans/index.tsx +0 -229
  220. data/app/frontend/pages/spans/show.tsx +0 -220
  221. data/app/frontend/pages/storage_ops/index.tsx +0 -150
  222. data/app/frontend/pages/tenants/index.tsx +0 -236
  223. data/app/frontend/pages/tenants/show.tsx +0 -477
  224. data/app/frontend/pages/tenants/tenant-path.ts +0 -26
  225. data/app/frontend/pages/thresholds/index.tsx +0 -414
  226. data/app/frontend/pages/traces/show.tsx +0 -208
  227. data/app/frontend/pages/transactions/index.tsx +0 -220
  228. data/app/frontend/pages/view_renders/index.tsx +0 -241
  229. data/app/frontend/pages/visits/index.tsx +0 -321
  230. data/app/frontend/public/icon.png +0 -0
  231. data/app/frontend/public/icon.svg +0 -5
  232. data/app/frontend/routes/index.d.ts +0 -2136
  233. data/app/frontend/routes/index.js +0 -2112
  234. data/app/frontend/types/globals.d.ts +0 -9
  235. data/app/frontend/types/index.ts +0 -288
  236. data/app/frontend/types/vite-env.d.ts +0 -9
  237. data/app/helpers/railwatch/assets_helper.rb +0 -52
  238. data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
  239. data/app/jobs/railwatch/application_job.rb +0 -7
  240. data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
  241. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
  242. data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
  243. data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
  244. data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
  245. data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
  246. data/app/jobs/railwatch/performance_scan_job.rb +0 -11
  247. data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
  248. data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
  249. data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
  250. data/app/jobs/railwatch/rollup_job.rb +0 -132
  251. data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
  252. data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
  253. data/app/models/railwatch/alert.rb +0 -229
  254. data/app/models/railwatch/alert_rule.rb +0 -121
  255. data/app/models/railwatch/anomaly_rule.rb +0 -51
  256. data/app/models/railwatch/application.rb +0 -44
  257. data/app/models/railwatch/application_record.rb +0 -33
  258. data/app/models/railwatch/comment.rb +0 -36
  259. data/app/models/railwatch/deploy.rb +0 -67
  260. data/app/models/railwatch/environment.rb +0 -68
  261. data/app/models/railwatch/execution_presenter.rb +0 -185
  262. data/app/models/railwatch/filter_query.rb +0 -184
  263. data/app/models/railwatch/followup_receipt.rb +0 -27
  264. data/app/models/railwatch/ingest/batch.rb +0 -355
  265. data/app/models/railwatch/ingest/mapper.rb +0 -585
  266. data/app/models/railwatch/ingest/payload.rb +0 -96
  267. data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
  268. data/app/models/railwatch/ingest/writer.rb +0 -137
  269. data/app/models/railwatch/issue.rb +0 -277
  270. data/app/models/railwatch/issue_activity.rb +0 -23
  271. data/app/models/railwatch/issue_detection_presenter.rb +0 -309
  272. data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
  273. data/app/models/railwatch/maintenance_task.rb +0 -53
  274. data/app/models/railwatch/saved_view.rb +0 -63
  275. data/app/models/railwatch/telemetry/aggregations.rb +0 -251
  276. data/app/models/railwatch/telemetry/attachment.rb +0 -31
  277. data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
  278. data/app/models/railwatch/telemetry/broadcast.rb +0 -13
  279. data/app/models/railwatch/telemetry/cache_event.rb +0 -16
  280. data/app/models/railwatch/telemetry/child.rb +0 -53
  281. data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
  282. data/app/models/railwatch/telemetry/deprecation.rb +0 -13
  283. data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
  284. data/app/models/railwatch/telemetry/exception.rb +0 -28
  285. data/app/models/railwatch/telemetry/execution.rb +0 -152
  286. data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
  287. data/app/models/railwatch/telemetry/export_destination.rb +0 -66
  288. data/app/models/railwatch/telemetry/health_sample.rb +0 -72
  289. data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
  290. data/app/models/railwatch/telemetry/llm_call.rb +0 -37
  291. data/app/models/railwatch/telemetry/log.rb +0 -91
  292. data/app/models/railwatch/telemetry/mail.rb +0 -13
  293. data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
  294. data/app/models/railwatch/telemetry/notification.rb +0 -13
  295. data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
  296. data/app/models/railwatch/telemetry/person.rb +0 -50
  297. data/app/models/railwatch/telemetry/process.rb +0 -10
  298. data/app/models/railwatch/telemetry/profile.rb +0 -37
  299. data/app/models/railwatch/telemetry/query.rb +0 -57
  300. data/app/models/railwatch/telemetry/query_shape.rb +0 -45
  301. data/app/models/railwatch/telemetry/release_health.rb +0 -69
  302. data/app/models/railwatch/telemetry/rollup.rb +0 -157
  303. data/app/models/railwatch/telemetry/session.rb +0 -24
  304. data/app/models/railwatch/telemetry/span.rb +0 -19
  305. data/app/models/railwatch/telemetry/storage_op.rb +0 -13
  306. data/app/models/railwatch/telemetry/tenant.rb +0 -240
  307. data/app/models/railwatch/telemetry/transaction.rb +0 -13
  308. data/app/models/railwatch/telemetry/view_render.rb +0 -13
  309. data/app/models/railwatch/telemetry/visit.rb +0 -24
  310. data/app/models/railwatch/telemetry_record.rb +0 -107
  311. data/app/models/railwatch/threshold.rb +0 -66
  312. data/app/models/railwatch/user.rb +0 -47
  313. data/app/models/railwatch/viewer.rb +0 -13
  314. data/app/models/railwatch/window.rb +0 -66
  315. data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
  316. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
  317. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
  318. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
  319. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
  320. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
  321. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
  322. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
  323. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
  324. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
  325. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
  326. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
  327. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
  328. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
  329. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
  330. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
  331. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
  332. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
  333. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
  334. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
  335. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
  336. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
  337. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
  338. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
  339. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
  340. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
  341. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
  342. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
  343. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
  344. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
  345. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
  346. data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
  347. data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
  348. data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
  349. data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
  350. data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
  351. data/docs/embedded.md +0 -503
  352. data/lib/puma/plugin/railwatch.rb +0 -215
  353. data/lib/railwatch/dashboard_assets.rb +0 -45
  354. data/lib/railwatch/embedded.rb +0 -50
  355. data/lib/railwatch/export/client.rb +0 -124
  356. data/lib/railwatch/export/lease.rb +0 -56
  357. data/lib/railwatch/export/outbox.rb +0 -361
  358. data/lib/railwatch/export/policy.rb +0 -79
  359. data/lib/railwatch/export/sender.rb +0 -130
  360. data/lib/railwatch/ingest_request_body_limit.rb +0 -10
  361. data/lib/railwatch/maintenance.rb +0 -194
  362. data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
  363. data/lib/railwatch/transport/local.rb +0 -78
  364. data/lib/railwatch/transport/socket.rb +0 -183
  365. data/lib/railwatch/writer.rb +0 -370
  366. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  367. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  368. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  369. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  370. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  371. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  372. data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
  373. data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
  374. data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
  375. data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
  376. data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
  377. data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
  378. data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
  379. data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
  380. data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
  381. data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
  382. data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
  383. data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
  384. data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
  385. data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
  386. data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
  387. data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
  388. data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
  389. data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
  390. data/public/railwatch/assets/index--NQ7us-L.js +0 -1
  391. data/public/railwatch/assets/index-2kgYe_99.js +0 -1
  392. data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
  393. data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
  394. data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
  395. data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
  396. data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
  397. data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
  398. data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
  399. data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
  400. data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
  401. data/public/railwatch/assets/index-C2j50AL2.js +0 -1
  402. data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
  403. data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
  404. data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
  405. data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
  406. data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
  407. data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
  408. data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
  409. data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
  410. data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
  411. data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
  412. data/public/railwatch/assets/index-Deq14BxB.js +0 -1
  413. data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
  414. data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
  415. data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
  416. data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
  417. data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
  418. data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
  419. data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
  420. data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
  421. data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
  422. data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
  423. data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
  424. data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
  425. data/public/railwatch/assets/route-CWh58C7K.js +0 -1
  426. data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
  427. data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
  428. data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
  429. data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
  430. data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
  431. data/public/railwatch/assets/show-BYPB74t4.js +0 -2
  432. data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
  433. data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
  434. data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
  435. data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
  436. data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
  437. data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
  438. data/public/railwatch/assets/show-g__jnN36.js +0 -1
  439. data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
  440. data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
  441. data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
  442. data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
  443. data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
  444. data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
  445. data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
  446. data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
  447. data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
  448. data/public/railwatch/icon.png +0 -0
  449. data/public/railwatch/icon.svg +0 -5
  450. data/public/railwatch/manifest.json +0 -1245
  451. data/public/railwatch/rails-vite.json +0 -1
  452. /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
@@ -1,185 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Everything the execution detail page needs: the parent, its waterfall
5
- # timeline of children, exceptions with frames, logs, and links to the
6
- # enqueuing request or the jobs it enqueued.
7
- class ExecutionPresenter
8
- # The profile page renders up to 2 MiB of collapsed stacks; the execution
9
- # page only carries an inline preview flamegraph alongside everything else
10
- # it already ships, so it takes a smaller slice and links out when cut.
11
- INLINE_COLLAPSED_BYTES = 512.kilobytes
12
-
13
- # Everything an attachment row needs except `data`, which is the whole
14
- # gzipped file and never belongs in a page prop.
15
- ATTACHMENT_COLUMNS = %i[id name content_type bytes truncated occurred_at execution_stage].freeze
16
-
17
- def initialize(execution, environment)
18
- @exe = execution
19
- @environment = environment
20
- end
21
-
22
- def props
23
- @environment.with_telemetry do
24
- {
25
- execution: base,
26
- timeline: timeline,
27
- exceptions: @exe.exceptions.map { |e| exception_row(e) },
28
- logs: @exe.logs.order(:occurred_at).limit(500).map { |l| { id: l.id, level: l.level, message: l.message, tags: l.tags, occurred_at: l.occurred_at, stage: l.execution_stage } },
29
- queries: @exe.queries.order(:occurred_at).limit(500).with_sql.map { |q| { id: q.id, sql: q.sql, duration: q.duration_ms.round(3), source: q.source, group_hash: q.group_hash, row_count: q.row_count, stage: q.execution_stage, offset: offset(q) } },
30
- spans: spans,
31
- profile: profile_card,
32
- attachments: attachment_rows.map { |a| attachment_row(a) },
33
- enqueued_jobs: enqueued_jobs,
34
- parent: parent,
35
- trace: trace,
36
- trace_url: trace_url,
37
- issues: issues,
38
- person: person
39
- }
40
- end
41
- end
42
-
43
- # Public so the issue page can build breadcrumbs from an occurrence's
44
- # execution without re-implementing the per-type mapping.
45
- def timeline_entries
46
- timeline
47
- end
48
-
49
- private
50
-
51
- def base
52
- {
53
- execution_id: @exe.execution_id, kind: @exe.kind, name: @exe.name, duration: @exe.duration_ms.round(3), status: @exe.status,
54
- outcome: @exe.outcome, method: @exe.method, route: @exe.route, controller: @exe.controller, action: @exe.action,
55
- queue: @exe.queue, attempt: @exe.attempt, queue_latency: @exe.queue_latency && (@exe.queue_latency / 1000.0).round(1),
56
- queue_time: @exe.queue_time && (@exe.queue_time / 1000.0).round(1),
57
- job_id: @exe.job_id, task_key: @exe.task_key, inertia_component: @exe.inertia_component,
58
- occurred_at: @exe.occurred_at, deploy: @exe.deploy, server: @exe.server, user_ref: @exe.user_ref, tenant: @exe.app_tenant,
59
- trace_id: @exe.trace_id, allocations: @exe.allocations, peak_memory: @exe.peak_memory, exception_preview: @exe.exception_preview,
60
- profiled: profile.present?,
61
- stages: @exe.stages.transform_values { |v| (v / 1000.0).round(3) }, counters: @exe.counters, detail: @exe.detail
62
- }
63
- end
64
-
65
- def offset(row)
66
- ((row.occurred_at - @exe.occurred_at) * 1000.0).round(3)
67
- end
68
-
69
- # Rich waterfall entries for the Timeline component: unlike the model's
70
- # generic `timeline_label`, this keeps type-specific fields (sql, status,
71
- # subject, level...) so the frontend can render a real hover/detail panel
72
- # instead of a truncated string.
73
- def timeline
74
- entries = []
75
- @exe.queries.with_sql.each { |q| entries << { type: "query", id: q.id, offset: offset(q), duration: q.duration_ms, stage: q.execution_stage,
76
- label: q.sql.to_s.first(120), sql: q.sql, source: q.source, detail: { "rows" => q.row_count, "connection" => q.connection } } }
77
- @exe.cache_events.each { |c| entries << { type: "cache_event", id: c.id, offset: offset(c), duration: c.duration_ms, stage: c.execution_stage,
78
- label: "#{c.type} #{c.key}".first(120), source: nil, detail: { "key" => c.key, "op" => c.type, "store" => c.store, "hits" => c.hits, "ttl" => c.ttl } } }
79
- @exe.outgoing_requests.each { |o| entries << { type: "outgoing_request", id: o.id, offset: offset(o), duration: o.duration_ms, stage: o.execution_stage,
80
- label: o.url.to_s.first(120), source: o.source, detail: { "method" => o.method, "url" => o.url, "status" => o.status_code, "host" => o.host, "error" => o.error, "body" => o.response_body } } }
81
- @exe.mails.each { |m| entries << { type: "mail", id: m.id, offset: offset(m), duration: m.duration_ms, stage: m.execution_stage,
82
- label: m.mailer.to_s.first(120), source: nil, detail: { "subject" => m.subject, "mailer" => m.mailer, "to" => m.to } } }
83
- @exe.logs.each { |l| entries << { type: "log", id: l.id, offset: offset(l), duration: l.duration_ms, stage: l.execution_stage,
84
- label: l.message.to_s.first(120), source: l.source, detail: { "level" => l.level, "message" => l.message.to_s.first(500) } } }
85
- @exe.exceptions.each { |e| entries << { type: "exception", id: e.id, offset: offset(e), duration: e.duration_ms, stage: e.execution_stage,
86
- label: "#{e.class_name}: #{e.message.to_s.first(100)}", source: (e.file ? "#{e.file}:#{e.line}" : nil), detail: { "class" => e.class_name, "message" => e.message.to_s.first(300), "handled" => e.handled } } }
87
- @exe.broadcasts.each { |b| entries << { type: "broadcast", id: b.id, offset: offset(b), duration: b.duration_ms, stage: b.execution_stage,
88
- label: b.channel.to_s.first(120), source: nil, detail: { "channel" => b.channel, "action" => b.action, "stream" => b.stream } } }
89
- @exe.storage_ops.each { |s| entries << { type: "storage_op", id: s.id, offset: offset(s), duration: s.duration_ms, stage: s.execution_stage,
90
- label: "#{s.op} #{s.key}".first(120), source: nil, detail: { "op" => s.op, "key" => s.key, "service" => s.service } } }
91
- @exe.view_renders.each { |v| entries << { type: "view_render", id: v.id, offset: offset(v), duration: v.duration_ms, stage: v.execution_stage,
92
- label: v.identifier.to_s.first(120), source: nil, detail: { "count" => v.count, "cache_hits" => v.cache_hits } } }
93
- @exe.enqueued_jobs.each { |j| entries << { type: "enqueued_job", id: j.id, offset: offset(j), duration: j.duration_ms, stage: j.execution_stage,
94
- label: j.name.to_s.first(120), source: nil, detail: { "queue" => j.queue, "job_id" => j.job_id } } }
95
- @exe.spans.each { |s| entries << { type: "span", id: s.id, offset: offset(s), duration: s.duration_ms, stage: s.execution_stage,
96
- label: s.name.to_s.first(120), source: nil, detail: { "status" => s.status }.merge(s.payload) } }
97
- @exe.transactions.each { |t| entries << { type: "transaction", id: t.id, offset: offset(t), duration: t.duration_ms, stage: t.execution_stage,
98
- label: t.outcome.to_s.first(120), source: nil, detail: { "outcome" => t.outcome, "connection" => t.connection } } }
99
- @exe.llm_calls.each { |c| entries << { type: "llm_call", id: c.id, offset: offset(c), duration: c.duration_ms, stage: c.execution_stage,
100
- label: c.timeline_label.first(120), source: nil, detail: { "operation" => c.operation, "provider" => c.provider, "model" => c.model,
101
- "tool" => c.tool_name, "input_tokens" => c.input_tokens, "output_tokens" => c.output_tokens, "cost" => c.cost,
102
- "status" => c.status, "error" => c.error, "workflow" => c.workflow_name, "step" => c.workflow_step_name } } }
103
- attachment_rows.each { |a| entries << { type: "attachment", id: a.id, offset: offset(a), duration: nil, stage: a.execution_stage,
104
- label: a.name.to_s.first(120), source: nil, detail: { "name" => a.name, "content_type" => a.content_type, "bytes" => a.bytes, "truncated" => a.truncated } } }
105
- entries.each { |e| e[:duration] = e[:duration]&.round(3) }
106
- entries.sort_by { |e| e[:offset] }
107
- end
108
-
109
- def exception_row(e)
110
- { id: e.id, class_name: e.class_name, message: e.message, handled: e.handled, severity: e.severity, source: e.source,
111
- file: e.file, line: e.line, frames: e.frames, cause: e.cause, locals: e.locals, context: e.context, occurred_at: e.occurred_at, group_hash: e.group_hash }
112
- end
113
-
114
- def spans
115
- @exe.spans.order(:occurred_at).limit(200).map do |s|
116
- { id: s.id, name: s.name, duration: s.duration_ms&.round(3), status: s.status, attributes: s.payload,
117
- offset: offset(s), stage: s.execution_stage, occurred_at: s.occurred_at }
118
- end
119
- end
120
-
121
- # The execution's profile, if one was shipped. Looked up by execution_id
122
- # rather than through the denormalised executions.profile_id so a profile
123
- # that arrived in a later batch than its execution still shows up.
124
- def profile
125
- return @profile if defined?(@profile)
126
- @profile = @exe.profiles.recent.first
127
- end
128
-
129
- def profile_card
130
- row = profile or return nil
131
- collapsed, truncated = row.collapsed_capped(INLINE_COLLAPSED_BYTES)
132
- { id: row.id, profiler: row.profiler, mode: row.mode, interval: row.interval, duration: row.duration_ms.round(2),
133
- samples: row.samples, stacks_bytes: row.stacks_bytes, collapsed: collapsed, truncated: truncated }
134
- rescue Telemetry::BoundedGzip::Error
135
- nil
136
- end
137
-
138
- def attachment_rows
139
- @attachment_rows ||= @exe.attachments.order(:occurred_at).limit(50).select(*ATTACHMENT_COLUMNS).to_a
140
- end
141
-
142
- def attachment_row(a)
143
- { id: a.id, name: a.name, content_type: a.content_type, bytes: a.bytes, truncated: a.truncated,
144
- viewable: a.viewable?, occurred_at: a.occurred_at }
145
- end
146
-
147
- def enqueued_jobs
148
- @exe.enqueued_jobs.map do |j|
149
- attempt = Telemetry::Execution.jobs.find_by(job_id: j.job_id)
150
- { id: j.id, name: j.name, queue: j.queue, job_id: j.job_id, scheduled_at: j.scheduled_at, offset: offset(j),
151
- attempt_execution_id: attempt&.execution_id, attempt_outcome: attempt&.outcome }
152
- end
153
- end
154
-
155
- def parent
156
- return nil unless @exe.kind == "job_attempt" && @exe.trace_id
157
- p = Telemetry::Execution.where(trace_id: @exe.trace_id).where.not(execution_id: @exe.execution_id).order(:occurred_at).first
158
- p && { execution_id: p.execution_id, kind: p.kind, name: p.name }
159
- end
160
-
161
- def trace
162
- @trace ||= Telemetry::Execution.where(trace_id: @exe.trace_id).order(:occurred_at).limit(50)
163
- .map { |e| { execution_id: e.execution_id, kind: e.kind, name: e.name, duration: e.duration_ms.round(1), status: e.status, outcome: e.outcome,
164
- occurred_at: e.occurred_at, parent_id: e.parent_id, server: e.server, current: e.execution_id == @exe.execution_id } }
165
- end
166
-
167
- # Only worth linking out to the distributed trace page when this execution
168
- # is not the whole trace.
169
- def trace_url
170
- return nil unless @exe.trace_id && trace.size > 1
171
- Railwatch.url_helpers.application_environment_trace_path(@environment.application, @environment, @exe.trace_id)
172
- end
173
-
174
- def issues
175
- groups = @exe.exceptions.map(&:group_hash).uniq
176
- @environment.issues.where(group_hash: groups).map { |i| { id: i.id, key: i.key, title: i.title, status: i.status, group_hash: i.group_hash } }
177
- end
178
-
179
- def person
180
- return nil unless @exe.user_ref
181
- p = Telemetry::Person.find_by(ref: @exe.user_ref) or return nil
182
- { ref: p.ref, name: p.display_name, email: p.email }
183
- end
184
- end
185
- end
@@ -1,184 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Parses and applies the "key:value key2:value2 free text" grammar shared by
5
- # telemetry pages, REST endpoints, and MCP tools.
6
- module FilterQuery
7
- TOKEN = /\A([a-z_]+):(\S+)\z/
8
- EXECUTION_KINDS = %w[request job scheduled_task command].freeze
9
- LOG_LEVELS = %w[debug info warn error fatal event unknown].freeze
10
- HANDLED_VALUES = %w[true false].freeze
11
-
12
- COMMON_FIELDS = %w[after before user tenant deploy kind].freeze
13
- RESOURCE_FIELDS = {
14
- jobs: %w[class job_id outcome queue status],
15
- exceptions: %w[source class handled severity status],
16
- logs: %w[source level status],
17
- queries: %w[source adapter connection role]
18
- }.freeze
19
-
20
- def self.parse(query)
21
- text = []
22
- fields = {}
23
- query.to_s.split(/\s+/).each do |word|
24
- if (m = TOKEN.match(word))
25
- fields[m[1]] = m[2]
26
- elsif word.present?
27
- text << word
28
- end
29
- end
30
- { text: text.join(" "), fields: fields }
31
- end
32
-
33
- # "5xx" -> 500..599, "404" -> 404..404, anything else -> nil
34
- def self.status_range(value)
35
- case value.to_s
36
- when /\A([2-5])xx\z/i then ($1.to_i * 100)..($1.to_i * 100 + 99)
37
- when /\A\d+\z/ then value.to_i..value.to_i
38
- end
39
- end
40
-
41
- # Narrows grouped route rows (Telemetry::Aggregations.grouped for "request")
42
- # by method:, route:, and status:. Rollup rows carry no raw status codes,
43
- # only aggregated count/errors/client_errors, so status:5xx and friends are
44
- # a family match against those buckets rather than an exact code lookup.
45
- def self.filter_routes(routes, query)
46
- fields = parse(query).fetch(:fields)
47
- routes = routes.select { |r| r[:name].start_with?("#{fields['method'].upcase} ") } if fields["method"].present?
48
- routes = routes.select { |r| r[:name].include?(fields["route"]) } if fields["route"].present?
49
- if (range = status_range(fields["status"]))
50
- routes = routes.select do |r|
51
- case range.begin
52
- when 500..599 then r[:errors].positive?
53
- when 400..499 then r[:client_errors].positive?
54
- else (r[:count] - r[:errors] - r[:client_errors]).positive?
55
- end
56
- end
57
- end
58
- routes
59
- end
60
-
61
- def self.fields_for(resource)
62
- COMMON_FIELDS + RESOURCE_FIELDS.fetch(resource.to_sym)
63
- end
64
-
65
- def self.cursor_context(environment_id:, resource:, query:, window:)
66
- Digest::SHA256.hexdigest([ environment_id, resource, query.to_s, window ].to_json)
67
- end
68
-
69
- # Applies the same telemetry grammar for the web UI, REST API, and MCP.
70
- # The supplied window remains the outer bound; after:/before: may only
71
- # narrow it. Unknown fields are inert, and every value is bound through
72
- # Active Record rather than interpolated into SQL.
73
- def self.apply(scope, resource:, query:, from:, to:, except: [])
74
- resource = resource.to_sym
75
- parsed = parse(query)
76
- fields = parsed[:fields].except(*Array(except).map(&:to_s))
77
- range = bounded_time_range(fields, from, to)
78
- scope = scope.where(occurred_at: range)
79
- scope = apply_common(scope, fields)
80
-
81
- case resource
82
- when :jobs then apply_jobs(scope, fields, parsed[:text], range)
83
- when :exceptions then apply_exceptions(scope, fields, parsed[:text])
84
- when :logs then apply_logs(scope, fields, parsed[:text])
85
- when :queries then apply_queries(scope, fields, parsed[:text])
86
- else raise ArgumentError, "unsupported telemetry resource: #{resource}"
87
- end
88
- end
89
-
90
- def self.bounded_time_range(fields, from, to)
91
- after = parse_time(fields["after"])
92
- before = parse_time(fields["before"])
93
- lower = [ from, after ].compact.max
94
- upper = [ to, before ].compact.min
95
- lower <= upper ? lower..upper : (lower...lower)
96
- end
97
- private_class_method :bounded_time_range
98
-
99
- def self.parse_time(value)
100
- return if value.blank?
101
- Time.zone.parse(value.to_s)
102
- rescue ArgumentError, TypeError
103
- nil
104
- end
105
- private_class_method :parse_time
106
-
107
- def self.apply_common(scope, fields)
108
- scope = scope.where(user_ref: fields["user"]) if fields["user"].present?
109
- scope = scope.where(app_tenant: fields["tenant"]) if fields["tenant"].present?
110
- scope = scope.where(deploy: fields["deploy"]) if fields["deploy"].present?
111
- if fields["source"].present?
112
- if EXECUTION_KINDS.include?(fields["source"].to_s) && scope.klass.column_names.include?("execution_source")
113
- scope = scope.where(execution_source: fields["source"])
114
- elsif scope.klass.column_names.include?("source")
115
- pattern = "#{scope.klass.sanitize_sql_like(fields["source"])}%"
116
- scope = scope.where(scope.klass.arel_table[:source].matches(pattern))
117
- end
118
- end
119
- scope = scope.where(execution_source: fields["kind"]) if EXECUTION_KINDS.include?(fields["kind"].to_s) && scope.klass.column_names.include?("execution_source")
120
- scope
121
- end
122
- private_class_method :apply_common
123
-
124
- def self.apply_jobs(scope, fields, text, range)
125
- outcome = fields["outcome"].presence || fields["status"].presence
126
- scope = scope.where(queue: fields["queue"]) if fields["queue"].present?
127
- scope = scope.where(outcome: outcome) if outcome
128
- # A job's group is the digest of its class name, and group_hash is
129
- # indexed with occurred_at; name is not, so class: alone walked every
130
- # job in the window (49 s for a week through MCP). Not every group
131
- # follows that rule -- Solid Queue's pruned attempts are named
132
- # "(pruned)" under the SolidQueue::Pruned group -- so the window's
133
- # rollups supply any other group recorded under the name, and the last
134
- # FRESH_NAMES, which rollups have not caught up with, match by name.
135
- #
136
- # A class with many attempts in the window is the opposite case: the
137
- # newest page is in the first rows of the walk (2 ms), while the seek
138
- # gathers and sorts every attempt (10 s for 93,000 RollupJobs over 30
139
- # days). The rollups say how many there are, so a common class walks.
140
- if fields["class"].present?
141
- name = fields["class"]
142
- rollups = Telemetry::Rollup.for_type("job_attempt").between(range.begin, range.end).where(name: name)
143
- scope = if rollups.sum(:count) >= Telemetry::Execution::DENSE_MATCHES
144
- scope.where(name: name)
145
- else
146
- groups = [ Record.group_hash(name) ] | rollups.distinct.pluck(:group_hash)
147
- scope.merge(Telemetry::Execution.in_groups_or_fresh("job_attempt", groups, range)).where(name: name)
148
- end
149
- end
150
- scope = scope.where(job_id: fields["job_id"]) if fields["job_id"].present?
151
- return scope unless text.present?
152
- scope.merge(Telemetry::Execution.named_like("job_attempt", text, range, previews: true))
153
- end
154
- private_class_method :apply_jobs
155
-
156
- def self.apply_exceptions(scope, fields, text)
157
- handled = fields["handled"]
158
- handled ||= { "handled" => "true", "unhandled" => "false" }[fields["status"]]
159
- scope = scope.where(class_name: fields["class"]) if fields["class"].present?
160
- scope = scope.where(handled: handled == "true") if HANDLED_VALUES.include?(handled.to_s)
161
- scope = scope.where(severity: fields["severity"]) if fields["severity"].present?
162
- return scope unless text.present?
163
- pattern = "%#{Telemetry::Exception.sanitize_sql_like(text)}%"
164
- scope.where("message LIKE :pattern OR class_name LIKE :pattern", pattern: pattern)
165
- end
166
- private_class_method :apply_exceptions
167
-
168
- def self.apply_logs(scope, fields, text)
169
- level = fields["level"].presence || fields["status"].presence
170
- scope = scope.where(level: level) if LOG_LEVELS.include?(level.to_s)
171
- text.present? ? scope.fts(text) : scope
172
- end
173
- private_class_method :apply_logs
174
-
175
- def self.apply_queries(scope, fields, text)
176
- %w[adapter connection role].each do |field|
177
- scope = scope.where(field => fields[field]) if fields[field].present?
178
- end
179
- return scope unless text.present?
180
- scope.matching(text)
181
- end
182
- private_class_method :apply_queries
183
- end
184
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # One row per (batch, exception group) whose occurrences have been counted
5
- # onto an issue. Lives beside the issues, in the same database and the same
6
- # transaction as the count, so "did this batch's exceptions already reach
7
- # this issue" has exactly one answer however many times the follow-up runs.
8
- class FollowupReceipt < ApplicationRecord
9
- self.table_name = "railwatch_followup_receipts"
10
-
11
- # Receipts outlive their batch's ledger row by this much; a replay after
12
- # that is one the ledger no longer knows about either.
13
- RETENTION = 30.days
14
-
15
- # True when this call is the first for the pair: the caller's work is
16
- # owed. False when a receipt already exists: the work was done, skip it.
17
- # Must run inside a transaction on this database with the work itself.
18
- def self.claim!(batch_id:, group_hash:)
19
- insert({ batch_id: batch_id, group_hash: group_hash, created_at: Time.current },
20
- unique_by: %i[batch_id group_hash]).rows.any?
21
- end
22
-
23
- def self.prune!(now: Time.current)
24
- where(created_at: ...(now - RETENTION)).delete_all
25
- end
26
- end
27
- end