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
@@ -60,23 +60,18 @@ module Railwatch
60
60
  Rack::QueryParser::ParameterTypeError
61
61
  ].freeze
62
62
 
63
- # What the dashboard controllers inherit from unless the host names its
64
- # own. Also how `dashboard_gate` tells "they wired their own auth" from
65
- # "they left the default".
63
+ # What railwatch-server's dashboard proxy inherits from unless the host
64
+ # names its own.
66
65
  DEFAULT_BASE_CONTROLLER = "ActionController::Base"
67
66
 
68
- attr_accessor :export_enabled, :export_policy, :export_url, :export_token, :export_max_bytes,
69
- :export_max_deliveries, :export_max_age
70
- attr_accessor :enabled, :token, :ingest_url, :allow_http, :server, :environment, :transport,
71
- :issue_prefix, :repository_url, :retention_days, :dashboard_user, :writer_socket,
67
+ attr_accessor :enabled, :token, :ingest_url, :allow_http, :server, :environment,
72
68
  :http_basic_auth_enabled, :http_basic_auth_user, :http_basic_auth_password, :base_controller_class,
73
- :dashboard_open,
74
69
  :sample, :log_level, :capture_request_payload,
75
70
  :capture_exception_source, :capture_exception_locals, :redact_headers, :redact_params,
76
71
  :buffer_size, :buffer_bytes, :execution_buffer_bytes, :batch_bytes,
77
72
  :backpressure,
78
73
  :flush_interval, :flush_threshold,
79
- :connect_timeout, :timeout, :shutdown_timeout, :migration_busy_timeout,
74
+ :connect_timeout, :timeout, :shutdown_timeout,
80
75
  :slow_query_threshold_ms, :n_plus_one_threshold,
81
76
  :max_view_renders_per_execution, :ignored_cache_key_prefixes,
82
77
  :beacon_enabled, :beacon_rate_limit, :beacon_global_rate_limit, :beacon_allowed_origins,
@@ -99,49 +94,24 @@ module Railwatch
99
94
  def initialize
100
95
  @enabled = env_bool("RAILWATCH_ENABLED", true)
101
96
  @token = ENV["RAILWATCH_TOKEN"]
102
- @transport = ENV.fetch("RAILWATCH_TRANSPORT", "http").to_sym
103
- # Embedded dashboard settings; ignored when transport is :http.
104
- @issue_prefix = ENV["RAILWATCH_ISSUE_PREFIX"]
105
- @repository_url = ENV["RAILWATCH_REPOSITORY_URL"]
106
- @retention_days = env_int("RAILWATCH_RETENTION_DAYS", 7)
107
- @dashboard_user = nil
108
- # Mirroring an embedded install's telemetry to a remote receiver. Off
109
- # unless asked for: an embedded install's promise is that nothing leaves
110
- # the machine, and a token being present is not consent.
111
- @export_enabled = env_bool("RAILWATCH_EXPORT_ENABLED", false)
112
- @export_policy = ENV.fetch("RAILWATCH_EXPORT_POLICY", "everything").to_sym
113
- @export_url = ENV["RAILWATCH_EXPORT_URL"]
114
- @export_token = ENV["RAILWATCH_EXPORT_TOKEN"]
115
- @export_max_bytes = env_int("RAILWATCH_EXPORT_MAX_BYTES", 256 * 1024 * 1024)
116
- @export_max_deliveries = env_int("RAILWATCH_EXPORT_MAX_DELIVERIES", 100_000)
117
- @export_max_age = env_int("RAILWATCH_EXPORT_MAX_AGE_SECONDS", 86_400)
118
- # Dashboard access, the way Mission Control Jobs does it: HTTP Basic
97
+ # The gate in front of the dashboard railwatch-server serves at
98
+ # /railwatch, the way Mission Control Jobs does it: HTTP Basic
119
99
  # authentication is on and CLOSED by default. With no user and password
120
100
  # configured every dashboard request is 401, so an install that forgot
121
101
  # to set anything up is never a public page. Credentials come from
122
102
  # Rails credentials (railwatch.http_basic_auth_user/_password, written
123
103
  # by `bin/rails railwatch:authentication:configure`), from these env
124
104
  # vars, or by assignment here. A host with its own admin auth turns
125
- # Basic off and names a base controller, or wraps the mount in a
126
- # routes constraint.
105
+ # Basic off and names a base controller. Nothing here reads them unless
106
+ # railwatch-server is installed.
127
107
  @http_basic_auth_enabled = env_bool("RAILWATCH_HTTP_BASIC_AUTH_ENABLED", true)
128
108
  @http_basic_auth_user = ENV["RAILWATCH_HTTP_BASIC_AUTH_USER"]
