railwatch 0.9.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +37 -33
  3. data/CHANGELOG.md +50 -0
  4. data/README.md +51 -60
  5. data/config/routes.rb +0 -59
  6. data/docs/ai-and-mcp.md +9 -8
  7. data/docs/configuration.md +35 -66
  8. data/docs/faq.md +17 -25
  9. data/docs/getting-started.md +105 -117
  10. data/docs/replacing-nightwatch.md +8 -7
  11. data/docs/replacing-sentry.md +12 -16
  12. data/docs/security.md +14 -20
  13. data/docs/self-hosting.md +24 -22
  14. data/docs/testing.md +6 -14
  15. data/docs/troubleshooting.md +12 -46
  16. data/lib/generators/railwatch/install/install_generator.rb +12 -240
  17. data/lib/generators/railwatch/install/templates/initializer.rb.tt +10 -40
  18. data/lib/generators/railwatch/install/templates/post-deploy +0 -7
  19. data/lib/railwatch/authentication.rb +6 -5
  20. data/lib/railwatch/configuration.rb +21 -151
  21. data/lib/railwatch/engine.rb +9 -118
  22. data/lib/railwatch/patches.rb +0 -11
  23. data/lib/railwatch/reporter.rb +3 -19
  24. data/lib/railwatch/subscribers/process_info.rb +1 -2
  25. data/lib/railwatch/version.rb +1 -1
  26. data/lib/railwatch.rb +4 -62
  27. data/lib/tasks/railwatch_tasks.rake +3 -325
  28. data/llms.txt +23 -25
  29. metadata +3 -453
  30. data/app/channels/railwatch/environment_channel.rb +0 -62
  31. data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
  32. data/app/controllers/railwatch/alerts_controller.rb +0 -48
  33. data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
  34. data/app/controllers/railwatch/attachments_controller.rb +0 -22
  35. data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
  36. data/app/controllers/railwatch/cache_events_controller.rb +0 -30
  37. data/app/controllers/railwatch/commands_controller.rb +0 -16
  38. data/app/controllers/railwatch/comments_controller.rb +0 -11
  39. data/app/controllers/railwatch/dashboard_controller.rb +0 -70
  40. data/app/controllers/railwatch/deploys_controller.rb +0 -34
  41. data/app/controllers/railwatch/deprecations_controller.rb +0 -54
  42. data/app/controllers/railwatch/environment_scoped.rb +0 -81
  43. data/app/controllers/railwatch/exceptions_controller.rb +0 -52
  44. data/app/controllers/railwatch/executions_controller.rb +0 -13
  45. data/app/controllers/railwatch/issues_controller.rb +0 -258
  46. data/app/controllers/railwatch/jobs_controller.rb +0 -63
  47. data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
  48. data/app/controllers/railwatch/logs_controller.rb +0 -40
  49. data/app/controllers/railwatch/mails_controller.rb +0 -19
  50. data/app/controllers/railwatch/notifications_controller.rb +0 -11
  51. data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
  52. data/app/controllers/railwatch/overview_controller.rb +0 -38
  53. data/app/controllers/railwatch/people_controller.rb +0 -25
  54. data/app/controllers/railwatch/processes_controller.rb +0 -33
  55. data/app/controllers/railwatch/profiles_controller.rb +0 -65
  56. data/app/controllers/railwatch/queries_controller.rb +0 -59
  57. data/app/controllers/railwatch/releases_controller.rb +0 -75
  58. data/app/controllers/railwatch/requests_controller.rb +0 -37
  59. data/app/controllers/railwatch/saved_views_controller.rb +0 -53
  60. data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
  61. data/app/controllers/railwatch/spans_controller.rb +0 -47
  62. data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
  63. data/app/controllers/railwatch/tenants_controller.rb +0 -44
  64. data/app/controllers/railwatch/thresholds_controller.rb +0 -47
  65. data/app/controllers/railwatch/traces_controller.rb +0 -72
  66. data/app/controllers/railwatch/transactions_controller.rb +0 -21
  67. data/app/controllers/railwatch/view_renders_controller.rb +0 -28
  68. data/app/controllers/railwatch/visits_controller.rb +0 -53
  69. data/app/frontend/components/app-content.tsx +0 -26
  70. data/app/frontend/components/app-logo-icon.tsx +0 -23
  71. data/app/frontend/components/app-logo.tsx +0 -14
  72. data/app/frontend/components/app-shell.tsx +0 -38
  73. data/app/frontend/components/app-sidebar-header.tsx +0 -54
  74. data/app/frontend/components/app-sidebar.tsx +0 -94
  75. data/app/frontend/components/app-wordmark.tsx +0 -29
  76. data/app/frontend/components/breadcrumbs.tsx +0 -46
  77. data/app/frontend/components/icon.tsx +0 -16
  78. data/app/frontend/components/input-error.tsx +0 -18
  79. data/app/frontend/components/nav-footer.tsx +0 -46
  80. data/app/frontend/components/nav-main.tsx +0 -41
  81. data/app/frontend/components/nav-user.tsx +0 -50
  82. data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
  83. data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
  84. data/app/frontend/components/railwatch/code.tsx +0 -28
  85. data/app/frontend/components/railwatch/command-palette.tsx +0 -132
  86. data/app/frontend/components/railwatch/copy-block.tsx +0 -52
  87. data/app/frontend/components/railwatch/copy-id.tsx +0 -49
  88. data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
  89. data/app/frontend/components/railwatch/data-table.tsx +0 -195
  90. data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
  91. data/app/frontend/components/railwatch/empty-state.tsx +0 -159
  92. data/app/frontend/components/railwatch/env-nav.tsx +0 -130
  93. data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
  94. data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
  95. data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
  96. data/app/frontend/components/railwatch/frames.tsx +0 -295
  97. data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
  98. data/app/frontend/components/railwatch/live-dot.tsx +0 -78
  99. data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
  100. data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
  101. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
  102. data/app/frontend/components/railwatch/onboarding.tsx +0 -91
  103. data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
  104. data/app/frontend/components/railwatch/page-header.tsx +0 -42
  105. data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
  106. data/app/frontend/components/railwatch/relative-time.tsx +0 -18
  107. data/app/frontend/components/railwatch/saved-views.tsx +0 -232
  108. data/app/frontend/components/railwatch/segmented.tsx +0 -64
  109. data/app/frontend/components/railwatch/series-chart.tsx +0 -400
  110. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
  111. data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
  112. data/app/frontend/components/railwatch/sort-header.tsx +0 -37
  113. data/app/frontend/components/railwatch/source-link.tsx +0 -57
  114. data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
  115. data/app/frontend/components/railwatch/sql-block.tsx +0 -116
  116. data/app/frontend/components/railwatch/stat.tsx +0 -185
  117. data/app/frontend/components/railwatch/status-badge.tsx +0 -79
  118. data/app/frontend/components/railwatch/step-picker.tsx +0 -57
  119. data/app/frontend/components/railwatch/timeline.tsx +0 -405
  120. data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
  121. data/app/frontend/components/railwatch/window-picker.tsx +0 -147
  122. data/app/frontend/components/ui/avatar.tsx +0 -51
  123. data/app/frontend/components/ui/badge.tsx +0 -46
  124. data/app/frontend/components/ui/breadcrumb.tsx +0 -109
  125. data/app/frontend/components/ui/button.tsx +0 -60
  126. data/app/frontend/components/ui/card.tsx +0 -92
  127. data/app/frontend/components/ui/chart.tsx +0 -374
  128. data/app/frontend/components/ui/checkbox.tsx +0 -32
  129. data/app/frontend/components/ui/collapsible.tsx +0 -31
  130. data/app/frontend/components/ui/command.tsx +0 -182
  131. data/app/frontend/components/ui/dialog.tsx +0 -158
  132. data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
  133. data/app/frontend/components/ui/hover-card.tsx +0 -42
  134. data/app/frontend/components/ui/input.tsx +0 -21
  135. data/app/frontend/components/ui/label.tsx +0 -24
  136. data/app/frontend/components/ui/popover.tsx +0 -87
  137. data/app/frontend/components/ui/select.tsx +0 -185
  138. data/app/frontend/components/ui/separator.tsx +0 -28
  139. data/app/frontend/components/ui/sheet.tsx +0 -137
  140. data/app/frontend/components/ui/sidebar.tsx +0 -726
  141. data/app/frontend/components/ui/skeleton.tsx +0 -13
  142. data/app/frontend/components/ui/sonner.tsx +0 -41
  143. data/app/frontend/components/ui/table.tsx +0 -114
  144. data/app/frontend/components/ui/tabs.tsx +0 -91
  145. data/app/frontend/components/ui/textarea.tsx +0 -18
  146. data/app/frontend/components/ui/tooltip.tsx +0 -61
  147. data/app/frontend/components/user-info.tsx +0 -32
  148. data/app/frontend/components/user-menu-content.tsx +0 -94
  149. data/app/frontend/entrypoints/application.css +0 -244
  150. data/app/frontend/entrypoints/inertia.tsx +0 -92
  151. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  152. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  153. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  154. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  155. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  156. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  157. data/app/frontend/hooks/use-appearance.tsx +0 -67
  158. data/app/frontend/hooks/use-clipboard.ts +0 -32
  159. data/app/frontend/hooks/use-flash.tsx +0 -23
  160. data/app/frontend/hooks/use-hotkeys.ts +0 -63
  161. data/app/frontend/hooks/use-initials.tsx +0 -15
  162. data/app/frontend/hooks/use-live.ts +0 -205
  163. data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
  164. data/app/frontend/hooks/use-mobile.ts +0 -21
  165. data/app/frontend/hooks/use-percentile.ts +0 -24
  166. data/app/frontend/hooks/use-reduced-motion.ts +0 -25
  167. data/app/frontend/hooks/use-row-nav.ts +0 -106
  168. data/app/frontend/hooks/use-step.ts +0 -27
  169. data/app/frontend/hooks/use-ticker.ts +0 -12
  170. data/app/frontend/hooks/use-window.ts +0 -58
  171. data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
  172. data/app/frontend/layouts/app-layout.tsx +0 -21
  173. data/app/frontend/layouts/env-layout.tsx +0 -53
  174. data/app/frontend/layouts/persistent-layout.tsx +0 -18
  175. data/app/frontend/lib/cable.ts +0 -10
  176. data/app/frontend/lib/env-pages.ts +0 -236
  177. data/app/frontend/lib/execution-path.ts +0 -55
  178. data/app/frontend/lib/explain.ts +0 -50
  179. data/app/frontend/lib/filter.ts +0 -25
  180. data/app/frontend/lib/flamegraph.ts +0 -155
  181. data/app/frontend/lib/format.ts +0 -71
  182. data/app/frontend/lib/keyboard.ts +0 -18
  183. data/app/frontend/lib/mount.ts +0 -23
  184. data/app/frontend/lib/source-link.ts +0 -217
  185. data/app/frontend/lib/utils.ts +0 -6
  186. data/app/frontend/pages/alerts/index.tsx +0 -171
  187. data/app/frontend/pages/broadcasts/index.tsx +0 -171
  188. data/app/frontend/pages/cache_events/index.tsx +0 -127
  189. data/app/frontend/pages/commands/index.tsx +0 -143
  190. data/app/frontend/pages/deploys/index.tsx +0 -94
  191. data/app/frontend/pages/deploys/show.tsx +0 -309
  192. data/app/frontend/pages/deprecations/index.tsx +0 -176
  193. data/app/frontend/pages/exceptions/index.tsx +0 -271
  194. data/app/frontend/pages/executions/show.tsx +0 -821
  195. data/app/frontend/pages/issues/index.tsx +0 -399
  196. data/app/frontend/pages/issues/show.tsx +0 -1367
  197. data/app/frontend/pages/jobs/index.tsx +0 -381
  198. data/app/frontend/pages/jobs/klass.tsx +0 -215
  199. data/app/frontend/pages/llm_calls/index.tsx +0 -664
  200. data/app/frontend/pages/logs/index.tsx +0 -303
  201. data/app/frontend/pages/mails/index.tsx +0 -218
  202. data/app/frontend/pages/notifications/index.tsx +0 -161
  203. data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
  204. data/app/frontend/pages/overview/show.tsx +0 -333
  205. data/app/frontend/pages/people/index.tsx +0 -102
  206. data/app/frontend/pages/people/show.tsx +0 -149
  207. data/app/frontend/pages/processes/index.tsx +0 -335
  208. data/app/frontend/pages/profiles/index.tsx +0 -110
  209. data/app/frontend/pages/profiles/show.tsx +0 -105
  210. data/app/frontend/pages/queries/index.tsx +0 -492
  211. data/app/frontend/pages/queries/show.tsx +0 -276
  212. data/app/frontend/pages/releases/index.tsx +0 -190
  213. data/app/frontend/pages/releases/release-health.tsx +0 -68
  214. data/app/frontend/pages/releases/release-path.ts +0 -25
  215. data/app/frontend/pages/releases/show.tsx +0 -262
  216. data/app/frontend/pages/requests/index.tsx +0 -235
  217. data/app/frontend/pages/requests/route.tsx +0 -159
  218. data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
  219. data/app/frontend/pages/spans/index.tsx +0 -229
  220. data/app/frontend/pages/spans/show.tsx +0 -220
  221. data/app/frontend/pages/storage_ops/index.tsx +0 -150
  222. data/app/frontend/pages/tenants/index.tsx +0 -236
  223. data/app/frontend/pages/tenants/show.tsx +0 -477
  224. data/app/frontend/pages/tenants/tenant-path.ts +0 -26
  225. data/app/frontend/pages/thresholds/index.tsx +0 -414
  226. data/app/frontend/pages/traces/show.tsx +0 -208
  227. data/app/frontend/pages/transactions/index.tsx +0 -220
  228. data/app/frontend/pages/view_renders/index.tsx +0 -241
  229. data/app/frontend/pages/visits/index.tsx +0 -321
  230. data/app/frontend/public/icon.png +0 -0
  231. data/app/frontend/public/icon.svg +0 -5
  232. data/app/frontend/routes/index.d.ts +0 -2136
  233. data/app/frontend/routes/index.js +0 -2112
  234. data/app/frontend/types/globals.d.ts +0 -9
  235. data/app/frontend/types/index.ts +0 -288
  236. data/app/frontend/types/vite-env.d.ts +0 -9
  237. data/app/helpers/railwatch/assets_helper.rb +0 -52
  238. data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
  239. data/app/jobs/railwatch/application_job.rb +0 -7
  240. data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
  241. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
  242. data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
  243. data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
  244. data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
  245. data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
  246. data/app/jobs/railwatch/performance_scan_job.rb +0 -11
  247. data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
  248. data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
  249. data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
  250. data/app/jobs/railwatch/rollup_job.rb +0 -132
  251. data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
  252. data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
  253. data/app/models/railwatch/alert.rb +0 -229
  254. data/app/models/railwatch/alert_rule.rb +0 -121
  255. data/app/models/railwatch/anomaly_rule.rb +0 -51
  256. data/app/models/railwatch/application.rb +0 -44
  257. data/app/models/railwatch/application_record.rb +0 -33
  258. data/app/models/railwatch/comment.rb +0 -36
  259. data/app/models/railwatch/deploy.rb +0 -67
  260. data/app/models/railwatch/environment.rb +0 -68
  261. data/app/models/railwatch/execution_presenter.rb +0 -185
  262. data/app/models/railwatch/filter_query.rb +0 -184
  263. data/app/models/railwatch/followup_receipt.rb +0 -27
  264. data/app/models/railwatch/ingest/batch.rb +0 -355
  265. data/app/models/railwatch/ingest/mapper.rb +0 -585
  266. data/app/models/railwatch/ingest/payload.rb +0 -96
  267. data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
  268. data/app/models/railwatch/ingest/writer.rb +0 -137
  269. data/app/models/railwatch/issue.rb +0 -277
  270. data/app/models/railwatch/issue_activity.rb +0 -23
  271. data/app/models/railwatch/issue_detection_presenter.rb +0 -309
  272. data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
  273. data/app/models/railwatch/maintenance_task.rb +0 -53
  274. data/app/models/railwatch/saved_view.rb +0 -63
  275. data/app/models/railwatch/telemetry/aggregations.rb +0 -251
  276. data/app/models/railwatch/telemetry/attachment.rb +0 -31
  277. data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
  278. data/app/models/railwatch/telemetry/broadcast.rb +0 -13
  279. data/app/models/railwatch/telemetry/cache_event.rb +0 -16
  280. data/app/models/railwatch/telemetry/child.rb +0 -53
  281. data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
  282. data/app/models/railwatch/telemetry/deprecation.rb +0 -13
  283. data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
  284. data/app/models/railwatch/telemetry/exception.rb +0 -28
  285. data/app/models/railwatch/telemetry/execution.rb +0 -152
  286. data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
  287. data/app/models/railwatch/telemetry/export_destination.rb +0 -66
  288. data/app/models/railwatch/telemetry/health_sample.rb +0 -72
  289. data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
  290. data/app/models/railwatch/telemetry/llm_call.rb +0 -37
  291. data/app/models/railwatch/telemetry/log.rb +0 -91
  292. data/app/models/railwatch/telemetry/mail.rb +0 -13
  293. data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
  294. data/app/models/railwatch/telemetry/notification.rb +0 -13
  295. data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
  296. data/app/models/railwatch/telemetry/person.rb +0 -50
  297. data/app/models/railwatch/telemetry/process.rb +0 -10
  298. data/app/models/railwatch/telemetry/profile.rb +0 -37
  299. data/app/models/railwatch/telemetry/query.rb +0 -57
  300. data/app/models/railwatch/telemetry/query_shape.rb +0 -45
  301. data/app/models/railwatch/telemetry/release_health.rb +0 -69
  302. data/app/models/railwatch/telemetry/rollup.rb +0 -157
  303. data/app/models/railwatch/telemetry/session.rb +0 -24
  304. data/app/models/railwatch/telemetry/span.rb +0 -19
  305. data/app/models/railwatch/telemetry/storage_op.rb +0 -13
  306. data/app/models/railwatch/telemetry/tenant.rb +0 -240
  307. data/app/models/railwatch/telemetry/transaction.rb +0 -13
  308. data/app/models/railwatch/telemetry/view_render.rb +0 -13
  309. data/app/models/railwatch/telemetry/visit.rb +0 -24
  310. data/app/models/railwatch/telemetry_record.rb +0 -107
  311. data/app/models/railwatch/threshold.rb +0 -66
  312. data/app/models/railwatch/user.rb +0 -47
  313. data/app/models/railwatch/viewer.rb +0 -13
  314. data/app/models/railwatch/window.rb +0 -66
  315. data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
  316. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
  317. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
  318. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
  319. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
  320. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
  321. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
  322. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
  323. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
  324. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
  325. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
  326. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
  327. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
  328. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
  329. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
  330. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
  331. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
  332. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
  333. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
  334. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
  335. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
  336. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
  337. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
  338. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
  339. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
  340. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
  341. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
  342. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
  343. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
  344. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
  345. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
  346. data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
  347. data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
  348. data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
  349. data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
  350. data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
  351. data/docs/embedded.md +0 -503
  352. data/lib/puma/plugin/railwatch.rb +0 -215
  353. data/lib/railwatch/dashboard_assets.rb +0 -45
  354. data/lib/railwatch/embedded.rb +0 -50
  355. data/lib/railwatch/export/client.rb +0 -124
  356. data/lib/railwatch/export/lease.rb +0 -56
  357. data/lib/railwatch/export/outbox.rb +0 -361
  358. data/lib/railwatch/export/policy.rb +0 -79
  359. data/lib/railwatch/export/sender.rb +0 -130
  360. data/lib/railwatch/ingest_request_body_limit.rb +0 -10
  361. data/lib/railwatch/maintenance.rb +0 -194
  362. data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
  363. data/lib/railwatch/transport/local.rb +0 -78
  364. data/lib/railwatch/transport/socket.rb +0 -183
  365. data/lib/railwatch/writer.rb +0 -370
  366. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  367. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  368. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  369. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  370. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  371. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  372. data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
  373. data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
  374. data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
  375. data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
  376. data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
  377. data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
  378. data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
  379. data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
  380. data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
  381. data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
  382. data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
  383. data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
  384. data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
  385. data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
  386. data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
  387. data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
  388. data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
  389. data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
  390. data/public/railwatch/assets/index--NQ7us-L.js +0 -1
  391. data/public/railwatch/assets/index-2kgYe_99.js +0 -1
  392. data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
  393. data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
  394. data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
  395. data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
  396. data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
  397. data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
  398. data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
  399. data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
  400. data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
  401. data/public/railwatch/assets/index-C2j50AL2.js +0 -1
  402. data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
  403. data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
  404. data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
  405. data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
  406. data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
  407. data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
  408. data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
  409. data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
  410. data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
  411. data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
  412. data/public/railwatch/assets/index-Deq14BxB.js +0 -1
  413. data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
  414. data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
  415. data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
  416. data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
  417. data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
  418. data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
  419. data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
  420. data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
  421. data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
  422. data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
  423. data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
  424. data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
  425. data/public/railwatch/assets/route-CWh58C7K.js +0 -1
  426. data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
  427. data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
  428. data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
  429. data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
  430. data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
  431. data/public/railwatch/assets/show-BYPB74t4.js +0 -2
  432. data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
  433. data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
  434. data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
  435. data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
  436. data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
  437. data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
  438. data/public/railwatch/assets/show-g__jnN36.js +0 -1
  439. data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
  440. data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
  441. data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
  442. data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
  443. data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
  444. data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
  445. data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
  446. data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
  447. data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
  448. data/public/railwatch/icon.png +0 -0
  449. data/public/railwatch/icon.svg +0 -5
  450. data/public/railwatch/manifest.json +0 -1245
  451. data/public/railwatch/rails-vite.json +0 -1
  452. /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
