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,215 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "puma/plugin"
4
-
5
- # `plugin :railwatch` in config/puma.rb. In embedded mode, forks one
6
- # Railwatch::Writer from the Puma process once it has booted, restarts it if
7
- # it dies, and stops it with Puma. Same shape as Solid Queue's
8
- # `solid_queue_mode :fork`, in both cluster and single mode: a default Rails
9
- # 8 app runs Puma in single mode (no WEB_CONCURRENCY), and that app's
10
- # batches belong off its request threads just as much. The fork happens
11
- # from the plugin's background thread with the app loaded and Rails'
12
- # ForkTracker resetting every Railwatch thread in the child, exactly as it
13
- # does for a Puma cluster worker. A no-op when Railwatch is off or the
14
- # transport is not :local, so it is safe to leave in place.
15
- #
16
- # Nothing in `start` may touch Railwatch: `bundle exec puma` fires plugin
17
- # starts BEFORE it loads the Rack app, so the constant may not exist yet.
18
- # Every decision that needs the app is made from the background supervisor,
19
- # which waits for after_booted.
20
- Puma::Plugin.create do
21
- attr_reader :log_writer, :writer_pid
22
-
23
- POLL = 2
24
- # How often a stopping writer is checked for, and how long a KILL is given
25
- # to take before the pid is abandoned (a process stuck in disk I/O cannot
26
- # die until the I/O returns, and Puma's exit should not wait for that).
27
- REAP_POLL = 0.05
28
- KILL_REAP = 1
29
-
30
- def start(launcher)
31
- @log_writer = launcher.log_writer
32
- @puma_pid = $$
33
- @launcher = launcher
34
- @shutting_down = false
35
- @booted = false
36
- @warned = false
37
-
38
- # When the app is already loaded (`bin/rails server` loads it before
39
- # handing over to Puma), mark a writer expected now: cluster workers fork
40
- # from this process after start and inherit the flag, so they retain
41
- # batches while the writer is starting instead of writing them in-process.
42
- # Under bare `puma` this is false and the workers find the writer on
43
- # their transport's next re-check instead (Transport::Socket).
44
- ::Railwatch::Writer.expected! if active?
45
-
46
- # in_background blocks are collected at plugin start and started by the
47
- # runner once, so this has to be registered here, not from after_booted
48
- # (by then they have already been fired and a late block never runs). It
49
- # waits for boot itself.
50
- in_background { supervise }
51
-
52
- launcher.events.after_booted { booted! }
53
- # A phased restart fires before_restart and then, once the new workers
54
- # are up, after_booted again. The writer is stopped for the restart and
55
- # the supervisor, which is still running, respawns it when @booted flips
56
- # back. Only a real stop latches shutdown.
57
- launcher.events.before_restart { restart_writer }
58
- # after_stopped only latches. It is NOT where the writer is stopped:
59
- # Puma's SIGTERM trap fires it BEFORE `stop_blocked` drains in-flight
60
- # requests, so stopping here would take the writer away from requests
61
- # that are still running. at_exit runs after the whole run loop.
62
- launcher.events.after_stopped { @shutting_down = true }
63
- at_exit { shutdown_writer if Process.pid == @puma_pid }
64
- end
65
-
66
- private
67
-
68
- def booted!
69
- @booted = true
70
- ::Railwatch::Writer.expected! if active?
71
- end
72
-
73
- # Whether this process should run a writer. Answered fresh each time, and
74
- # safe before the app is loaded: `defined?(Railwatch)` is true as soon as
75
- # the gem's entrypoint has been required (Bundler.require), but a bare
76
- # `puma` on an app that has not required it yet sees neither, and must not
77
- # raise out of Puma's boot.
78
- def active?
79
- return false unless defined?(::Railwatch) && ::Railwatch.respond_to?(:enabled?)
80
- return false unless ::Railwatch.enabled? && ::Railwatch.config.local?
81
-
82
- path = ::Railwatch.config.writer_socket_path
83
- return false if path.nil?
84
- return true if ::Railwatch::Writer.usable_path?(path)
85
-
86
- unless @warned
87
- @warned = true
88
- log "Railwatch writer not started: socket path #{path} is #{path.bytesize} bytes, over the " \
89
- "#{::Railwatch::Writer::MAX_SOCKET_PATH}-byte limit; set RAILWATCH_WRITER_SOCKET to a shorter path. " \
90
- "Batches are written in-process meanwhile."
91
- end
92
- false
93
- end
94
-
95
- def spawn_writer
96
- @writer_pid = ::Railwatch::Writer.fork_writer! do
97
- ::Railwatch::Writer.run!(parent: @puma_pid)
98
- end
99
- log "Railwatch writer started (pid #{@writer_pid})"
100
- rescue SystemCallError => e
101
- @writer_pid = nil
102
- log "Railwatch writer could not be forked (#{e.class}: #{e.message}); retrying"
103
- end
104
-
105
- # Puma's cluster reaps every child with wait2(-1), the writer included, so
106
- # waitpid on the writer's pid raises ECHILD after it has died. Liveness is
107
- # asked with signal 0 instead, which works whoever reaped it.
108
- def supervise
109
- loop do
110
- sleep POLL
111
- break if @shutting_down
112
- next unless @booted && active?
113
- next if writer_alive?
114
-
115
- log "Railwatch writer (pid #{@writer_pid}) is gone; starting" if @writer_pid
116
- spawn_writer
117
- end
118
- end
119
-
120
- def writer_alive?
121
- return false unless @writer_pid
122
-
123
- Process.kill(0, @writer_pid)
124
- # A zombie the cluster has not reaped yet still answers signal 0.
125
- !Process.waitpid(@writer_pid, Process::WNOHANG)
126
- rescue Errno::ESRCH
127
- false
128
- rescue Errno::ECHILD
129
- # Already reaped by the cluster: alive iff it still exists for kill 0.
130
- begin
131
- Process.kill(0, @writer_pid)
132
- true
133
- rescue Errno::ESRCH
134
- false
135
- end
136
- end
137
-
138
- # For a restart: stop the writer and mark the cluster as not booted, so the
139
- # supervisor spawns a fresh one once after_booted fires again.
140
- def restart_writer
141
- @booted = false
142
- stop_writer
143
- end
144
-
145
- # Runs from at_exit, after Puma's run loop has returned and every request
146
- # has been served, in both single and cluster mode. Flushes this process's
147
- # own reporter (its process and health records, and in single mode the
148
- # requests' records) while the writer is still listening, then stops it.
149
- def shutdown_writer
150
- @shutting_down = true
151
- if defined?(::Railwatch) && ::Railwatch.respond_to?(:enabled?) && ::Railwatch.enabled? && ::Railwatch.config.local?
152
- ::Railwatch.reporter.shutdown
153
- end
154
- stop_writer
155
- rescue StandardError => e
156
- log "Railwatch writer shutdown failed (#{e.class}: #{e.message})"
157
- end
158
-
159
- # TERM closes the writer's listener; it finishes what it is holding and
160
- # exits on its own. Waited for with a deadline, not Process.wait, which
161
- # has none: a writer wedged in a SQLite write or on a full disk would hold
162
- # Puma's exit open for as long as it stayed wedged. KILL past the deadline.
163
- # Reaped either way, so a cluster master never leaves a zombie behind.
164
- def stop_writer
165
- return unless @writer_pid
166
-
167
- Process.kill(:TERM, @writer_pid)
168
- return if reaped_within?(stop_timeout)
169
-
170
- log "Railwatch writer (pid #{@writer_pid}) did not exit within #{stop_timeout}s of TERM; killing it"
171
- Process.kill(:KILL, @writer_pid)
172
- log "Railwatch writer (pid #{@writer_pid}) did not exit on KILL; leaving it" unless reaped_within?(KILL_REAP)
173
- rescue Errno::ECHILD, Errno::ESRCH
174
- nil
175
- ensure
176
- @writer_pid = nil
177
- end
178
-
179
- # shutdown_timeout: the same allowance this process gives its own
180
- # reporter, and deliberately NOT the writer's full theoretical exit time
181
- # (a sequential SHUTDOWN_DRAIN join per worker thread, its maintenance
182
- # join, then its own reporter shutdown -- 13s at the defaults). Waiting
183
- # that long would buy nothing: a writer killed mid-batch loses no data,
184
- # because the transaction rolls back and the worker retries the batch by
185
- # id against the next writer (Writer#serve says so). Exit time spent
186
- # waiting for the drain is spent for nothing. An idle writer is gone in
187
- # well under a second either way.
188
- #
189
- # This runs from at_exit, inside the container's TERM-to-KILL grace. Under
190
- # Kamal that is Docker's 10s default for a proxied role that has not set
191
- # `stop_timeout`, or whatever `stop_timeout` says when it has; an app that
192
- # wants the writer given longer can raise RAILWATCH_SHUTDOWN_TIMEOUT to
193
- # match its grace, and this bound rises with it.
194
- def stop_timeout
195
- [ ::Railwatch.config.shutdown_timeout.to_f, 0.0 ].max
196
- end
197
-
198
- # Non-blocking waits on a short poll; Process.wait has no timeout and a
199
- # child that never exits would hold it forever. ECHILD (the cluster's
200
- # wait2(-1) reaped it first) propagates to stop_writer, which reads it as
201
- # gone.
202
- def reaped_within?(seconds)
203
- deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
204
- loop do
205
- return true if Process.waitpid(@writer_pid, Process::WNOHANG)
206
- return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
207
-
208
- sleep REAP_POLL
209
- end
210
- end
211
-
212
- def log(message)
213
- log_writer.log(message)
214
- end
215
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rack/files"
4
-
5
- module Railwatch
6
- # Serves the prebuilt dashboard bundle the gem ships under public/railwatch.
7
- # Vite wrote every asset URL with base /railwatch/assets/, so a request for
8
- # /railwatch/assets/assets/app-abc123.js maps to public/railwatch/assets/
9
- # app-abc123.js. Files are content-hashed, so they are immutable. The
10
- # favicon is the one unhashed file, served with a short cache instead.
11
- class DashboardAssets
12
- PREFIX = "/railwatch/assets/"
13
- ICONS = { "/railwatch/icon.svg" => "/icon.svg", "/railwatch/icon.png" => "/icon.png" }.freeze
14
-
15
- # Put this middleware in the host's stack. Before ActionDispatch::Static
16
- # when the host has it, which is what keeps a same-named file in the
17
- # app's own public/ from shadowing the dashboard's. An app that serves
18
- # static files from nginx, a CDN or Thruster sets
19
- # `config.public_file_server.enabled = false` and has no such middleware:
20
- # inserting relative to it would raise and take the whole application
21
- # down at boot, so go to the front instead, which is the same position
22
- # relative to everything that remains.
23
- def self.install!(app, root:)
24
- if app.config.public_file_server.enabled
25
- app.middleware.insert_before ActionDispatch::Static, self, root: root
26
- else
27
- app.middleware.insert 0, self, root: root
28
- end
29
- end
30
-
31
- def initialize(app, root:)
32
- @app = app
33
- @files = Rack::Files.new(root, { "cache-control" => "public, max-age=31536000, immutable" })
34
- @icons = Rack::Files.new(root, { "cache-control" => "public, max-age=86400" })
35
- end
36
-
37
- def call(env)
38
- path = env["PATH_INFO"]
39
- return @icons.call(env.merge("PATH_INFO" => ICONS[path])) if ICONS.key?(path)
40
- return @app.call(env) unless path.start_with?(PREFIX) && !path.include?("..")
41
-
42
- @files.call(env.merge("PATH_INFO" => path.delete_prefix("/railwatch/assets")))
43
- end
44
- end
45
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Process-local state for the embedded (in-process) mode and the stand-ins
5
- # the platform's code expects from its account layer.
6
- module Embedded
7
- # Relation-shaped empty set for platform associations an embedded install
8
- # does not have (Linear links, integrations). Every scope returns itself.
9
- NONE = Class.new do
10
- def method_missing(*) = self
11
- def respond_to_missing?(*) = true
12
- def to_a = []
13
- def each(&) = [].each(&)
14
- def first = nil
15
- def map(&) = []
16
- def count = 0
17
- def any? = false
18
- def none? = true
19
- def exists?(*) = false
20
- def empty? = true
21
- def limit(*) = self
22
- def where(*) = self
23
- def find_each(&) = nil
24
- end.new.freeze
25
-
26
- # The platform scopes everything by account (plan, quota, retention,
27
- # members). One embedded install is one account with none of that.
28
- module Account
29
- module_function
30
-
31
- def id = 1
32
- def name = Rails.application.class.module_parent_name
33
- def slug = name.parameterize
34
- def plan = "embedded"
35
- def retention_days = Railwatch.config.retention_days
36
- def auto_resolve_after_days = 14
37
- # Whoever the host's resolver named for this request, not the anonymous
38
- # placeholder: an embedded install has no member list, so the account's
39
- # one "member" is the person looking at it.
40
- def users = [ User.current || User.default ]
41
- def members = users
42
- def memberships = NONE
43
- def integrations = NONE
44
- def applications = [ Application.current ]
45
- def environments = [ Environment.current ]
46
- def quota_exhausted? = false
47
- def as_json(*) = { id: id, name: name, slug: slug, plan: plan }
48
- end
49
- end
50
- end
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- module Export
5
- # Speaks the receipt protocol: names the delivery so the receiver can
6
- # recognise a repeat of it, and turns the answer into something the queue
7
- # can act on.
8
- #
9
- # It makes one attempt and has no opinion about when to try again. That
10
- # belongs to the queue, which is the thing with durable storage.
11
- class Client
12
- Outcome = Struct.new(:disposition, :status, :reason, :retry_after_at, :ack, keyword_init: true)
13
-
14
- # The queue is bound to a fingerprint of the export credential, so the
15
- # request has to be made with that credential: authenticating as someone
16
- # else would deliver these bytes to a tenant that never admitted them.
17
- def initialize(config)
18
- @config = config
19
- @transports = {}
20
- end
21
-
22
- # Not `send`: shadowing Object#send on a class makes it impossible to
23
- # reach the real one, and reads as a coincidence rather than a verb.
24
- def deliver(claim, producer_id:)
25
- # Snapshot once, then verify and use those exact bytes. Configuration
26
- # can change after Outbox#claim! checked the destination binding.
27
- token = @config.resolved_export_token.to_s.dup.freeze
28
- unless Digest::SHA256.hexdigest(token) == claim.token_digest
29
- return Outcome.new(disposition: :deferred, status: 401, reason: "export credential changed after claim")
30
- end
31
-
32
- result = transport_for(claim, token: token).deliver_encoded(
33
- body: claim.body, expected_count: claim.record_count, batch_id: claim.delivery_id,
34
- dropped: claim.metadata.fetch("dropped", 0).to_i,
35
- dropped_bytes: claim.metadata.fetch("dropped_bytes", 0).to_i,
36
- # Stored with the delivery, not read from the reporter now: this is
37
- # what the batch was carrying when it was queued.
38
- backpressure_factor: claim.metadata.fetch("backpressure_factor", 1.0).to_f,
39
- gem_version: claim.metadata.fetch("version", Railwatch::VERSION),
40
- headers: headers(claim, producer_id)
41
- )
42
- interpret(result)
43
- end
44
-
45
- def reset_after_fork!
46
- @transports = {}
47
- self
48
- end
49
-
50
- # Drops any latch the transports have picked up. After a credential
51
- # problem is fixed, the next send must be an actual send.
52
- def reset!
53
- @transports = {}
54
- self
55
- end
56
-
57
- private
58
-
59
- # Keyed on the destination this delivery is FOR, not on whatever the
60
- # configuration says right now. A process reconfigured mid-flight would
61
- # otherwise post bytes admitted for one receiver to another one -- and,
62
- # if they belong to different tenants, hand a customer's telemetry to
63
- # somebody else while marking it delivered.
64
- def transport_for(claim, token:)
65
- key = [ claim.url, claim.token_digest ]
66
- @transports[key] ||= begin
67
- credentialed = @config.dup
68
- credentialed.token = token
69
- Transport::Http.new(credentialed, endpoint: claim.url)
70
- end
71
- end
72
-
73
- def headers(claim, producer_id)
74
- {
75
- "X-Railwatch-Producer-Id" => producer_id,
76
- "X-Railwatch-Body-SHA256" => Digest::SHA256.hexdigest(claim.body),
77
- "X-Railwatch-Policy" => claim.metadata.fetch("policy", Policy::Everything::VERSION)
78
- }
79
- end
80
-
81
- # The receiver's answer, reduced to what the queue needs to decide.
82
- # Anything it cannot read is treated as "not stored": keeping bytes we
83
- # might not need costs a retry, discarding bytes that never arrived
84
- # costs the telemetry.
85
- STORED = %w[committed already_committed].freeze
86
- REJECTING_STATUSES = [ 400, 409, 410, 413, 422 ].freeze
87
-
88
- def interpret(result)
89
- return Outcome.new(disposition: :stored, status: result.status, ack: result.to_h) if stored?(result)
90
- return Outcome.new(disposition: :rejected, status: result.status, reason: reason_for(result)) if rejected?(result)
91
-
92
- Outcome.new(disposition: :deferred, status: result.status, reason: reason_for(result),
93
- retry_after_at: result.retry_after_at)
94
- end
95
-
96
- # A 200 that stored the records, or a duplicate the receiver already
97
- # holds. When the receiver names what it did, we believe the name and
98
- # not the arithmetic: counts that happen to add up are not a receipt.
99
- def stored?(result)
100
- return false unless result.ok && !result.deferred?
101
- return STORED.include?(result.ack_disposition) if result.ack_disposition
102
-
103
- true
104
- end
105
-
106
- # Permanently unacceptable TO THE RECEIVER: sending it again cannot
107
- # change the answer. 409 is a delivery id reused for different bytes,
108
- # 410 one too old to be recognised.
109
- #
110
- # Deliberately not our own refusals. `:permanent` from the transport
111
- # means we declined to send -- a latched credential failure, a
112
- # configuration we will not use -- and throwing the bytes away because
113
- # of something on this side would destroy telemetry that was never
114
- # offered to anyone.
115
- def rejected?(result)
116
- REJECTING_STATUSES.include?(result.status)
117
- end
118
-
119
- def reason_for(result)
120
- (result.reason || result.error || result.status).to_s[0, 64]
121
- end
122
- end
123
- end
124
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- module Export
5
- # Which process is allowed to send, right now.
6
- #
7
- # Every eligible process runs a sender; the lease decides which one does
8
- # anything. It is held for a short time and renewed, so a process that
9
- # dies holding it blocks the queue for seconds rather than forever.
10
- #
11
- # The generation is a fence. A holder that stalls past its expiry may wake
12
- # and finish a request it had already started -- the receipt at the other
13
- # end makes that harmless -- but it must not then be able to overwrite
14
- # what the new holder has since recorded. Every write it attempts carries
15
- # the generation it was granted, and a stale one matches nothing.
16
- module Lease
17
- TTL = 30
18
- # Renewed well inside the TTL: a renewal that has to wait on the write
19
- # lock still has room to land before the lease it is extending lapses.
20
- RENEW_EVERY = 10
21
-
22
- module_function
23
-
24
- # Takes the lease, or extends it if we already hold it. Returns the
25
- # generation we hold it under, or nil if someone else has it.
26
- def acquire(destination_id, owner:, now: Time.current)
27
- taken = Telemetry::ExportDestination
28
- .where(id: destination_id)
29
- .where("lease_owner IS NULL OR lease_owner = ? OR lease_expires_at < ?", owner, now)
30
- .update_all([
31
- "lease_owner = ?, lease_expires_at = ?, lease_generation = lease_generation + 1, updated_at = ?",
32
- owner, now + TTL, now
33
- ])
34
- return nil if taken.zero?
35
-
36
- Telemetry::ExportDestination.where(id: destination_id).pick(:lease_generation)
37
- end
38
-
39
- # Extends a lease we still hold. Cannot revive an expired one: taking it
40
- # again is acquire's job, and that increments the generation so anything
41
- # in flight under the old one is fenced out.
42
- def renew(destination_id, owner:, generation:, now: Time.current)
43
- Telemetry::ExportDestination
44
- .where(id: destination_id, lease_owner: owner, lease_generation: generation)
45
- .where(lease_expires_at: now..)
46
- .update_all([ "lease_expires_at = ?, updated_at = ?", now + TTL, now ]) == 1
47
- end
48
-
49
- def release(destination_id, owner:, generation:, now: Time.current)
50
- Telemetry::ExportDestination
51
- .where(id: destination_id, lease_owner: owner, lease_generation: generation)
52
- .update_all([ "lease_owner = NULL, lease_expires_at = NULL, updated_at = ?", now ]) == 1
53
- end
54
- end
55
- end
56
- end