129
109
  @http_basic_auth_password = ENV["RAILWATCH_HTTP_BASIC_AUTH_PASSWORD"]
130
- # Only consulted when HTTP Basic is off: the host saying, in as many
131
- # words, "I accept that anyone who can reach this URL can read it".
132
- # A private network or a VPN is a real answer; this is how you say so,
133
- # and it is what silences the boot warning and opens the live channel.
134
- @dashboard_open = env_bool("RAILWATCH_DASHBOARD_OPEN", false)
135
- # The dashboard controllers inherit from this class, so a host's own
136
- # before_actions (require an admin, redirect to sign-in) run in front of
137
- # every page. Default is the engine's own base, which authenticates
138
- # nothing itself beyond HTTP Basic above.
110
+ # railwatch-server's dashboard proxy inherits from this class, so a
111
+ # host's own before_actions (require an admin, redirect to sign-in) run
112
+ # in front of every page. The default authenticates nothing itself
113
+ # beyond HTTP Basic above.
139
114
  @base_controller_class = ENV.fetch("RAILWATCH_BASE_CONTROLLER_CLASS", DEFAULT_BASE_CONTROLLER)
140
- # Embedded mode's writer process (lib/railwatch/writer.rb): the Unix
141
- # socket the Puma workers hand their batches to. Relative paths are
142
- # under Rails.root. nil disables the writer and every process writes
143
- # its own batches, as before.
144
- @writer_socket = ENV.fetch("RAILWATCH_WRITER_SOCKET", "tmp/sockets/railwatch-writer.sock")
145
115
  @ingest_url = ENV.fetch("RAILWATCH_INGEST_URL", "https://railwatch.rebulk.com")
146
116
  @allow_http = env_bool("RAILWATCH_ALLOW_HTTP", false)
147
117
  @project_root = defined?(Rails) ? Rails.root : Dir.pwd
@@ -187,13 +157,6 @@ module Railwatch
187
157
  @connect_timeout = env_float("RAILWATCH_CONNECT_TIMEOUT", 1.0)
188
158
  @timeout = env_float("RAILWATCH_TIMEOUT", 3.0)
189
159
  @shutdown_timeout = env_float("RAILWATCH_SHUTDOWN_TIMEOUT", 2.0)
190
- # Seconds a migration of the railwatch databases waits for SQLite's
191
- # write lock (Patches::MigrationBusyTimeout). A deploy migrates while
192
- # the old release's writer is still committing to the same file; the
193
- # database's own `timeout` (5 s) is too short to get between its
194
- # transactions when the disk is busy. Kept under a typical health-check
195
- # window (Kamal's deploy_timeout defaults to 30 s and is often 90 s).
196
- @migration_busy_timeout = env_float("RAILWATCH_MIGRATION_BUSY_TIMEOUT", 60.0)
197
160
  @slow_query_threshold_ms = env_float("RAILWATCH_SLOW_QUERY_MS", 5.0)
198
161
  @n_plus_one_threshold = env_int("RAILWATCH_N_PLUS_ONE_THRESHOLD", 5)
199
162
  @max_view_renders_per_execution = 20
@@ -318,67 +281,13 @@ module Railwatch
318
281
  end
319
282
 
320
283
  def enabled?
321
- @enabled && (local? || token.present?)
284
+ @enabled && token.present?
322
285
  end
323
286
 
