railwatch 0.9.3 → 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 (454) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +37 -33
  3. data/CHANGELOG.md +66 -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 +57 -71
  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/buffer.rb +9 -0
  21. data/lib/railwatch/configuration.rb +21 -151
  22. data/lib/railwatch/engine.rb +9 -118
  23. data/lib/railwatch/patches.rb +0 -11
  24. data/lib/railwatch/reporter.rb +92 -21
  25. data/lib/railwatch/sampler.rb +10 -2
  26. data/lib/railwatch/subscribers/process_info.rb +1 -2
  27. data/lib/railwatch/version.rb +1 -1
  28. data/lib/railwatch.rb +8 -64
  29. data/lib/tasks/railwatch_tasks.rake +3 -325
  30. data/llms.txt +23 -25
  31. metadata +3 -453
  32. data/app/channels/railwatch/environment_channel.rb +0 -62
  33. data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
  34. data/app/controllers/railwatch/alerts_controller.rb +0 -48
  35. data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
  36. data/app/controllers/railwatch/attachments_controller.rb +0 -22
  37. data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
  38. data/app/controllers/railwatch/cache_events_controller.rb +0 -30
  39. data/app/controllers/railwatch/commands_controller.rb +0 -16
  40. data/app/controllers/railwatch/comments_controller.rb +0 -11
  41. data/app/controllers/railwatch/dashboard_controller.rb +0 -70
  42. data/app/controllers/railwatch/deploys_controller.rb +0 -34
  43. data/app/controllers/railwatch/deprecations_controller.rb +0 -54
  44. data/app/controllers/railwatch/environment_scoped.rb +0 -81
  45. data/app/controllers/railwatch/exceptions_controller.rb +0 -52
  46. data/app/controllers/railwatch/executions_controller.rb +0 -13
  47. data/app/controllers/railwatch/issues_controller.rb +0 -258
  48. data/app/controllers/railwatch/jobs_controller.rb +0 -63
  49. data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
  50. data/app/controllers/railwatch/logs_controller.rb +0 -40
  51. data/app/controllers/railwatch/mails_controller.rb +0 -19
  52. data/app/controllers/railwatch/notifications_controller.rb +0 -11
  53. data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
  54. data/app/controllers/railwatch/overview_controller.rb +0 -38
  55. data/app/controllers/railwatch/people_controller.rb +0 -25
  56. data/app/controllers/railwatch/processes_controller.rb +0 -33
  57. data/app/controllers/railwatch/profiles_controller.rb +0 -65
  58. data/app/controllers/railwatch/queries_controller.rb +0 -59
  59. data/app/controllers/railwatch/releases_controller.rb +0 -75
  60. data/app/controllers/railwatch/requests_controller.rb +0 -37
  61. data/app/controllers/railwatch/saved_views_controller.rb +0 -53
  62. data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
  63. data/app/controllers/railwatch/spans_controller.rb +0 -47
  64. data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
  65. data/app/controllers/railwatch/tenants_controller.rb +0 -44
  66. data/app/controllers/railwatch/thresholds_controller.rb +0 -47
  67. data/app/controllers/railwatch/traces_controller.rb +0 -72
  68. data/app/controllers/railwatch/transactions_controller.rb +0 -21
  69. data/app/controllers/railwatch/view_renders_controller.rb +0 -28
  70. data/app/controllers/railwatch/visits_controller.rb +0 -53
  71. data/app/frontend/components/app-content.tsx +0 -26
  72. data/app/frontend/components/app-logo-icon.tsx +0 -23
  73. data/app/frontend/components/app-logo.tsx +0 -14
  74. data/app/frontend/components/app-shell.tsx +0 -38
  75. data/app/frontend/components/app-sidebar-header.tsx +0 -54
  76. data/app/frontend/components/app-sidebar.tsx +0 -94
  77. data/app/frontend/components/app-wordmark.tsx +0 -29
  78. data/app/frontend/components/breadcrumbs.tsx +0 -46
  79. data/app/frontend/components/icon.tsx +0 -16
  80. data/app/frontend/components/input-error.tsx +0 -18
  81. data/app/frontend/components/nav-footer.tsx +0 -46
  82. data/app/frontend/components/nav-main.tsx +0 -41
  83. data/app/frontend/components/nav-user.tsx +0 -50
  84. data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
  85. data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
  86. data/app/frontend/components/railwatch/code.tsx +0 -28
  87. data/app/frontend/components/railwatch/command-palette.tsx +0 -132
  88. data/app/frontend/components/railwatch/copy-block.tsx +0 -52
  89. data/app/frontend/components/railwatch/copy-id.tsx +0 -49
  90. data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
  91. data/app/frontend/components/railwatch/data-table.tsx +0 -195
  92. data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
  93. data/app/frontend/components/railwatch/empty-state.tsx +0 -159
  94. data/app/frontend/components/railwatch/env-nav.tsx +0 -130
  95. data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
  96. data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
  97. data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
  98. data/app/frontend/components/railwatch/frames.tsx +0 -295
  99. data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
  100. data/app/frontend/components/railwatch/live-dot.tsx +0 -78
  101. data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
  102. data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
  103. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
  104. data/app/frontend/components/railwatch/onboarding.tsx +0 -91
  105. data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
  106. data/app/frontend/components/railwatch/page-header.tsx +0 -42
  107. data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
  108. data/app/frontend/components/railwatch/relative-time.tsx +0 -18
  109. data/app/frontend/components/railwatch/saved-views.tsx +0 -232
  110. data/app/frontend/components/railwatch/segmented.tsx +0 -64
  111. data/app/frontend/components/railwatch/series-chart.tsx +0 -400
  112. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
  113. data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
  114. data/app/frontend/components/railwatch/sort-header.tsx +0 -37
  115. data/app/frontend/components/railwatch/source-link.tsx +0 -57
  116. data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
  117. data/app/frontend/components/railwatch/sql-block.tsx +0 -116
  118. data/app/frontend/components/railwatch/stat.tsx +0 -185
  119. data/app/frontend/components/railwatch/status-badge.tsx +0 -79
  120. data/app/frontend/components/railwatch/step-picker.tsx +0 -57
  121. data/app/frontend/components/railwatch/timeline.tsx +0 -405
  122. data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
  123. data/app/frontend/components/railwatch/window-picker.tsx +0 -147
  124. data/app/frontend/components/ui/avatar.tsx +0 -51
  125. data/app/frontend/components/ui/badge.tsx +0 -46
  126. data/app/frontend/components/ui/breadcrumb.tsx +0 -109
  127. data/app/frontend/components/ui/button.tsx +0 -60
  128. data/app/frontend/components/ui/card.tsx +0 -92
  129. data/app/frontend/components/ui/chart.tsx +0 -374
  130. data/app/frontend/components/ui/checkbox.tsx +0 -32
  131. data/app/frontend/components/ui/collapsible.tsx +0 -31
  132. data/app/frontend/components/ui/command.tsx +0 -182
  133. data/app/frontend/components/ui/dialog.tsx +0 -158
  134. data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
  135. data/app/frontend/components/ui/hover-card.tsx +0 -42
  136. data/app/frontend/components/ui/input.tsx +0 -21
  137. data/app/frontend/components/ui/label.tsx +0 -24
  138. data/app/frontend/components/ui/popover.tsx +0 -87
  139. data/app/frontend/components/ui/select.tsx +0 -185
  140. data/app/frontend/components/ui/separator.tsx +0 -28
  141. data/app/frontend/components/ui/sheet.tsx +0 -137
  142. data/app/frontend/components/ui/sidebar.tsx +0 -726
  143. data/app/frontend/components/ui/skeleton.tsx +0 -13
  144. data/app/frontend/components/ui/sonner.tsx +0 -41
  145. data/app/frontend/components/ui/table.tsx +0 -114
  146. data/app/frontend/components/ui/tabs.tsx +0 -91
  147. data/app/frontend/components/ui/textarea.tsx +0 -18
  148. data/app/frontend/components/ui/tooltip.tsx +0 -61
  149. data/app/frontend/components/user-info.tsx +0 -32
  150. data/app/frontend/components/user-menu-content.tsx +0 -94
  151. data/app/frontend/entrypoints/application.css +0 -244
  152. data/app/frontend/entrypoints/inertia.tsx +0 -92
  153. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  154. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  155. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  156. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  157. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  158. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  159. data/app/frontend/hooks/use-appearance.tsx +0 -67
  160. data/app/frontend/hooks/use-clipboard.ts +0 -32
  161. data/app/frontend/hooks/use-flash.tsx +0 -23
  162. data/app/frontend/hooks/use-hotkeys.ts +0 -63
  163. data/app/frontend/hooks/use-initials.tsx +0 -15
  164. data/app/frontend/hooks/use-live.ts +0 -205
  165. data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
  166. data/app/frontend/hooks/use-mobile.ts +0 -21
  167. data/app/frontend/hooks/use-percentile.ts +0 -24
  168. data/app/frontend/hooks/use-reduced-motion.ts +0 -25
  169. data/app/frontend/hooks/use-row-nav.ts +0 -106
  170. data/app/frontend/hooks/use-step.ts +0 -27
  171. data/app/frontend/hooks/use-ticker.ts +0 -12
  172. data/app/frontend/hooks/use-window.ts +0 -58
  173. data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
  174. data/app/frontend/layouts/app-layout.tsx +0 -21
  175. data/app/frontend/layouts/env-layout.tsx +0 -53
  176. data/app/frontend/layouts/persistent-layout.tsx +0 -18
  177. data/app/frontend/lib/cable.ts +0 -10
  178. data/app/frontend/lib/env-pages.ts +0 -236
  179. data/app/frontend/lib/execution-path.ts +0 -55
  180. data/app/frontend/lib/explain.ts +0 -50
  181. data/app/frontend/lib/filter.ts +0 -25
  182. data/app/frontend/lib/flamegraph.ts +0 -155
  183. data/app/frontend/lib/format.ts +0 -71
  184. data/app/frontend/lib/keyboard.ts +0 -18
  185. data/app/frontend/lib/mount.ts +0 -23
  186. data/app/frontend/lib/source-link.ts +0 -217
  187. data/app/frontend/lib/utils.ts +0 -6
  188. data/app/frontend/pages/alerts/index.tsx +0 -171
  189. data/app/frontend/pages/broadcasts/index.tsx +0 -171
  190. data/app/frontend/pages/cache_events/index.tsx +0 -127
  191. data/app/frontend/pages/commands/index.tsx +0 -143
  192. data/app/frontend/pages/deploys/index.tsx +0 -94
  193. data/app/frontend/pages/deploys/show.tsx +0 -309
  194. data/app/frontend/pages/deprecations/index.tsx +0 -176
  195. data/app/frontend/pages/exceptions/index.tsx +0 -271
  196. data/app/frontend/pages/executions/show.tsx +0 -821
  197. data/app/frontend/pages/issues/index.tsx +0 -399
  198. data/app/frontend/pages/issues/show.tsx +0 -1367
  199. data/app/frontend/pages/jobs/index.tsx +0 -381
  200. data/app/frontend/pages/jobs/klass.tsx +0 -215
  201. data/app/frontend/pages/llm_calls/index.tsx +0 -664
  202. data/app/frontend/pages/logs/index.tsx +0 -303
  203. data/app/frontend/pages/mails/index.tsx +0 -218
  204. data/app/frontend/pages/notifications/index.tsx +0 -161
  205. data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
  206. data/app/frontend/pages/overview/show.tsx +0 -333
  207. data/app/frontend/pages/people/index.tsx +0 -102
  208. data/app/frontend/pages/people/show.tsx +0 -149
  209. data/app/frontend/pages/processes/index.tsx +0 -335
  210. data/app/frontend/pages/profiles/index.tsx +0 -110
  211. data/app/frontend/pages/profiles/show.tsx +0 -105
  212. data/app/frontend/pages/queries/index.tsx +0 -492
  213. data/app/frontend/pages/queries/show.tsx +0 -276
  214. data/app/frontend/pages/releases/index.tsx +0 -190
  215. data/app/frontend/pages/releases/release-health.tsx +0 -68
  216. data/app/frontend/pages/releases/release-path.ts +0 -25
  217. data/app/frontend/pages/releases/show.tsx +0 -262
  218. data/app/frontend/pages/requests/index.tsx +0 -235
  219. data/app/frontend/pages/requests/route.tsx +0 -159
  220. data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
  221. data/app/frontend/pages/spans/index.tsx +0 -229
  222. data/app/frontend/pages/spans/show.tsx +0 -220
  223. data/app/frontend/pages/storage_ops/index.tsx +0 -150
  224. data/app/frontend/pages/tenants/index.tsx +0 -236
  225. data/app/frontend/pages/tenants/show.tsx +0 -477
  226. data/app/frontend/pages/tenants/tenant-path.ts +0 -26
  227. data/app/frontend/pages/thresholds/index.tsx +0 -414
  228. data/app/frontend/pages/traces/show.tsx +0 -208
  229. data/app/frontend/pages/transactions/index.tsx +0 -220
  230. data/app/frontend/pages/view_renders/index.tsx +0 -241
  231. data/app/frontend/pages/visits/index.tsx +0 -321
  232. data/app/frontend/public/icon.png +0 -0
  233. data/app/frontend/public/icon.svg +0 -5
  234. data/app/frontend/routes/index.d.ts +0 -2136
  235. data/app/frontend/routes/index.js +0 -2112
  236. data/app/frontend/types/globals.d.ts +0 -9
  237. data/app/frontend/types/index.ts +0 -288
  238. data/app/frontend/types/vite-env.d.ts +0 -9
  239. data/app/helpers/railwatch/assets_helper.rb +0 -52
  240. data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
  241. data/app/jobs/railwatch/application_job.rb +0 -7
  242. data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
  243. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
  244. data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
  245. data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
  246. data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
  247. data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
  248. data/app/jobs/railwatch/performance_scan_job.rb +0 -11
  249. data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
  250. data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
  251. data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
  252. data/app/jobs/railwatch/rollup_job.rb +0 -132
  253. data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
  254. data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
  255. data/app/models/railwatch/alert.rb +0 -229
  256. data/app/models/railwatch/alert_rule.rb +0 -121
  257. data/app/models/railwatch/anomaly_rule.rb +0 -51
  258. data/app/models/railwatch/application.rb +0 -44
  259. data/app/models/railwatch/application_record.rb +0 -33
  260. data/app/models/railwatch/comment.rb +0 -36
  261. data/app/models/railwatch/deploy.rb +0 -67
  262. data/app/models/railwatch/environment.rb +0 -68
  263. data/app/models/railwatch/execution_presenter.rb +0 -185
  264. data/app/models/railwatch/filter_query.rb +0 -184
  265. data/app/models/railwatch/followup_receipt.rb +0 -27
  266. data/app/models/railwatch/ingest/batch.rb +0 -355
  267. data/app/models/railwatch/ingest/mapper.rb +0 -585
  268. data/app/models/railwatch/ingest/payload.rb +0 -96
  269. data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
  270. data/app/models/railwatch/ingest/writer.rb +0 -137
  271. data/app/models/railwatch/issue.rb +0 -277
  272. data/app/models/railwatch/issue_activity.rb +0 -23
  273. data/app/models/railwatch/issue_detection_presenter.rb +0 -309
  274. data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
  275. data/app/models/railwatch/maintenance_task.rb +0 -53
  276. data/app/models/railwatch/saved_view.rb +0 -63
  277. data/app/models/railwatch/telemetry/aggregations.rb +0 -251
  278. data/app/models/railwatch/telemetry/attachment.rb +0 -31
  279. data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
  280. data/app/models/railwatch/telemetry/broadcast.rb +0 -13
  281. data/app/models/railwatch/telemetry/cache_event.rb +0 -16
  282. data/app/models/railwatch/telemetry/child.rb +0 -53
  283. data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
  284. data/app/models/railwatch/telemetry/deprecation.rb +0 -13
  285. data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
  286. data/app/models/railwatch/telemetry/exception.rb +0 -28
  287. data/app/models/railwatch/telemetry/execution.rb +0 -152
  288. data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
  289. data/app/models/railwatch/telemetry/export_destination.rb +0 -66
  290. data/app/models/railwatch/telemetry/health_sample.rb +0 -72
  291. data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
  292. data/app/models/railwatch/telemetry/llm_call.rb +0 -37
  293. data/app/models/railwatch/telemetry/log.rb +0 -91
  294. data/app/models/railwatch/telemetry/mail.rb +0 -13
  295. data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
  296. data/app/models/railwatch/telemetry/notification.rb +0 -13
  297. data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
  298. data/app/models/railwatch/telemetry/person.rb +0 -50
  299. data/app/models/railwatch/telemetry/process.rb +0 -10
  300. data/app/models/railwatch/telemetry/profile.rb +0 -37
  301. data/app/models/railwatch/telemetry/query.rb +0 -57
  302. data/app/models/railwatch/telemetry/query_shape.rb +0 -45
  303. data/app/models/railwatch/telemetry/release_health.rb +0 -69
  304. data/app/models/railwatch/telemetry/rollup.rb +0 -157
  305. data/app/models/railwatch/telemetry/session.rb +0 -24
  306. data/app/models/railwatch/telemetry/span.rb +0 -19
  307. data/app/models/railwatch/telemetry/storage_op.rb +0 -13
  308. data/app/models/railwatch/telemetry/tenant.rb +0 -240
  309. data/app/models/railwatch/telemetry/transaction.rb +0 -13
  310. data/app/models/railwatch/telemetry/view_render.rb +0 -13
  311. data/app/models/railwatch/telemetry/visit.rb +0 -24
  312. data/app/models/railwatch/telemetry_record.rb +0 -107
  313. data/app/models/railwatch/threshold.rb +0 -66
  314. data/app/models/railwatch/user.rb +0 -47
  315. data/app/models/railwatch/viewer.rb +0 -13
  316. data/app/models/railwatch/window.rb +0 -66
  317. data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
  318. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
  319. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
  320. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
  321. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
  322. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
  323. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
  324. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
  325. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
  326. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
  327. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
  328. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
  329. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
  330. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
  331. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
  332. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
  333. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
  334. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
  335. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
  336. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
  337. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
  338. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
  339. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
  340. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
  341. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
  342. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
  343. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
  344. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
  345. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
  346. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
  347. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
  348. data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
  349. data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
  350. data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
  351. data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
  352. data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
  353. data/docs/embedded.md +0 -503
  354. data/lib/puma/plugin/railwatch.rb +0 -215
  355. data/lib/railwatch/dashboard_assets.rb +0 -45
  356. data/lib/railwatch/embedded.rb +0 -50
  357. data/lib/railwatch/export/client.rb +0 -124
  358. data/lib/railwatch/export/lease.rb +0 -56
  359. data/lib/railwatch/export/outbox.rb +0 -361
  360. data/lib/railwatch/export/policy.rb +0 -79
  361. data/lib/railwatch/export/sender.rb +0 -130
  362. data/lib/railwatch/ingest_request_body_limit.rb +0 -10
  363. data/lib/railwatch/maintenance.rb +0 -194
  364. data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
  365. data/lib/railwatch/transport/local.rb +0 -78
  366. data/lib/railwatch/transport/socket.rb +0 -183
  367. data/lib/railwatch/writer.rb +0 -370
  368. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  369. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  370. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  371. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  372. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  373. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  374. data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
  375. data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
  376. data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
  377. data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
  378. data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
  379. data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
  380. data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
  381. data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
  382. data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
  383. data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
  384. data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
  385. data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
  386. data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
  387. data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
  388. data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
  389. data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
  390. data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
  391. data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
  392. data/public/railwatch/assets/index--NQ7us-L.js +0 -1
  393. data/public/railwatch/assets/index-2kgYe_99.js +0 -1
  394. data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
  395. data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
  396. data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
  397. data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
  398. data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
  399. data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
  400. data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
  401. data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
  402. data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
  403. data/public/railwatch/assets/index-C2j50AL2.js +0 -1
  404. data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
  405. data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
  406. data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
  407. data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
  408. data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
  409. data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
  410. data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
  411. data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
  412. data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
  413. data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
  414. data/public/railwatch/assets/index-Deq14BxB.js +0 -1
  415. data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
  416. data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
  417. data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
  418. data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
  419. data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
  420. data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
  421. data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
  422. data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
  423. data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
  424. data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
  425. data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
  426. data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
  427. data/public/railwatch/assets/route-CWh58C7K.js +0 -1
  428. data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
  429. data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
  430. data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
  431. data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
  432. data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
  433. data/public/railwatch/assets/show-BYPB74t4.js +0 -2
  434. data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
  435. data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
  436. data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
  437. data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
  438. data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
  439. data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
  440. data/public/railwatch/assets/show-g__jnN36.js +0 -1
  441. data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
  442. data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
  443. data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
  444. data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
  445. data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
  446. data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
  447. data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
  448. data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
  449. data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
  450. data/public/railwatch/icon.png +0 -0
  451. data/public/railwatch/icon.svg +0 -5
  452. data/public/railwatch/manifest.json +0 -1245
  453. data/public/railwatch/rails-vite.json +0 -1
  454. /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