railwatch 0.9.3 → 1.0.0

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