324
- # :local writes telemetry into the engine's own database in-process;
325
- # anything else ships it to ingest_url over HTTPS.
326
- def local? = transport.to_s == "local"
327
-
328
- # The receiver admits an unseen delivery for seven days; queueing one for
329
- # longer cannot help.
330
- MAX_EXPORT_AGE = 7 * 24 * 60 * 60
331
-
332
- # Mirroring is a thing an embedded install opts into; it is meaningless
333
- # for an install that is already sending everything over HTTP.
334
- def export? = export_enabled && local? && export_problem.nil?
335
-
336
- # Why export is configured but unusable, or nil when it is fine. The
337
- # doctor reports this; nothing silently half-enables.
338
- def export_problem
339
- return nil unless export_enabled
340
- return "export needs transport :local; an :http install already sends everything" unless local?
341
- return "RAILWATCH_EXPORT_POLICY #{export_policy} is not implemented" unless export_policy.to_s == "everything"
342
- return "no export token: set RAILWATCH_EXPORT_TOKEN or RAILWATCH_TOKEN" if resolved_export_token.to_s.empty?
343
- return "no export url: set RAILWATCH_EXPORT_URL or RAILWATCH_INGEST_URL" if resolved_export_url.to_s.empty?
344
- return "export url must be HTTPS (or set RAILWATCH_ALLOW_HTTP=true)" unless url_allowed?(resolved_export_url)
345
- # Past this a receiver stops recognising a delivery's id, so holding one
346
- # any longer just means discovering later that it can never be sent.
347
- if export_max_age > MAX_EXPORT_AGE
348
- return "RAILWATCH_EXPORT_MAX_AGE_SECONDS cannot exceed #{MAX_EXPORT_AGE} (the receiver stops recognising a delivery past that)"
349
- end
350
-
351
- nil
352
- end
353
-
354
- # The receiver, and the credential we are bound to it with. Both fall back
355
- # to the ordinary ingest settings so switching an install from embedded to
356
- # cloud needs no second set of values.
357
- def resolved_export_url
358
- url = export_url.presence || (ingest_url.presence && URI.join(ingest_url, "/ingest").to_s)
359
- url&.sub(%r{/\z}, "")
360
- rescue URI::Error
361
- # BadURIError (a relative ingest_url) is not an InvalidURIError, and
362
- # letting it out of here takes the doctor down with it.
363
- nil
364
- end
365
-
366
- def resolved_export_token = export_token.presence || token
367
-
368
- # Absolute path of the writer socket, or nil when the writer is off.
369
- def writer_socket_path
370
- path = writer_socket.to_s
371
- return nil if path.empty?
372
-
373
- root = defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? Rails.root.to_s : Dir.pwd
374
- File.expand_path(path, root)
375
- end
287
+ # Always false: there is no embedded transport any more. Only
288
+ # railwatch-server 0.1.0 calls it.
289
+ def local? = false
376
290
 
377
- # Who the embedded dashboard shows as the signed-in operator. A host
378
- # passes a lambda taking the request (cookies, warden, whatever it uses)
379
- # and returning a User, {id:, name:, email:} or nil. Authentication and
380
- # authorisation stay the host's job: put the mount behind its own
381
- # constraint. This only names the person for comments and saved views.
382
291
  # Both halves present. Read late (not at boot) so credentials set from an
383
292
  # initializer, an env var or the configure task all count.
384
293
  def http_basic_auth_configured?
@@ -386,17 +295,17 @@ module Railwatch
386
295
  end
387
296
 
388
297
  # Development with Basic on and nothing configured is open, so a first
389
- # run is `rails g railwatch:install` and a page, not a password step
390
- # first. Rails already shows full error pages there. Every other
391
- # environment stays closed until credentials exist.
298
+ # run is a page, not a password step first. Rails already shows full
299
+ # error pages there. Every other environment stays closed until
300
+ # credentials exist.
392
301
  def http_basic_auth_waived?
393
302
  http_basic_auth_enabled && !http_basic_auth_configured? && defined?(Rails) && Rails.env.development?
394
303
  end
395
304
 
396
305
  # Whether the request carries the configured HTTP Basic credentials.
397
306
  # False when Basic is on and nothing is configured (closed), true when
398
- # Basic is off (the host's base controller or routes constraint is the
399
- # gate then). Shared by the dashboard controller and the live channel.
307
+ # Basic is off (the host's base controller is the gate then).
308
+ # railwatch-server's dashboard proxy calls it.
400
309
  def http_basic_auth_ok?(request)
401
310
  return true unless http_basic_auth_enabled
402
311
  return true if http_basic_auth_waived?
@@ -408,45 +317,6 @@ module Railwatch
408
317
  end == true
409
318
  end
410
319
 
411
- # Which gate is in force, for the doctor, the boot warning and the live
412
- # channel. :basic when HTTP Basic is on (closed until credentials exist),
413
- # :controller when the host named its own base controller, :resolver when
414
- # it gave a dashboard_user that can refuse, :open when it said the
415
- # dashboard is deliberately public, and :undeclared when Basic is off and
416
- # none of those is true -- which usually means a routes constraint the
417
- # gem cannot see, and might mean nothing at all.
418
- def dashboard_gate
419
- return :basic if http_basic_auth_enabled
420
- return :controller unless base_controller_class == DEFAULT_BASE_CONTROLLER
421
- return :resolver if dashboard_user
422
- return :open if dashboard_open
423
-
424
- :undeclared
425
- end
426
-
427
- # Whether a live-update subscription is allowed. Action Cable runs on the
428
- # host's own /cable endpoint, which a routes constraint around the
429
- # engine's mount does not cover and a base controller cannot reach, so an
430
- # undeclared gate refuses rather than assuming.
431
- def dashboard_channel_allowed?(request)
432
- return http_basic_auth_ok?(request) if http_basic_auth_enabled
433
- if dashboard_user
434
- return !!(dashboard_user.respond_to?(:call) ? dashboard_user.call(request) : dashboard_user)
435
- end
436
-
437
- # A controller name is not evidence of authorization on /cable.
438
- !!dashboard_open
439
- end
440
-
441
- def resolve_dashboard_user(request)
442
- resolved = dashboard_user.respond_to?(:call) ? dashboard_user.call(request) : dashboard_user
443
- case resolved
444
- when User then resolved
445
- when Hash then User.new(id: resolved[:id] || User::ID, name: resolved[:name].to_s.presence || "Operator", email: resolved[:email])
446
- else User.default
447
- end
448
- end
449
-
450
320
  def ingest_url_allowed?