@@ -1,194 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # TASKS is built from duration literals at load time; the engine has these
4
- # loaded already, but a writer process required ahead of the app must not
5
- # depend on that.
6
- require "active_support/core_ext/numeric/time"
7
-
8
- module Railwatch
9
- # The embedded install's own clock. One background thread per web/worker
10
- # process wakes every TICK seconds and runs whichever maintenance task is
11
- # due: release-health rollups, threshold and anomaly scans, missed
12
- # scheduled tasks, auto-resolve, pruning. Every process runs the clock;
13
- # one process at a time runs a given task, claimed through a lease row in
14
- # the railwatch database (MaintenanceTask), so a Puma cluster and a Solid
15
- # Queue worker on the same host do not all prune at once.
16
- #
17
- # Same thread shape as Railwatch::Health: parked on a ConditionVariable,
18
- # started from the engine, re-armed in every forked child. Nothing here
19
- # goes through Active Job -- the bodies call the job classes' perform
20
- # directly -- so an embedded install needs no worker, no recurring.yml,
21
- # and never writes the host's queue adapter.
22
- #
23
- # A task must never be visible to the app it is maintaining: each body runs
24
- # inside Railwatch.ignore and rescues everything.
25
- module Maintenance
26
- ROLES = %w[web worker writer].freeze
27
- TICK = 30
28
- FOLLOWUP_BATCHES_PER_TICK = 200
29
-
30
- # name => [interval, lease, body]. The lease is how long a claim is held
31
- # by a process that never releases it (crashed mid-task); it is a ceiling
32
- # on the work, not an estimate of it.
33
- TASKS = {
34
- # A batch's exceptions are grouped into issues right after it commits;
35
- # a process that dies in between leaves the work recorded on the
36
- # batch's ledger row. Bounded per tick so a long outage drains over a
37
- # few ticks rather than one long one.
38
- "drain_followups" => [ 1.minute, 5.minutes, lambda { |env|
39
- env.with_telemetry do
40
- Telemetry::IngestBatch.with_pending_followups.limit(FOLLOWUP_BATCHES_PER_TICK).each do |batch|
41
- batch.drain_followups!(env)
42
- end
43
- end
44
- } ],
45
- "release_health" => [ 1.minute, 5.minutes, lambda { |env|
46
- now = Time.current
47
- [ now.beginning_of_hour, (now - 1.hour).beginning_of_hour ].each do |bucket|
48
- ReleaseHealthRollupJob.new.perform(env, bucket)
49
- end
50
- } ],
51
- # Embedded batches fold themselves into the current hour as they land
52
- # (Ingest::RollupAbsorber), so the reconciler only ever needs to catch
53
- # rows that arrived after their hour closed.
54
- "rollup_reconcile" => [ 1.hour, 10.minutes, lambda { |env|
55
- RollupJob.new.perform(env, (Time.current - 1.hour).beginning_of_hour)
56
- } ],
57
- "performance_scan" => [ 5.minutes, 10.minutes, lambda { |env|
58
- DetectPerformanceIssuesJob.new.perform(env)
59
- } ],
60
- "anomaly_scan" => [ 5.minutes, 10.minutes, lambda { |env|
61
- DetectAnomaliesJob.new.perform(env) if AnomalyRule.where(enabled: true).exists?
62
- } ],
63
- "scheduled_tasks" => [ 10.minutes, 10.minutes, lambda { |env|
64
- CheckScheduledTasksJob.new.perform(env)
65
- } ],
66
- "auto_resolve" => [ 24.hours, 10.minutes, lambda { |_env|
67
- AutoResolveIssuesJob.new.perform
68
- } ],
69
- # Deliveries the export queue has stopped waiting for, and the history
70
- # of ones it finished. Without this the age limit is advice rather than
71
- # a limit: a delivery whose receiver never recovers keeps its bytes,
72
- # and the queue eventually fills and sheds live telemetry instead.
73
- "export_expiry" => [ 5.minutes, 10.minutes, lambda { |env|
74
- next unless Railwatch.config.export?
75
-
76
- outbox = Export::Outbox.new(Railwatch.config, env)
77
- outbox.expire!
78
- outbox.prune!
79
- } ],
80
- # PASSIVE, not TRUNCATE: this runs inside a Puma worker, and a
81
- # truncating checkpoint blocks every reader and writer on the file.
82
- "prune" => [ 24.hours, 60.minutes, lambda { |env|
83
- PruneTelemetryJob.new.perform(env, checkpoint: "PASSIVE")
84
- OptimizeTelemetryJob.new.perform(env)
85
- FollowupReceipt.prune!
86
- } ]
87
- }.freeze
88
-
89
- @mutex = Mutex.new
90
- @wakeup = ConditionVariable.new
91
- @thread = nil
92
- @pid = nil
93
- @stopping = false
94
-
95
- module_function
96
-
97
- # Idempotent; Railwatch.restart_after_fork! calls it again in every forked
98
- # child.
99
- def start!
100
- return unless Railwatch.enabled? && Railwatch.config.local?
101
- return if defined?(Rails) && Rails.env.test?
102
- return unless ROLES.include?(Subscribers::ProcessInfo.role)
103
- # With a writer process listening, it is the one clock. The lease table
104
- # would keep two clocks honest, but there is no reason to run a second.
105
- return if !Writer.running? && Writer.listening?
106
- return if @thread&.alive? && @pid == Process.pid
107
-
108
- @mutex.synchronize do
109
- return if @thread&.alive? && @pid == Process.pid
110
-
111
- @pid = Process.pid
112
- @stopping = false
113
- @thread = Thread.new { run }
114
- @thread.name = "railwatch-maintenance"
115
- @thread.abort_on_exception = false
116
- @thread.report_on_exception = false
117
- end
118
- end
119
-
120
- # A forked child inherits a dead thread and may inherit a mutex held by a
121
- # vanished parent thread; every primitive is replaced before start!.
122
- def restart_after_fork!
123
- @mutex = Mutex.new
124
- @wakeup = ConditionVariable.new
125
- @thread = nil
126
- @pid = nil
127
- @stopping = false
128
- start!
129
- end
130
-
131
- def stop!
132
- return unless @thread
133
-
134
- @stopping = true
135
- @mutex.synchronize { @wakeup.signal }
136
- @thread.join(1)
137
- @thread = nil
138
- end
139
-
140
- def run
141
- until @stopping
142
- @mutex.synchronize { @wakeup.wait(@mutex, TICK) unless @stopping }
143
- tick unless @stopping
144
- end
145
- end
146
-
147
- # Runs every task that is due and unclaimed. Returns the names it ran.
148
- # Public so a spec, or an operator in a console, can drive the clock by
149
- # hand. One failing task is reported and does not stop the others.
150
- #
151
- # A web worker's clock starts at boot, before the Puma plugin has forked
152
- # the writer, so the start-time check in start! cannot see it. Checked
153
- # again on every tick: once a writer is listening this process's clock
154
- # stands down and stays down (the lease table would keep both honest,
155
- # but there is no reason to run a second one).
156
- def tick(now: Time.current)
157
- return [] if !Writer.running? && Writer.listening?
158
-
159
- ran = []
160
- Rails.application.executor.wrap do
161
- env = Environment.current
162
- TASKS.each do |name, (every, lease, body)|
163
- token = MaintenanceTask.claim(name, every: every, lease: lease, owner: owner, now: now) or next
164
-
165
- succeeded = false
166
- begin
167
- Railwatch.ignore { body.call(env) }
168
- succeeded = true
169
- ran << name
170
- rescue StandardError => e
171
- # Not Rails.error: this thread has no execution for Railwatch.ignore
172
- # to pause, so a report there would be captured by Railwatch's own
173
- # subscriber and opened as an application issue about Railwatch.
174
- Railwatch.debug { "maintenance #{name} failed: #{e.class}: #{e.message}" }
175
- Railwatch.notify_unrecoverable(TaskError.new("maintenance task #{name} failed: #{e.class}: #{e.message}"))
176
- ensure
177
- MaintenanceTask.release(name, token: token, ran_at: now, succeeded: succeeded)
178
- end
179
- end
180
- end
181
- ran
182
- rescue StandardError => e
183
- # The lease table not being migrated yet is the usual way to get here.
184
- Railwatch.debug { "maintenance tick failed: #{e.class}: #{e.message}" }
185
- ran
186
- end
187
-
188
- def owner
189
- "#{Railwatch.config.server}:#{Process.pid}"
190
- end
191
-
192
- class TaskError < StandardError; end
193
- end
194
- end
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- module Patches
5
- # Gives the migration connection to a Railwatch database a long SQLite
6
- # busy timeout, for the migration only.
7
- #
8
- # A deploy migrates while the previous release is still serving, and on
9
- # an embedded install that release's writer process is committing
10
- # telemetry to the same file the whole time. Each of its transactions
11
- # takes SQLite's single write lock: about a second on a warm 15 GB file
12
- # (the export sender's claim scans the destination's whole delivery
13
- # history), several seconds while a deploy's image build has the disk.
14
- # The migration connection waited only the database's configured
15
- # `timeout` -- 5 s in the generated database.yml -- and then raised
16
- # SQLite3::BusyException, which db:prepare reports as a failed migration
17
- # and the container entrypoint as a failed boot. That is how a deploy of
18
- # rebulk-system crash-looped eight times without one migration running.
19
- #
20
- # The configured timeout is the right one for the app's own requests,
21
- # which should fail fast rather than queue behind ingest. A migration is
22
- # the opposite: it runs once, before the server binds, and waiting is the
23
- # only way through. So only the connection Active Record migrates with is
24
- # changed, only while it migrates, and only for a database whose
25
- # migrations_paths include this gem's. The host's own databases keep
26
- # their timeouts.
27
- module MigrationBusyTimeout
28
- def migrate(*, **)
29
- pool = migration_connection_pool
30
- raw = Railwatch::Patches::MigrationBusyTimeout.lengthen(pool)
31
- super
32
- ensure
33
- Railwatch::Patches::MigrationBusyTimeout.restore(pool, raw) if raw
34
- end
35
-
36
- class << self
37
- # The raw SQLite connection whose timeout was lengthened, or nil when
38
- # this is not a Railwatch database (or not SQLite).
39
- def lengthen(pool)
40
- return unless railwatch_database?(pool.db_config)
41
-
42
- raw = pool.lease_connection.raw_connection
43
- return unless raw.respond_to?(:busy_handler_timeout=)
44
-
45
- raw.busy_handler_timeout = (Railwatch.config.migration_busy_timeout.to_f * 1000).to_i
46
- raw
47
- rescue StandardError => e
48
- Railwatch.debug { "migration busy timeout not applied: #{e.class}: #{e.message}" }
49
- nil
50
- end
51
-
52
- # Back to what database.yml says, so a connection that outlives the
53
- # migration (db:migrate in a long-lived process, a spec) behaves as
54
- # configured afterwards.
55
- def restore(pool, raw)
56
- timeout = pool.db_config.configuration_hash[:timeout]
57
- if timeout
58
- raw.busy_handler_timeout = Integer(timeout)
59
- else
60
- raw.busy_handler(nil)
61
- end
62
- rescue StandardError => e
63
- Railwatch.debug { "migration busy timeout not restored: #{e.class}: #{e.message}" }
64
- end
65
-
66
- def railwatch_database?(db_config)
67
- ours = %i[railwatch railwatch_telemetry].map { |name| File.expand_path(Railwatch.migrations_path(name)) }
68
- Array(db_config.migrations_paths).any? { |path| ours.include?(File.expand_path(path.to_s, Rails.root.to_s)) }
69
- end
70
- end
71
- end
72
- end
73
- end
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- module Transport
5
- # Writes each batch straight into the engine's telemetry database instead
6
- # of POSTing it. Same Reporter, same buffer, same backpressure; the only
7
- # difference from Transport::Http is that deliver ends in Ingest::Batch
8
- # rather than Net::HTTP. Runs on the reporter thread, never on a request.
9
- class Local
10
- Result = Struct.new(:ok, :status, :accepted, :rejected, :rejections, :error, :retryable_error, keyword_init: true) do
11
- def retryable? = retryable_error ? true : false
12
- end
13
-
14
- def initialize(config)
15
- @config = config
16
- end
17
-
18
- def deliver(records, dropped: 0, dropped_bytes: 0, backpressure_factor: 1.0, batch_id: nil)
19
- wire = stringify(records)
20
- # The reporter thread is not a request or a job: nothing has set up
21
- # an ExecutionContext for it. Query log tags, Rails.error.report and
22
- # connection checkin all assume one, so run the write inside the
23
- # executor the same way a job would.
24
- #
25
- # The rescue is INSIDE the wrap. The executor reports any exception
26
- # that escapes its block to Rails.error before re-raising it, and
27
- # Railwatch subscribes to Rails.error: a write that failed (the
28
- # telemetry database not migrated yet, a locked file) would otherwise
29
- # be captured as one of the application's own exceptions and opened
30
- # as an issue about Railwatch, by Railwatch.
31
- Rails.application.executor.wrap do
32
- write(wire, records.size, dropped: dropped, backpressure_factor: backpressure_factor, batch_id: batch_id)
33
- rescue StandardError => e
34
- Railwatch.debug { "local ingest failed: #{e.class}: #{e.message}" }
35
- # With a batch id the reporter can safely retry: the ledger says
36
- # whether the write committed, and the unique execution_id index
37
- # makes a replay of a half-visible batch a no-op. Without one (an
38
- # older caller) the batch is consumed, since a retry could
39
- # double-insert. A malformed record never gets here: the mapper
40
- # counts it as rejected and the rest of the batch is written.
41
- Result.new(ok: false, error: "#{e.class}: #{e.message}", retryable_error: !batch_id.nil?)
42
- end
43
- end
44
-
45
- def ping = true
46
- def unauthorized? = false
47
- def reset_after_fork! = self
48
-
49
- private
50
-
51
- def write(wire, count, dropped:, backpressure_factor:, batch_id:)
52
- environment = Environment.current
53
- # The same contract the HTTP path gets from X-Railwatch-Batch-Id: a
54
- # batch the reporter retries after a failure is written once. The
55
- # ledger row is created inside the batch transaction, so its presence
56
- # is the commit.
57
- if (ledger = environment.with_telemetry { Ingest::Batch.committed(batch_id) })
58
- return Result.new(ok: true, status: 200, accepted: ledger.accepted, rejected: ledger.rejected, rejections: [])
59
- end
60
-
61
- result = Ingest::Batch.new(environment, wire, dropped_by_client: dropped, backpressure_factor: backpressure_factor,
62
- gem_version: Railwatch::VERSION, embedded: true, batch_id: batch_id).write!
63
- Result.new(ok: true, status: 200, accepted: result.accepted, rejected: result.rejected,
64
- rejections: result.rejections.first(10))
65
- end
66
-
67
- # The gem builds symbol-keyed records, nested hashes included (stages,
68
- # counters, headers); the mapper reads string keys throughout and
69
- # rejects a symbol-keyed structure. A JSON round trip is the deep
70
- # stringify. Measured against the alternatives on a 500-record batch:
71
- # 1.9 ms and 11k allocations here versus 4.8 ms and 25k for
72
- # deep_transform_keys, which also leaves Symbol values as Symbols.
73
- def stringify(records)
74
- JSON.parse(JSON.generate(records))
75
- end
76
- end
77
- end
78
- end
@@ -1,183 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "socket"
4
- require "zlib"
5
- require "json"
6
-
7
- module Railwatch
8
- module Transport
9
- # Hands each batch to the embedded writer process (Railwatch::Writer) over
10
- # a Unix socket instead of writing it into SQLite on this thread. The
11
- # workers' Ruby threads then never map a record, hold SQLite's write lock,
12
- # or run a t-digest; all of that happens in one process whose GVL no
13
- # request shares. Same Reporter, same buffer, same batch id, same ledger:
14
- # a batch the writer already committed replays as a no-op there.
15
- #
16
- # One request per connection, length-prefixed, gzip JSON both ways:
17
- # > [u32 length][gzip(JSON {batch_id, records, dropped, dropped_bytes, backpressure_factor})]
18
- # < [u32 length][JSON {ok, status, accepted, rejected, rejections, error, retryable_error}]
19
- #
20
- # Whether a writer is EXPECTED decides what a missing one means. Under
21
- # the Puma plugin (which sets Writer.expected! in the master before it
22
- # forks the workers) a socket that is absent or refusing is a writer
23
- # that is starting or restarting: the batch is retained and retried on
24
- # the reporter's backoff ladder, which is bounded -- after
25
- # Reporter::MAX_RETRY_ATTEMPTS the batch is counted as dropped and
26
- # reported through on_unrecoverable, rather than held for ever. Anywhere else
27
- # (a plain `rails server`, a runner, a Solid Queue worker, the suite) no
28
- # writer will ever appear, so the first miss switches this transport to
29
- # an in-process Transport::Local for the rest of the process's life and
30
- # embedded mode works exactly as it did before the writer existed. A
31
- # refusing socket in that case is a stale inode from a dead writer and
32
- # falls back the same way rather than dropping batches forever.
33
- class Socket
34
- Result = Local::Result
35
- MAX_REPLY_BYTES = 1 << 20
36
- # How long an in-process fallback lasts before the socket is tried
37
- # again. The fallback is provisional on purpose: a process that missed
38
- # the writer once (a bare `puma` whose plugin could not mark one
39
- # expected, a worker forked before the writer bound) would otherwise
40
- # write its own batches for the rest of its life.
41
- FALLBACK_RECHECK = 30
42
- # How long a process that EXPECTS a writer keeps retaining batches for
43
- # one that never answers before it writes them itself. A writer that is
44
- # restarting is back in seconds; one that cannot bind at all (an
45
- # unwritable socket directory, a fork that keeps failing) would
46
- # otherwise retain until the reporter's retry cap and then drop the
47
- # batch. Writing it here instead keeps the records.
48
- WRITER_GRACE = 60
49
-
50
- def initialize(config, path: nil, expected: nil)
51
- @config = config
52
- @path = path || config.writer_socket_path
53
- @expected = expected
54
- # A path the kernel cannot bind (over 108 bytes on Linux) can never
55
- # have a writer behind it; do not spend a batch finding out, and do
56
- # not keep re-checking it either.
57
- @unusable_path = !Writer.usable_path?(@path)
58
- @fallback = @unusable_path ? Local.new(config) : nil
59
- @fallback_at = Clock.monotonic if @fallback
60
- Railwatch.debug { "writer socket path #{@path.inspect} is too long; writing batches in-process" } if @fallback
61
- end
62
-
63
- attr_reader :path
64
-
65
- def fallback? = !@fallback.nil?
66
-
67
- # Read when a batch misses, not when the transport is built: under
68
- # `rails server` the app boots (and the reporter picks this transport)
69
- # before Puma evaluates config/puma.rb, so the plugin's Writer.expected!
70
- # lands after construction, and the workers inherit a copy of this
71
- # object at fork. A flag captured here at construction was always false
72
- # in every worker, and the first flush before the writer was listening
73
- # fell back to in-process writes for the life of the worker.
74
- def expected? = @expected.nil? ? Writer.expected? : @expected
75
-
76
- def deliver(records, dropped: 0, dropped_bytes: 0, backpressure_factor: 1.0, batch_id: nil)
77
- return @fallback.deliver(records, dropped: dropped, dropped_bytes: dropped_bytes,
78
- backpressure_factor: backpressure_factor, batch_id: batch_id) if falling_back?
79
-
80
- payload = Zlib.gzip(JSON.generate(batch_id: batch_id, records: records, dropped: dropped,
81
- dropped_bytes: dropped_bytes, backpressure_factor: backpressure_factor))
82
- reply = exchange(payload)
83
- Result.new(**reply.slice("ok", "status", "accepted", "rejected", "rejections", "error", "retryable_error").transform_keys(&:to_sym))
84
- rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTSOCK => e
85
- @missing_since ||= Clock.monotonic
86
- if expected? && Clock.monotonic - @missing_since < WRITER_GRACE
87
- Railwatch.debug { "writer not answering at #{@path} (#{e.class}); retaining the batch" }
88
- return Result.new(ok: false, error: "writer unavailable: #{e.message}", retryable_error: true)
89
- end
90
-
91
- Railwatch.debug do
92
- if expected?
93
- "no writer at #{@path} after #{WRITER_GRACE}s (#{e.class}); writing batches in-process, re-checking every #{FALLBACK_RECHECK}s"
94
- else
95
- "no writer at #{@path} (#{e.class}) and none expected; writing batches in-process, re-checking every #{FALLBACK_RECHECK}s"
96
- end
97
- end
98
- @fallback = Local.new(@config)
99
- @fallback_at = Clock.monotonic
100
- deliver(records, dropped: dropped, dropped_bytes: dropped_bytes, backpressure_factor: backpressure_factor, batch_id: batch_id)
101
- rescue SystemCallError, IOError, Zlib::Error, JSON::ParserError, Timeout::Error => e
102
- Railwatch.debug { "writer delivery failed: #{e.class}: #{e.message}" }
103
- Result.new(ok: false, error: "#{e.class}: #{e.message}", retryable_error: true)
104
- end
105
-
106
- # Whether this batch goes in-process. A fallback for an unbindable path
107
- # is permanent; any other one is re-examined every FALLBACK_RECHECK
108
- # seconds, so a writer that turns up later takes the work back.
109
- def falling_back?
110
- return false if @fallback.nil?
111
- return true if @unusable_path || Clock.monotonic - @fallback_at < FALLBACK_RECHECK
112
-
113
- if Writer.listening?(@path)
114
- Railwatch.debug { "writer is answering at #{@path} again; handing batches back to it" }
115
- @fallback = nil
116
- @missing_since = nil
117
- false
118
- else
119
- @fallback_at = Clock.monotonic
120
- true
121
- end
122
- end
123
-
124
- def ping
125
- return true if @fallback
126
-
127
- UNIXSocket.new(@path).close
128
- true
129
- rescue SystemCallError, ArgumentError
130
- false
131
- end
132
-
133
- def unauthorized? = false
134
- def reset_after_fork! = self
135
-
136
- private
137
-
138
- # Every read and write is under one deadline of config.timeout, the
139
- # same budget the HTTP transport gives a POST; a writer that accepts but
140
- # stops reading cannot hold the reporter thread past it.
141
- def exchange(payload)
142
- UNIXSocket.open(@path) do |sock|
143
- deadline = Clock.monotonic + @config.timeout
144
- write_exactly(sock, [ payload.bytesize ].pack("N") + payload, deadline)
145
- header = read_exactly(sock, 4, deadline)
146
- length = header.unpack1("N")
147
- raise IOError, "writer reply too large (#{length} bytes)" if length > MAX_REPLY_BYTES
148
-
149
- JSON.parse(read_exactly(sock, length, deadline))
150
- end
151
- end
152
-
153
- def write_exactly(sock, data, deadline)
154
- offset = 0
155
- while offset < data.bytesize
156
- remaining = deadline - Clock.monotonic
157
- raise Timeout::Error, "writer did not accept the batch within #{@config.timeout}s" if remaining <= 0
158
- raise IOError, "writer closed the connection" unless sock.wait_writable(remaining)
159
-
160
- written = sock.write_nonblock(data.byteslice(offset..), exception: false)
161
- offset += written if written.is_a?(Integer)
162
- end
163
- end
164
-
165
- def read_exactly(sock, count, deadline)
166
- buffer = +""
167
- while buffer.bytesize < count
168
- remaining = deadline - Clock.monotonic
169
- raise Timeout::Error, "writer did not answer within #{@config.timeout}s" if remaining <= 0
170
- raise IOError, "writer closed the connection" unless sock.wait_readable(remaining)
171
-
172
- chunk = sock.read_nonblock(count - buffer.bytesize, exception: false)
173
- case chunk
174
- when :wait_readable then next
175
- when nil then raise IOError, "writer closed the connection"
176
- else buffer << chunk
177
- end
178
- end
179
- buffer
180
- end
181
- end
182
- end
183
- end