451
321
  url_allowed?(ingest_url)
452
322
  end
@@ -1,88 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The dashboard controllers are Inertia controllers; a host that does not use
4
- # Inertia itself never requires the gem, so the engine does.
5
- require "inertia_rails"
6
- # The engine's jobs (grouping, rollups, scans) are Active Job classes even
7
- # though embedded mode calls their perform directly, so a host built with
8
- # `rails new --minimal` (no Active Job) still needs the framework loaded.
9
- # Part of the rails gem this one depends on, so nothing new is pulled in.
10
- require "active_job/railtie"
11
-
12
3
  module Railwatch
13
- # isolate_namespace would give every model a railwatch_ prefix. Only the
14
- # engine's own records (issues, comments, deploys: Railwatch::ApplicationRecord)
15
- # carry it; the telemetry tables are the hosted platform's schema and stay
16
- # unprefixed. Defined before isolate_namespace runs, which only adds a
17
- # prefix when none is set.
18
- def self.table_name_prefix = ""
19
-
4
+ # Mounted for the browser beacon (POST <mount>/beacon), its only route.
20
5
  class Engine < ::Rails::Engine
21
6
  isolate_namespace Railwatch
22
7
 
23
- config.railwatch = ActiveSupport::OrderedOptions.new
24
-
25
- # The dashboard bundle is built into the gem at release time; serve it
26
- # from here so a host app needs no asset pipeline integration at all.
27
- # Where it goes in the stack is DashboardAssets.install!'s decision.
28
- initializer "railwatch.dashboard_assets" do |app|
29
- Railwatch::DashboardAssets.install!(app, root: root.join("public/railwatch").to_s)
30
- end
31
-
32
- # Action Cable is optional: live dashboard updates need it, nothing else
33
- # does. A host without it (`rails new --minimal`, --skip-action-cable)
34
- # must not eager-load the engine's channel, which inherits from a class
35
- # that does not exist there.
36
- initializer "railwatch.channels", before: :setup_main_autoloader do
37
- Rails.autoloaders.main.ignore(root.join("app/channels")) unless defined?(::ActionCable)
38
- end
39
-
40
- # The dashboard bundle subscribes to "EnvironmentChannel" by name; Action
41
- # Cable constantizes that, so the engine's channel needs the bare name.
42
- # Only defined when the host has not got one of its own.
43
- initializer "railwatch.live_channel" do
44
- next unless defined?(::ActionCable)
45
-
46
- # Action Cable constantizes the subscription's channel name at
47
- # subscribe time; the alias only has to exist by then, and it must
48
- # not clobber a host channel of the same name.
49
- config.to_prepare do
50
- Object.const_set(:EnvironmentChannel, Railwatch::EnvironmentChannel) unless Object.const_defined?(:EnvironmentChannel)
51
- end
52
- end
53
-
54
8
  # The request middleware goes first so wall time includes every other
55
9
  # middleware, exactly like Nightwatch's GlobalMiddleware.
56
10
  initializer "railwatch.middleware", before: :load_config_initializers do |app|
57
11
  app.middleware.insert_before 0, Railwatch::Middleware::Request
58
12
  end
59
13
 
60
- # HTTP installs have no embedded databases or job backend. Loading their
61
- # models still registers them with Active Record, whose schema-cache boot
62
- # hook asks every descendant for its connection_pool (e.g. with
63
- # activerecord-tenanted). Decide after the host's initializer selects the
64
- # transport. Keep autoloading available for the local installer and tools;
65
- # only local installs should eagerly load the embedded models and jobs.
66
- # Dashboard controllers also reference model constants in their class
67
- # bodies. The beacon is the only controller a cloud install needs.
68
- initializer "railwatch.embedded_eager_loading", after: :load_config_initializers, before: :setup_main_autoloader do
69
- next if Railwatch.config.local?
70
-
71
- Rails.autoloaders.main.do_not_eager_load(root.join("app/models"))
72
- Rails.autoloaders.main.do_not_eager_load(root.join("app/jobs"))
73
- root.glob("app/controllers/railwatch/*.rb").each do |path|
74
- Rails.autoloaders.main.do_not_eager_load(path) unless path.basename.to_s == "beacon_controller.rb"
75
- end
76
- end
77
-
78
14
  # Before anything subscribes or starts a thread, and after the app's own
79
15
  # initializer has had its say about config: a console captures nothing.
80
16
  initializer "railwatch.console", after: :load_config_initializers, before: "railwatch.subscribe" do
81
17
  Railwatch::Console.silence!
82
18
  end
83
19
 
84
- # Same source Mission Control Jobs reads: railwatch.http_basic_auth_user
85
- # and _password in Rails credentials, which `bin/rails
20
+ # The dashboard gate railwatch-server's proxy applies. Same source
21
+ # Mission Control Jobs reads: railwatch.http_basic_auth_user and
22
+ # _password in Rails credentials, which `bin/rails
86
23
  # railwatch:authentication:configure` writes. An initializer or env var
87
24
  # that already set them wins.
88
25
  initializer "railwatch.http_basic_auth", after: :load_config_initializers do |app|
@@ -91,49 +28,26 @@ module Railwatch
91
28
  config.http_basic_auth_password ||= app.credentials.dig(:railwatch, :http_basic_auth_password)
92
29
  end
93
30
 
94
- # Three things worth one line in the log at boot, because all are
95
- # invisible until something is already wrong: a Rails/json pair that
96
- # cannot decode, an embedded dashboard with nothing declared in front of
97
- # it, and one that is closed to everyone because Basic has no
98
- # credentials outside development.
31
+ # Worth one line in the log at boot, because it is invisible until
32
+ # something is already wrong: a Rails/json pair that cannot decode.
99
33
  initializer "railwatch.warnings", after: :load_config_initializers do
100
34
  config.after_initialize do
101
35
  next unless Railwatch.enabled?
102
36
 
103
37
  Rails.logger.warn("[railwatch] #{Railwatch::JsonCompat.advice}") if Railwatch::JsonCompat.broken?
104
-
105
- if Railwatch.config.local? && Railwatch.config.dashboard_gate == :basic &&
106
- !Railwatch.config.http_basic_auth_configured? && !Rails.env.local?
107
- Rails.logger.warn(
108
- "[railwatch] the dashboard is closed: HTTP Basic is on and no credentials are configured for " \
109
- "#{Rails.env}, so every request to it is 401. Run `RAILS_ENV=#{Rails.env} bin/rails " \
110
- "railwatch:authentication:configure`, or gate it with your own auth and set " \
111
- "`c.http_basic_auth_enabled = false` (docs/embedded.md)."
112
- )
113
- end
114
-
115
- if Railwatch.config.local? && Railwatch.config.dashboard_gate == :undeclared && !Rails.env.local?
116
- Rails.logger.warn(
117
- "[railwatch] the dashboard at the engine's mount has no gate this gem can see: HTTP Basic is off and " \
118
- "no base_controller_class, dashboard_user or dashboard_open is set. If a routes constraint or your " \
119
- "network already gates it, authorize /cable separately with `c.dashboard_user`, or set `c.dashboard_open = true` " \
120
- "only when the network protects both endpoints. Otherwise anyone who can reach the dashboard can read every query, log line and exception this app records."
121
- )
122
- end
123
38
  end
124
39
  end
125
40
 
126
41
  # A rake process starts its command execution before this point -- the
127
42
  # top-level task is invoked before its `:environment` prerequisite boots
128
- # Rails -- so a reporter can already exist, built from a config that had
129
- # not yet been told the app is embedded. This is the first moment the
130
- # app's own configuration is final.
43
+ # Rails -- so a reporter can already exist, built from a config that was
44
+ # not final. This is the first moment the app's own configuration is.
131
45
  initializer "railwatch.adopt_final_config", after: :load_config_initializers do
132
46
  Railwatch.adopt_final_config!
133
47
  end
134
48
 
135
49
  initializer "railwatch.transport_security", after: :load_config_initializers do
136
- next if Railwatch.config.local? || Railwatch.config.ingest_url_allowed?
50
+ next if Railwatch.config.ingest_url_allowed?
137
51
 
138
52
  Rails.logger.warn(
139
53
  "Railwatch will not send telemetry to #{Railwatch.config.ingest_url}: plain HTTP is allowed only for loopback " \
@@ -243,28 +157,6 @@ module Railwatch
243
157
  at_exit { Railwatch::Sessions.stop! }
244
158
  end
245
159
 
246
- # The embedded install's maintenance clock (release health, scans,
247
- # pruning). Same shape and ordering rationale as "railwatch.health":
248
- # stopped before the reporter's final flush. Maintenance.start! is a
249
- # no-op unless the transport is local.
250
- initializer "railwatch.maintenance" do
251
- next unless Railwatch.enabled?
252
-
253
- Railwatch::Maintenance.start!
254
- at_exit { Railwatch::Maintenance.stop! }
255
- end
256
-
257
- # Draining the export queue. Same shape and ordering as the others:
258
- # stopped before the reporter's final flush, so its last claim is
259
- # released rather than left to expire. Sender.start! is a no-op unless
260
- # export is configured and usable.
261
- initializer "railwatch.export" do
262
- next unless Railwatch.enabled?
263
-
264
- Railwatch::Export::Sender.start!
265
- at_exit { Railwatch::Export::Sender.stop! }
266
- end
267
-
268
160
  # lib/tasks/railwatch_tasks.rake is picked up by Rails::Engine's default
269
161
  # lib/tasks convention; the rake_tasks block above only installs the
270
162
  # Rake::Task patch.
@@ -274,7 +166,6 @@ end
274
166
  require "railwatch/console"
275
167
  require "railwatch/health"
276
168
  require "railwatch/sessions"
277
- require "railwatch/maintenance"
278
169
  require "railwatch/middleware/request"
279
170
  require "railwatch/job_tracing"
280
171
  require "railwatch/controller_helpers"
@@ -4,7 +4,6 @@ require "railwatch/patches/net_http"
4
4
  require "railwatch/patches/rake_task"
5
5
  require "railwatch/patches/runner_command"
6
6
  require "railwatch/patches/inertia"
7
- require "railwatch/patches/migration_busy_timeout"
8
7
 
9
8
  module Railwatch
10
9
  module Patches
@@ -23,16 +22,6 @@ module Railwatch
23
22
  # From Rails::Engine#load_tasks, which has already required rake.
24
23
  def install_rake_task!
25
24
  ::Rake::Task.prepend(RakeTask) unless ::Rake::Task.ancestors.include?(RakeTask)
26
- install_migration_busy_timeout!
27
- end
28
-
29
- # db:prepare and db:migrate run as rake tasks, so this goes in with the
30
- # rake patch. Unlike it, this one is not gated on Railwatch.enabled?: a
31
- # container entrypoint migrates with Railwatch switched off, and that is
32
- # exactly the migration that has to wait out the old release's writer.
33
- def install_migration_busy_timeout!
34
- tasks = ::ActiveRecord::Tasks::DatabaseTasks.singleton_class
35
- tasks.prepend(MigrationBusyTimeout) unless tasks.ancestors.include?(MigrationBusyTimeout)
36
25
  end
37
26
 
38
27
  # From Rails::Application#load_runner, which RunnerCommand#perform calls
@@ -13,7 +13,8 @@ module Railwatch
13
13
  # counted) so the buffer's newest records win again. Without the cap a
14
14
  # batch that keeps failing would be pinned forever while everything
15
15
  # newer was discarded around it. Eight attempts on the backoff ladder is
16
- # roughly four minutes of outage.
16
+ # roughly four minutes of outage; one to two when every attempt is
17
+ # refused with a Retry-After (MAX_REFUSED_RETRY_DELAY).
17
18
  MAX_RETRY_ATTEMPTS = 8
18
19
  # An unhandled exception asks for an immediate flush so it reaches the
19
20
  # platform without waiting out flush_interval. "Immediate" is this many
@@ -26,7 +27,23 @@ module Railwatch
26
27
  # is enough to turn a saturated stream into breathing room without the
27
28
  # long recovery and sparse telemetry a 16x cap would impose.
28
29
  MAX_BACKPRESSURE_FACTOR = 8.0
30
+ # A receiver that answers with a Retry-After (503 "disk almost full" or
31
+ # "database busy", 429 "rate limit exceeded") is refusing, not failing.
32
+ # The next attempt waits for its Retry-After instead of the ladder, up
33
+ # to this long. Railwatch's receivers send 15 s for a busy database,
34
+ # honoured as given, and 60 s for the disk floor and the rate-limit
35
+ # window, which are not forecasts: the floor is re-checked every 10 s
36
+ # and lifts the moment space is back, and a window can end at any time.
37
+ # The ladder alone was worse than either: a minute into a refusal its
38
+ # rungs were 16-60 s apart, and a recovered app stayed sampled down
39
+ # until the next one. A longer wait is cut to between half and all of
40
+ # this, jittered so processes refused together do not return together:
41
+ # one POST per process every 7.5-15 s.
42
+ MAX_REFUSED_RETRY_DELAY = 15.0
29
43
  DeliveryBatch = Data.define(:id, :records, :bytes, :dropped, :dropped_bytes, :prepared)
44
+ # From the first refusal to the next accepted delivery: when it began,
45
+ # what the receiver said, and the loss counters as they stood then.
46
+ Refusal = Struct.new(:started_at, :reason, :overflowed, :sampled_out, :gave_up)
30
47
 
31
48
  class DeliveryError < StandardError
32
49
  attr_reader :status, :records, :bytes, :dropped, :dropped_bytes
@@ -66,6 +83,8 @@ module Railwatch
66
83
  @in_flight_bytes = 0
67
84
  @in_flight_dropped_bytes = 0
68
85
  @shutdown_notified = false
86
+ @refusal = nil
87
+ @sampled_out = 0
69
88
  end
70
89
 
71
90
  def buffer
@@ -75,6 +94,13 @@ module Railwatch
75
94
 
76
95
  attr_reader :backpressure_factor
77
96
 
97
+ # The sampler calls this for an execution the configured rate kept and
98
+ # backpressure did not. Only reached while the factor is above one.
99
+ def note_sampled_out
100
+ ensure_process!
101
+ @mutex.synchronize { @sampled_out += 1 }
102
+ end
103
+
78
104
  def write(record, bytes = nil)
79
105
  ensure_process!
80
106
  size = @buffer.push(record, bytes)
@@ -146,6 +172,8 @@ module Railwatch
146
172
  @in_flight_bytes = 0
147
173
  @in_flight_dropped_bytes = 0
148
174
  @shutdown_notified = false
175
+ @refusal = nil
176
+ @sampled_out = 0
149
177
  remove_instance_variable(:@shutdown_deadline) if defined?(@shutdown_deadline)
150
178
  self
151
179
  end
@@ -155,20 +183,11 @@ module Railwatch
155
183
  # where the command patch starts the execution, and sampling that
156
184
  # execution builds the reporter -- before the task's `:environment`
157
185
  # prerequisite boots Rails and runs config/initializers. A token in the
158
- # environment is enough for Railwatch to be enabled that early, so an
159
- # embedded app gets a reporter aimed at HTTP, chosen from a config that
160
- # had not yet been told the app is embedded. Left alone it stays aimed
161
- # there: the memo outlives the configuration that produced it, and every
162
- # record from that process goes to the receiver instead of the app's own
163
- # database, skipping the export queue and the replay receipts it earns.
186
+ # environment is enough for Railwatch to be enabled that early.
164
187
  #
165
188
  # Nothing has been recorded at that point -- starting an execution
166
189
  # decides sampling and pushes nothing -- so deciding again here is free.
167
190
  def adopt_final_config!
168
- # The swap forked_transport already makes, for the same reason: the
169
- # situation this transport was chosen for is not the situation now.
170
- reselected = Railwatch.local_transport if @config.local?
171
- @transport = reselected if reselected && reselected.class != @transport.class
172
191
  # Sized from the config as it stood, which was the default one. Safe
173
192
  # only while the buffer is still empty: resizing means a new buffer,
174
193
  # and anything already in the old one would go with it.
@@ -220,12 +239,6 @@ module Railwatch
220
239
  end
221
240
 
222
241
  def forked_transport
223
- # A child may be a different kind of process from its parent: the
224
- # writer forked from a Puma master must write SQLite itself, not hand
225
- # batches back to the socket it is about to serve.
226
- reselected = Railwatch.local_transport if @config.local?
227
- return reselected if reselected && reselected.class != @transport.class
228
-
229
242
  transport = @transport.dup
230
243
  transport.reset_after_fork! if transport.respond_to?(:reset_after_fork!)
231
244
  transport
@@ -348,6 +361,7 @@ module Railwatch
348
361
  # raise here and be retained as if it had failed.
349
362
  Railwatch.debug { "ingest rejected #{result.rejected} of #{batch.records.size} records" } if result.rejected.to_i.positive?
350
363
  delivery_succeeded
364
+ refusal_ended if @refusal
351
365
  elsif retryable?(result)
352
366
  retain(batch, result)
353
367
  else
@@ -370,22 +384,33 @@ module Railwatch
370
384
  end
371
385
 
372
386
  def retain(batch, result)
387
+ refused_until = result.retry_after_at if result.respond_to?(:retry_after_at)
388
+ refusal = nil
373
389
  gave_up = @mutex.synchronize do
374
390
  @in_flight_records = 0
375
391
  @in_flight_dropped = 0
376
392
  @in_flight_bytes = 0
377
393
  @in_flight_dropped_bytes = 0
394
+ if refused_until && !@refusal
395
+ refusal = @refusal = Refusal.new(started_at: Clock.monotonic, reason: failure_detail(result),
396
+ overflowed: @buffer.overflowed, sampled_out: @sampled_out, gave_up: 0)
397
+ # Nothing leaves this buffer until the receiver accepts again, and
398
+ # with retries now seconds apart, so are the flush ticks that would
399
+ # otherwise double the factor up to here. They hold it there.
400
+ @backpressure_factor = MAX_BACKPRESSURE_FACTOR if @config.backpressure
401
+ end
378
402
  @retry_attempt += 1
379
403
  if @retry_attempt > MAX_RETRY_ATTEMPTS
380
404
  @buffer.account_dropped(batch.records.size + batch.dropped,
381
405
  bytes: batch.bytes + batch.dropped_bytes)
406
+ @refusal.gave_up += batch.records.size if @refusal
382
407
  @retry_attempt = 0
383
408
  @retry_at = nil
384
409
  Railwatch.debug { "gave up on a batch of #{batch.records.size} records after #{MAX_RETRY_ATTEMPTS} retries (#{result.error || result.status}); dropped and counted" }
385
410
  next true
386
411
  end
387
412
  @retry_batch = batch
388
- delay = retry_delay(@retry_attempt)
413
+ delay = refused_until ? refused_delay(refused_until) : retry_delay(@retry_attempt)
389
414
  @retry_at = Clock.monotonic + delay
390
415
  @wakeup.signal
391
416
  Railwatch.debug do
@@ -394,11 +419,21 @@ module Railwatch
394
419
  end
395
420
  false
396
421
  end
422
+ # From here backpressure samples out executions the app asked to keep,
423
+ # and the receiver refusing them has no way to count them. Said once
424
+ # per refusal, and refusal_ended says what it cost.
425
+ if refusal
426
+ defer_notification(
427
+ DeliveryError.new("Railwatch ingest is refusing deliveries (#{refusal.reason}); holding " \
428
+ "#{batch.records.size} #{batch.records.size == 1 ? "record" : "records"}" \
429
+ "#{" and sampling down" if @config.backpressure} until it accepts again",
430
+ status: result.status)
431
+ )
432
+ end
397
433
  return unless gave_up
398
434
 
399
- # Losing a batch is not a debug-level event: with an ingest (or an
400
- # embedded writer) that never comes back this is the only place the
401
- # loss is ever reported, and the dropped counter it leaves behind
435
+ # Losing a batch is not a debug-level event: with an ingest that never
436
+ # comes back this is the only place the loss is ever reported, and the dropped counter it leaves behind
402
437
  # rides on the NEXT successful delivery, which may never happen.
403
438
  #
404
439
  # Reported here, after the lock is released, never inside it. The
@@ -420,6 +455,42 @@ module Railwatch
420
455
  )
421
456
  end
422
457
 
458
+ # The first delivery ingest accepted after refusing. The refusal was the
459
+ # pressure, so the divisor goes straight back to one, as it went up,
460
+ # rather than halving over three more flush ticks. A buffer still over
461
+ # its high-water mark raises it again on the next tick, as it would have
462
+ # anyway.
463
+ def refusal_ended
464
+ refusal, sampled_out, overflowed = @mutex.synchronize do
465
+ ended = @refusal
466
+ @refusal = nil
467
+ [ ended, @sampled_out - ended.sampled_out, @buffer.overflowed - ended.overflowed ]
468
+ end
469
+ @backpressure_factor = 1.0
470
+ dropped = overflowed + refusal.gave_up
471
+ seconds = (Clock.monotonic - refusal.started_at).round
472
+ defer_notification(
473
+ DeliveryError.new("Railwatch ingest is accepting deliveries again after refusing for #{seconds}s; " \
474
+ "meanwhile backpressure sampled out #{sampled_out} " \
475
+ "#{sampled_out == 1 ? "execution" : "executions"} and #{dropped} " \
476
+ "#{dropped == 1 ? "record was" : "records were"} dropped",
477
+ records: dropped)
478
+ )
479
+ end
480
+
481
+ # Never sooner than the ladder's rung either, so a receiver answering
482
+ # "Retry-After: 0" gets backoff, not a loop.
483
+ def refused_delay(retry_after_at)
484
+ wanted = retry_after_at - Time.now
485
+ return MAX_REFUSED_RETRY_DELAY * (0.5 + @random.rand * 0.5) if wanted > MAX_REFUSED_RETRY_DELAY
486
+
487
+ [ wanted, retry_delay(@retry_attempt) ].max.clamp(0.0, MAX_REFUSED_RETRY_DELAY)
488
+ end
489
+
490
+ def failure_detail(result)
491
+ [ result.status && "HTTP #{result.status}", result.error ].compact.reject { |part| part.to_s.empty? }.join(": ")
492
+ end
493
+
423
494
  def discard_unauthorized
424
495
  batch = drain_into_flight
425
496
  delivery_succeeded