railwatch 0.1.4 → 0.2.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 (302) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -0
  3. data/README.md +10 -0
  4. data/app/channels/railwatch/environment_channel.rb +28 -0
  5. data/app/controllers/concerns/railwatch/telemetry_identity.rb +25 -0
  6. data/app/controllers/railwatch/alerts_controller.rb +48 -0
  7. data/app/controllers/railwatch/anomaly_rules_controller.rb +31 -0
  8. data/app/controllers/railwatch/attachments_controller.rb +22 -0
  9. data/app/controllers/railwatch/beacon_controller.rb +67 -7
  10. data/app/controllers/railwatch/broadcasts_controller.rb +15 -0
  11. data/app/controllers/railwatch/cache_events_controller.rb +30 -0
  12. data/app/controllers/railwatch/commands_controller.rb +16 -0
  13. data/app/controllers/railwatch/comments_controller.rb +11 -0
  14. data/app/controllers/railwatch/dashboard_controller.rb +69 -0
  15. data/app/controllers/railwatch/deploys_controller.rb +34 -0
  16. data/app/controllers/railwatch/deprecations_controller.rb +54 -0
  17. data/app/controllers/railwatch/environment_scoped.rb +99 -0
  18. data/app/controllers/railwatch/exceptions_controller.rb +53 -0
  19. data/app/controllers/railwatch/executions_controller.rb +13 -0
  20. data/app/controllers/railwatch/issues_controller.rb +258 -0
  21. data/app/controllers/railwatch/jobs_controller.rb +63 -0
  22. data/app/controllers/railwatch/llm_calls_controller.rb +124 -0
  23. data/app/controllers/railwatch/logs_controller.rb +40 -0
  24. data/app/controllers/railwatch/mails_controller.rb +19 -0
  25. data/app/controllers/railwatch/notifications_controller.rb +11 -0
  26. data/app/controllers/railwatch/outgoing_requests_controller.rb +19 -0
  27. data/app/controllers/railwatch/overview_controller.rb +38 -0
  28. data/app/controllers/railwatch/people_controller.rb +25 -0
  29. data/app/controllers/railwatch/processes_controller.rb +33 -0
  30. data/app/controllers/railwatch/profiles_controller.rb +50 -0
  31. data/app/controllers/railwatch/queries_controller.rb +59 -0
  32. data/app/controllers/railwatch/releases_controller.rb +75 -0
  33. data/app/controllers/railwatch/requests_controller.rb +55 -0
  34. data/app/controllers/railwatch/saved_views_controller.rb +53 -0
  35. data/app/controllers/railwatch/scheduled_tasks_controller.rb +45 -0
  36. data/app/controllers/railwatch/spans_controller.rb +47 -0
  37. data/app/controllers/railwatch/storage_ops_controller.rb +15 -0
  38. data/app/controllers/railwatch/tenants_controller.rb +44 -0
  39. data/app/controllers/railwatch/thresholds_controller.rb +40 -0
  40. data/app/controllers/railwatch/traces_controller.rb +72 -0
  41. data/app/controllers/railwatch/transactions_controller.rb +21 -0
  42. data/app/controllers/railwatch/view_renders_controller.rb +28 -0
  43. data/app/controllers/railwatch/visits_controller.rb +53 -0
  44. data/app/helpers/railwatch/assets_helper.rb +52 -0
  45. data/app/jobs/railwatch/anomaly_scan_job.rb +11 -0
  46. data/app/jobs/railwatch/application_job.rb +7 -0
  47. data/app/jobs/railwatch/auto_resolve_issues_job.rb +19 -0
  48. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +113 -0
  49. data/app/jobs/railwatch/detect_anomalies_job.rb +171 -0
  50. data/app/jobs/railwatch/detect_performance_issues_job.rb +85 -0
  51. data/app/jobs/railwatch/group_exceptions_job.rb +91 -0
  52. data/app/jobs/railwatch/optimize_telemetry_job.rb +23 -0
  53. data/app/jobs/railwatch/performance_scan_job.rb +11 -0
  54. data/app/jobs/railwatch/prune_telemetry_job.rb +110 -0
  55. data/app/jobs/railwatch/release_health_rollup_job.rb +64 -0
  56. data/app/jobs/railwatch/rollup_catchup_job.rb +21 -0
  57. data/app/jobs/railwatch/rollup_job.rb +130 -0
  58. data/app/jobs/railwatch/scheduled_task_scan_job.rb +11 -0
  59. data/app/models/concerns/railwatch/detection_snapshotting.rb +20 -0
  60. data/app/models/railwatch/alert.rb +229 -0
  61. data/app/models/railwatch/alert_rule.rb +121 -0
  62. data/app/models/railwatch/anomaly_rule.rb +33 -0
  63. data/app/models/railwatch/application.rb +44 -0
  64. data/app/models/railwatch/application_record.rb +33 -0
  65. data/app/models/railwatch/comment.rb +36 -0
  66. data/app/models/railwatch/deploy.rb +67 -0
  67. data/app/models/railwatch/environment.rb +54 -0
  68. data/app/models/railwatch/execution_presenter.rb +185 -0
  69. data/app/models/railwatch/filter_query.rb +143 -0
  70. data/app/models/railwatch/followup_receipt.rb +27 -0
  71. data/app/models/railwatch/ingest/batch.rb +305 -0
  72. data/app/models/railwatch/ingest/mapper.rb +575 -0
  73. data/app/models/railwatch/ingest/payload.rb +96 -0
  74. data/app/models/railwatch/ingest/rollup_absorber.rb +170 -0
  75. data/app/models/railwatch/ingest/writer.rb +137 -0
  76. data/app/models/railwatch/issue.rb +277 -0
  77. data/app/models/railwatch/issue_activity.rb +23 -0
  78. data/app/models/railwatch/issue_detection_presenter.rb +309 -0
  79. data/app/models/railwatch/issue_detection_snapshot.rb +77 -0
  80. data/app/models/railwatch/maintenance_task.rb +53 -0
  81. data/app/models/railwatch/saved_view.rb +63 -0
  82. data/app/models/railwatch/telemetry/aggregations.rb +116 -0
  83. data/app/models/railwatch/telemetry/attachment.rb +31 -0
  84. data/app/models/railwatch/telemetry/bounded_gzip.rb +101 -0
  85. data/app/models/railwatch/telemetry/broadcast.rb +13 -0
  86. data/app/models/railwatch/telemetry/cache_event.rb +16 -0
  87. data/app/models/railwatch/telemetry/child.rb +53 -0
  88. data/app/models/railwatch/telemetry/cursor_page.rb +99 -0
  89. data/app/models/railwatch/telemetry/deprecation.rb +13 -0
  90. data/app/models/railwatch/telemetry/enqueued_job.rb +13 -0
  91. data/app/models/railwatch/telemetry/exception.rb +21 -0
  92. data/app/models/railwatch/telemetry/execution.rb +71 -0
  93. data/app/models/railwatch/telemetry/health_sample.rb +72 -0
  94. data/app/models/railwatch/telemetry/ingest_batch.rb +31 -0
  95. data/app/models/railwatch/telemetry/llm_call.rb +37 -0
  96. data/app/models/railwatch/telemetry/log.rb +85 -0
  97. data/app/models/railwatch/telemetry/mail.rb +13 -0
  98. data/app/models/railwatch/telemetry/n_plus_one.rb +92 -0
  99. data/app/models/railwatch/telemetry/notification.rb +13 -0
  100. data/app/models/railwatch/telemetry/outgoing_request.rb +13 -0
  101. data/app/models/railwatch/telemetry/person.rb +50 -0
  102. data/app/models/railwatch/telemetry/process.rb +10 -0
  103. data/app/models/railwatch/telemetry/profile.rb +37 -0
  104. data/app/models/railwatch/telemetry/query.rb +50 -0
  105. data/app/models/railwatch/telemetry/query_shape.rb +45 -0
  106. data/app/models/railwatch/telemetry/release_health.rb +63 -0
  107. data/app/models/railwatch/telemetry/rollup.rb +78 -0
  108. data/app/models/railwatch/telemetry/session.rb +24 -0
  109. data/app/models/railwatch/telemetry/span.rb +19 -0
  110. data/app/models/railwatch/telemetry/storage_op.rb +13 -0
  111. data/app/models/railwatch/telemetry/tenant.rb +221 -0
  112. data/app/models/railwatch/telemetry/transaction.rb +13 -0
  113. data/app/models/railwatch/telemetry/view_render.rb +13 -0
  114. data/app/models/railwatch/telemetry/visit.rb +24 -0
  115. data/app/models/railwatch/telemetry_record.rb +57 -0
  116. data/app/models/railwatch/threshold.rb +29 -0
  117. data/app/models/railwatch/user.rb +47 -0
  118. data/app/models/railwatch/viewer.rb +13 -0
  119. data/app/views/layouts/railwatch/dashboard.html.erb +25 -0
  120. data/config/routes.rb +59 -0
  121. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +151 -0
  122. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +18 -0
  123. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +20 -0
  124. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +71 -0
  125. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +481 -0
  126. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +14 -0
  127. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +9 -0
  128. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +10 -0
  129. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +9 -0
  130. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +82 -0
  131. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +10 -0
  132. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +60 -0
  133. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +9 -0
  134. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +51 -0
  135. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +11 -0
  136. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +10 -0
  137. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +37 -0
  138. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +11 -0
  139. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +15 -0
  140. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +9 -0
  141. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +10 -0
  142. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +9 -0
  143. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +18 -0
  144. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +25 -0
  145. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +55 -0
  146. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +21 -0
  147. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +16 -0
  148. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +18 -0
  149. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +16 -0
  150. data/docs/configuration.md +26 -0
  151. data/docs/embedded.md +352 -0
  152. data/docs/getting-started.md +5 -0
  153. data/lib/generators/railwatch/install/install_generator.rb +222 -1
  154. data/lib/generators/railwatch/install/templates/{initializer.rb → initializer.rb.tt} +39 -0
  155. data/lib/generators/railwatch/install/templates/post-deploy +8 -0
  156. data/lib/puma/plugin/railwatch.rb +170 -0
  157. data/lib/railwatch/authentication.rb +83 -0
  158. data/lib/railwatch/configuration.rb +134 -3
  159. data/lib/railwatch/dashboard_assets.rb +45 -0
  160. data/lib/railwatch/embedded.rb +54 -0
  161. data/lib/railwatch/engine.rb +89 -1
  162. data/lib/railwatch/ingest_request_body_limit.rb +10 -0
  163. data/lib/railwatch/json_compat.rb +60 -0
  164. data/lib/railwatch/maintenance.rb +183 -0
  165. data/lib/railwatch/patches/runner_command.rb +21 -1
  166. data/lib/railwatch/record.rb +33 -8
  167. data/lib/railwatch/reporter.rb +16 -0
  168. data/lib/railwatch/subscribers/process_info.rb +2 -1
  169. data/lib/railwatch/transport/local.rb +78 -0
  170. data/lib/railwatch/transport/socket.rb +183 -0
  171. data/lib/railwatch/version.rb +1 -1
  172. data/lib/railwatch/writer.rb +370 -0
  173. data/lib/railwatch.rb +38 -1
  174. data/lib/tasks/railwatch_tasks.rake +128 -0
  175. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  176. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  177. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  178. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  179. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  180. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  181. data/public/railwatch/assets/app-layout-yh-sPWgK.js +1 -0
  182. data/public/railwatch/assets/app-wordmark-nbjkzxwQ.js +1 -0
  183. data/public/railwatch/assets/appearance-CDuRvQTB.js +1 -0
  184. data/public/railwatch/assets/application-C_kpBdnf.css +1 -0
  185. data/public/railwatch/assets/arrow-up-DVtOGdVA.js +1 -0
  186. data/public/railwatch/assets/auth-layout-TCwPpS1K.js +1 -0
  187. data/public/railwatch/assets/badge-Daw4Hvr8.js +1 -0
  188. data/public/railwatch/assets/braces-DhFbHPsz.js +1 -0
  189. data/public/railwatch/assets/card-BX_3HXcJ.js +1 -0
  190. data/public/railwatch/assets/chart-B14-N9g7.js +39 -0
  191. data/public/railwatch/assets/chart-hover-Vy52H4uD.js +1 -0
  192. data/public/railwatch/assets/chart-panel-Cb4S_ej_.js +1 -0
  193. data/public/railwatch/assets/checkbox-D3aRSsBj.js +1 -0
  194. data/public/railwatch/assets/code-KvW8k7Jr.js +1 -0
  195. data/public/railwatch/assets/copy-DaQMJWoT.js +1 -0
  196. data/public/railwatch/assets/copy-block-BKFGd11J.js +1 -0
  197. data/public/railwatch/assets/copy-id-Djks1fXB.js +1 -0
  198. data/public/railwatch/assets/cursor-load-more-BXV0f1D_.js +1 -0
  199. data/public/railwatch/assets/data-table-iv1bdF6u.js +1 -0
  200. data/public/railwatch/assets/edit-BZc_Iawe.js +1 -0
  201. data/public/railwatch/assets/edit-DMKUF8Zi.js +8 -0
  202. data/public/railwatch/assets/edit-__9yJlO3.js +1 -0
  203. data/public/railwatch/assets/empty-state-6j_0AaQQ.js +1 -0
  204. data/public/railwatch/assets/env-layout-DrT8rO6P.js +1 -0
  205. data/public/railwatch/assets/execution-path-CzgBUi5e.js +1 -0
  206. data/public/railwatch/assets/filter-bar-9SU5NrzX.js +1 -0
  207. data/public/railwatch/assets/flamegraph-qcekju8V.js +2 -0
  208. data/public/railwatch/assets/format-B9SDkrWj.js +1 -0
  209. data/public/railwatch/assets/frames-Cyu7KMxZ.js +1 -0
  210. data/public/railwatch/assets/google-sign-in-button-DsTSfmzY.js +1 -0
  211. data/public/railwatch/assets/index-1ol1-QWI.js +1 -0
  212. data/public/railwatch/assets/index-5jI4aFzC.js +1 -0
  213. data/public/railwatch/assets/index-9KTrVnrc.js +1 -0
  214. data/public/railwatch/assets/index-B0-8lcTp.js +1 -0
  215. data/public/railwatch/assets/index-B7jjfNfO.js +1 -0
  216. data/public/railwatch/assets/index-BBchRy0M.js +1 -0
  217. data/public/railwatch/assets/index-BRiq3SNR.js +1 -0
  218. data/public/railwatch/assets/index-BgKj9xhr.js +1 -0
  219. data/public/railwatch/assets/index-BkTZqqOu.js +1 -0
  220. data/public/railwatch/assets/index-BprKx8QO.js +1 -0
  221. data/public/railwatch/assets/index-C3jzvPs3.js +1 -0
  222. data/public/railwatch/assets/index-Cbs6gGyQ.js +1 -0
  223. data/public/railwatch/assets/index-CdRZ6AWF.js +1 -0
  224. data/public/railwatch/assets/index-CeYKnapu.js +1 -0
  225. data/public/railwatch/assets/index-Cmlwy1-V.js +1 -0
  226. data/public/railwatch/assets/index-Cwx6058d.js +1 -0
  227. data/public/railwatch/assets/index-D4CSdbHv.js +1 -0
  228. data/public/railwatch/assets/index-DEFMSkdG.js +1 -0
  229. data/public/railwatch/assets/index-DFiHEBSh.js +1 -0
  230. data/public/railwatch/assets/index-DL4vWdWJ.js +1 -0
  231. data/public/railwatch/assets/index-DU9F5b5d.js +1 -0
  232. data/public/railwatch/assets/index-DaXgPcGL.js +1 -0
  233. data/public/railwatch/assets/index-DbtaU-EE.js +1 -0
  234. data/public/railwatch/assets/index-DeOe83F4.js +1 -0
  235. data/public/railwatch/assets/index-DiucHN4B.js +1 -0
  236. data/public/railwatch/assets/index-DlnR_l9o.js +1 -0
  237. data/public/railwatch/assets/index-DlumCsWY.js +2 -0
  238. data/public/railwatch/assets/index-DmRd7aIG.js +1 -0
  239. data/public/railwatch/assets/index-DxSh2UpM.js +1 -0
  240. data/public/railwatch/assets/index-MIMGuFNt.js +1 -0
  241. data/public/railwatch/assets/index-OqI59zPb.js +1 -0
  242. data/public/railwatch/assets/index-P4rC7IlX.js +1 -0
  243. data/public/railwatch/assets/index-gpPOcFWq.js +1 -0
  244. data/public/railwatch/assets/index-oVkururr.js +1 -0
  245. data/public/railwatch/assets/index-p9puqVge.js +1 -0
  246. data/public/railwatch/assets/inertia-TViv6kNv.js +97 -0
  247. data/public/railwatch/assets/input-error-LxImUkxv.js +1 -0
  248. data/public/railwatch/assets/json-viewer-Ar4cjPDW.js +1 -0
  249. data/public/railwatch/assets/klass-CJ-J4INB.js +1 -0
  250. data/public/railwatch/assets/label-COUKWqE_.js +1 -0
  251. data/public/railwatch/assets/layout-DNSLAkw_.js +1 -0
  252. data/public/railwatch/assets/live-dot-ChfUtY3p.js +41 -0
  253. data/public/railwatch/assets/nav-CNnDqPlm.js +1 -0
  254. data/public/railwatch/assets/new-84S8ZJq9.js +1 -0
  255. data/public/railwatch/assets/new-Be55nmt9.js +1 -0
  256. data/public/railwatch/assets/new-Bi_xQiIb.js +1 -0
  257. data/public/railwatch/assets/new-BvCT8TMg.js +1 -0
  258. data/public/railwatch/assets/new-D05SajFR.js +1 -0
  259. data/public/railwatch/assets/new-D4uewYC8.js +1 -0
  260. data/public/railwatch/assets/onboarding-CYZi5Cqc.js +1 -0
  261. data/public/railwatch/assets/origin-identity-6q1-CBts.js +1 -0
  262. data/public/railwatch/assets/percentile-picker-gFZCXtdb.js +1 -0
  263. data/public/railwatch/assets/relative-time-IOOgl5n2.js +1 -0
  264. data/public/railwatch/assets/release-health-DC8oc7uw.js +1 -0
  265. data/public/railwatch/assets/route-Dv6LAWvT.js +1 -0
  266. data/public/railwatch/assets/segmented-h1VdDTqE.js +1 -0
  267. data/public/railwatch/assets/select-_AJsUa7X.js +1 -0
  268. data/public/railwatch/assets/separator-BwwTYtCF.js +1 -0
  269. data/public/railwatch/assets/series-chart-DaFPefku.js +1 -0
  270. data/public/railwatch/assets/show-B7NCgkEo.js +1 -0
  271. data/public/railwatch/assets/show-BKqyKjBK.js +1 -0
  272. data/public/railwatch/assets/show-BM6X2Mpo.js +1 -0
  273. data/public/railwatch/assets/show-BNw4tN5q.js +1 -0
  274. data/public/railwatch/assets/show-BO3bnG5h.js +1 -0
  275. data/public/railwatch/assets/show-BhrAVAEA.js +1 -0
  276. data/public/railwatch/assets/show-C4Ltf5i9.js +2 -0
  277. data/public/railwatch/assets/show-C8sHalnw.js +1 -0
  278. data/public/railwatch/assets/show-CeTL4B37.js +2 -0
  279. data/public/railwatch/assets/show-CpfgV1jP.js +1 -0
  280. data/public/railwatch/assets/show-DACku6AD.js +3 -0
  281. data/public/railwatch/assets/show-DIOSGcXV.js +6 -0
  282. data/public/railwatch/assets/show-DQp_1n-B.js +1 -0
  283. data/public/railwatch/assets/show-DVNz46RI.js +1 -0
  284. data/public/railwatch/assets/show-DYteoYWW.js +1 -0
  285. data/public/railwatch/assets/show-DgSIoRvA.js +1 -0
  286. data/public/railwatch/assets/show-JxFtB4eK.js +2 -0
  287. data/public/railwatch/assets/sort-header-DpFzXblu.js +1 -0
  288. data/public/railwatch/assets/source-link-B2183i2-.js +1 -0
  289. data/public/railwatch/assets/sparkline-cell-C3-5vFkP.js +1 -0
  290. data/public/railwatch/assets/stat-s4RpOS9w.js +1 -0
  291. data/public/railwatch/assets/status-badge-8jVV-LA4.js +1 -0
  292. data/public/railwatch/assets/tenant-path-G-6u9A-o.js +1 -0
  293. data/public/railwatch/assets/text-link-DfsiaCcP.js +1 -0
  294. data/public/railwatch/assets/textarea-Dye72uP7.js +1 -0
  295. data/public/railwatch/assets/timeline-CD7WHnbo.js +1 -0
  296. data/public/railwatch/assets/transition-B_AW8rMK.js +5 -0
  297. data/public/railwatch/assets/use-clipboard-ColgLyQ2.js +1 -0
  298. data/public/railwatch/icon.png +0 -0
  299. data/public/railwatch/icon.svg +5 -0
  300. data/public/railwatch/manifest.json +2171 -0
  301. data/public/railwatch/rails-vite.json +1 -0
  302. metadata +314 -4
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tier 1-3 telemetry: custom spans, process health samples, request queue
4
+ # time, web vitals on visits, and an FTS5 index over log messages.
5
+ class AddSpansHealthVitalsAndFts < ActiveRecord::Migration[8.1]
6
+ def up
7
+ create_table :spans do |t|
8
+ # The record envelope as it was when this migration shipped; inlined so
9
+ # the history never calls a model that has since changed.
10
+ t.datetime :occurred_at, null: false, precision: 6
11
+ t.string :deploy, limit: 128
12
+ t.string :server, limit: 255
13
+ t.string :group_hash, limit: 32
14
+ t.string :trace_id, limit: 36
15
+ t.string :execution_source, limit: 20
16
+ t.string :execution_id, limit: 36
17
+ t.string :execution_preview, limit: 255
18
+ t.string :execution_stage, limit: 32
19
+ t.string :user_ref, limit: 255
20
+ t.string :app_tenant, limit: 255
21
+ t.string :name, null: false, limit: 255
22
+ t.integer :duration, null: false # microseconds
23
+ t.json :attributes, null: false, default: {}
24
+ t.string :status, limit: 20 # ok, failed
25
+ end
26
+ add_index :spans, [ :group_hash, :occurred_at ]
27
+ add_index :spans, [ :execution_id, :occurred_at ]
28
+
29
+ # Periodic Puma / Solid Queue samples, one row per process per interval.
30
+ create_table :health_samples do |t|
31
+ t.datetime :sampled_at, null: false, precision: 6
32
+ t.integer :pid
33
+ t.string :role, limit: 20
34
+ t.string :server, limit: 255
35
+ t.string :deploy, limit: 128
36
+ t.integer :threads_max
37
+ t.integer :threads_busy
38
+ t.integer :backlog
39
+ t.integer :pool_size
40
+ t.integer :pool_busy
41
+ t.integer :pool_waiting
42
+ t.integer :queue_depth
43
+ t.integer :queue_latency # microseconds, oldest ready job
44
+ t.bigint :memory
45
+ t.json :detail, null: false, default: {}
46
+ end
47
+ add_index :health_samples, [ :server, :sampled_at ]
48
+ add_index :health_samples, :sampled_at
49
+
50
+ add_column :executions, :queue_time, :integer # microseconds, from X-Request-Start
51
+ add_column :executions, :parent_id, :string, limit: 36
52
+ add_index :executions, :parent_id
53
+
54
+ add_column :visits, :lcp, :integer # milliseconds
55
+ add_column :visits, :cls, :float
56
+ add_column :visits, :inp, :integer # milliseconds
57
+ add_column :visits, :ttfb, :integer # milliseconds
58
+
59
+ add_column :queries, :explain, :text # captured plan for slow queries (opt-in)
60
+
61
+ # External-content FTS5 index over log messages. Kept in sync explicitly
62
+ # by Ingest::Writer (insert) and PruneTelemetryJob (rebuild) rather than
63
+ # by triggers: the schema dumper carries create_virtual_table but not
64
+ # triggers, so a tenant created from telemetry_schema.rb would silently
65
+ # lose them.
66
+ execute "CREATE VIRTUAL TABLE logs_fts USING fts5(message, content='logs', content_rowid='id')"
67
+ execute "INSERT INTO logs_fts(rowid, message) SELECT id, message FROM logs"
68
+ end
69
+
70
+ def down
71
+ execute "DROP TABLE IF EXISTS logs_fts"
72
+ remove_column :queries, :explain
73
+ remove_column :visits, :lcp
74
+ remove_column :visits, :cls
75
+ remove_column :visits, :inp
76
+ remove_column :visits, :ttfb
77
+ remove_column :executions, :queue_time
78
+ remove_column :executions, :parent_id
79
+ drop_table :health_samples
80
+ drop_table :spans
81
+ end
82
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `attributes` is Active Record's own method; a column by that name raises
4
+ # DangerousAttributeError on first access. The wire key stays "attributes",
5
+ # Ingest::Mapper writes it to payload.
6
+ class RenameSpanAttributesToPayload < ActiveRecord::Migration[8.1]
7
+ def change
8
+ rename_column :spans, :attributes, :payload
9
+ end
10
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sampled CPU/wall profiles for an execution, attachments (files an app
4
+ # attaches to an execution or exception), and opt-in captured bodies.
5
+ class AddProfilesAndAttachments < ActiveRecord::Migration[8.1]
6
+ def change
7
+ create_table :profiles do |t|
8
+ # The record envelope as it was when this migration shipped; inlined so
9
+ # the history never calls a model that has since changed.
10
+ t.datetime :occurred_at, null: false, precision: 6
11
+ t.string :deploy, limit: 128
12
+ t.string :server, limit: 255
13
+ t.string :group_hash, limit: 32
14
+ t.string :trace_id, limit: 36
15
+ t.string :execution_source, limit: 20
16
+ t.string :execution_id, limit: 36
17
+ t.string :execution_preview, limit: 255
18
+ t.string :execution_stage, limit: 32
19
+ t.string :user_ref, limit: 255
20
+ t.string :app_tenant, limit: 255
21
+ t.string :profiler, null: false, limit: 16 # vernier, stackprof
22
+ t.string :mode, limit: 8 # wall, cpu
23
+ t.integer :interval # microseconds between samples
24
+ t.integer :duration, null: false # microseconds profiled
25
+ t.integer :samples, null: false # total samples collected
26
+ t.binary :stacks, null: false # gzip of collapsed stacks: "frame;frame;frame count\n"
27
+ t.integer :stacks_bytes # uncompressed size
28
+ end
29
+ add_index :profiles, [ :execution_id ]
30
+ add_index :profiles, [ :group_hash, :occurred_at ]
31
+ add_index :profiles, [ :occurred_at ]
32
+
33
+ create_table :attachments do |t|
34
+ # The record envelope as it was when this migration shipped; inlined so
35
+ # the history never calls a model that has since changed.
36
+ t.datetime :occurred_at, null: false, precision: 6
37
+ t.string :deploy, limit: 128
38
+ t.string :server, limit: 255
39
+ t.string :group_hash, limit: 32
40
+ t.string :trace_id, limit: 36
41
+ t.string :execution_source, limit: 20
42
+ t.string :execution_id, limit: 36
43
+ t.string :execution_preview, limit: 255
44
+ t.string :execution_stage, limit: 32
45
+ t.string :user_ref, limit: 255
46
+ t.string :app_tenant, limit: 255
47
+ t.string :name, null: false, limit: 255
48
+ t.string :content_type, limit: 128
49
+ t.integer :bytes, null: false
50
+ t.binary :data, null: false # gzip
51
+ t.string :exception_group_hash, limit: 32 # set when attached to an exception
52
+ end
53
+ add_index :attachments, [ :execution_id ]
54
+ add_index :attachments, [ :exception_group_hash, :occurred_at ]
55
+ add_index :attachments, [ :occurred_at ]
56
+
57
+ add_column :outgoing_requests, :response_body, :text # captured on error only, truncated
58
+ add_column :executions, :profile_id, :integer # denormalised: the execution has a profile
59
+ end
60
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The gem cuts an attachment off at its size limit and flags the wire
4
+ # record; without this column the UI cannot tell a whole file from a stub.
5
+ class AddTruncatedToAttachments < ActiveRecord::Migration[8.1]
6
+ def change
7
+ add_column :attachments, :truncated, :boolean, default: false, null: false
8
+ end
9
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Release health. `sessions` are the raw records the gem ships -- one per
4
+ # browser tab and per authenticated/cookied server session, repeated every
5
+ # flush interval, so a session id appears many times and the rollup keeps the
6
+ # worst status it saw. `release_health` is the hourly aggregate per deploy
7
+ # (the release) that the crash-free rates are read from.
8
+ class CreateSessionsAndReleaseHealth < ActiveRecord::Migration[8.1]
9
+ def change
10
+ create_table :sessions do |t|
11
+ # The record envelope as it was when this migration shipped; inlined so
12
+ # the history never calls a model that has since changed.
13
+ t.datetime :occurred_at, null: false, precision: 6
14
+ t.string :deploy, limit: 128
15
+ t.string :server, limit: 255
16
+ t.string :group_hash, limit: 32
17
+ t.string :trace_id, limit: 36
18
+ t.string :execution_source, limit: 20
19
+ t.string :execution_id, limit: 36
20
+ t.string :execution_preview, limit: 255
21
+ t.string :execution_stage, limit: 32
22
+ t.string :user_ref, limit: 255
23
+ t.string :app_tenant, limit: 255
24
+ t.string :session_id, null: false, limit: 64
25
+ t.string :source, limit: 16 # browser, server
26
+ t.string :status, limit: 16 # started, ok, errored, crashed
27
+ t.datetime :started_at, precision: 6
28
+ t.integer :duration # microseconds, null until the session has one
29
+ t.integer :requests, default: 0
30
+ t.integer :visits, default: 0
31
+ t.integer :error_count, default: 0 # `errors` is a reserved Active Record name
32
+ t.boolean :ended, default: false
33
+ end
34
+ add_index :sessions, [ :session_id, :occurred_at ]
35
+ add_index :sessions, [ :deploy, :occurred_at ]
36
+ add_index :sessions, [ :occurred_at ]
37
+
38
+ create_table :release_health do |t|
39
+ t.string :deploy, null: false, limit: 128
40
+ t.datetime :bucket, null: false, precision: 6
41
+ t.integer :sessions, default: 0
42
+ t.integer :sessions_errored, default: 0
43
+ t.integer :sessions_crashed, default: 0
44
+ t.integer :users, default: 0
45
+ t.integer :users_crashed, default: 0
46
+ t.bigint :duration_sum, default: 0
47
+ t.integer :duration_count, default: 0
48
+ end
49
+ add_index :release_health, [ :deploy, :bucket ], unique: true
50
+ end
51
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The parts the gem hashed this exception into an issue on, and where they
4
+ # came from (its own default, a resolver, a per-call fingerprint, or the
5
+ # error class itself) -- so an issue can show why it grouped the way it did.
6
+ class AddFingerprintToExceptions < ActiveRecord::Migration[8.1]
7
+ def change
8
+ add_column :exceptions, :fingerprint, :json, default: [], null: false
9
+ add_column :exceptions, :fingerprint_source, :string, limit: 16
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Whether a channel action raised. The gem reports the exception itself
4
+ # (source "application.action_cable"); this flag lets the Broadcasts page mark
5
+ # the failed action alongside its siblings, the way jobs and mail already do.
6
+ class AddFailedToBroadcasts < ActiveRecord::Migration[8.1]
7
+ def change
8
+ add_column :broadcasts, :failed, :boolean, default: false, null: false
9
+ end
10
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Keyset pagination orders every telemetry list by (sort column, id), so an
4
+ # index that stops at the sort column still leaves SQLite sorting ties in a
5
+ # temp B-tree. Each index below ends in id for that reason, and exists only
6
+ # where the leading filter column is selective enough that seeking beats
7
+ # scanning the ordering index and filtering.
8
+ #
9
+ # Deliberately NOT indexed, even though the FilterBar exposes them:
10
+ # exceptions.handled, queries.connection, queries.role, and executions.queue
11
+ # all carry a handful of distinct values in practice, so the existing ordering
12
+ # index plus a row filter is as good as a seek and costs nothing on write.
13
+ class AddFilterCursorIndexes < ActiveRecord::Migration[8.1]
14
+ def change
15
+ # Jobs page, filtered by tenant. index_executions_on_app_tenant_and_occurred_at
16
+ # cannot serve it: the page is already restricted to kind = "job".
17
+ add_index :executions, [ :kind, :app_tenant, :occurred_at, :id ], name: "idx_executions_kind_tenant_cursor"
18
+ # "Failed jobs" is the most-used view on that page and failures are rare,
19
+ # so the two-valued outcome column is highly selective here.
20
+ add_index :executions, [ :kind, :outcome, :occurred_at, :id ], name: "idx_executions_kind_outcome_cursor"
21
+
22
+ # exceptions has only (occurred_at); both of these filters are per-tenant
23
+ # or per-class needles in a large table.
24
+ add_index :exceptions, [ :app_tenant, :occurred_at, :id ], name: "idx_exceptions_tenant_cursor"
25
+ add_index :exceptions, [ :class_name, :occurred_at, :id ], name: "idx_exceptions_class_cursor"
26
+
27
+ add_index :logs, [ :app_tenant, :occurred_at, :id ], name: "idx_logs_tenant_cursor"
28
+ # Replaced, not added: the same index with the keyset tiebreaker appended.
29
+ remove_index :logs, [ :level, :occurred_at ], name: "index_logs_on_level_and_occurred_at"
30
+ add_index :logs, [ :level, :occurred_at, :id ], name: "idx_logs_level_cursor"
31
+
32
+ # The queries page sorts by duration, which index_queries_on_occurred_at_and_duration
33
+ # cannot order by. The first index serves the unfiltered page.
34
+ add_index :queries, [ :duration, :id ], name: "idx_queries_duration_cursor"
35
+ add_index :queries, [ :app_tenant, :duration, :id ], name: "idx_queries_tenant_duration_cursor"
36
+ end
37
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # n_plus_ones had no execution_id index, alone among the child tables, so
4
+ # every trace page and every get_route call that asked "which N+1s did
5
+ # these requests hit" scanned the table: 1.6 s at 70k rows on the
6
+ # platform's own tenant, and it only grows.
7
+ class AddNPlusOnesExecutionIdIndex < ActiveRecord::Migration[8.1]
8
+ def change
9
+ add_index :n_plus_ones, :execution_id, name: "index_n_plus_ones_on_execution_id"
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A query group's normalized statement, stored once; rows that carry
4
+ # exactly it store "" instead (docs/architecture.md). queries.sql keeps
5
+ # its NOT NULL: relaxing it would rewrite the table on every tenant, which
6
+ # SQLite cannot do in place and the deploy's health window cannot afford.
7
+ class CreateQueryShapes < ActiveRecord::Migration[8.1]
8
+ def change
9
+ create_table :query_shapes, id: false do |t|
10
+ t.string :group_hash, null: false, limit: 32
11
+ t.text :sql, null: false
12
+ end
13
+ add_index :query_shapes, :group_hash, unique: true
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The gem reports when load forces it to sample every execution kind, so the
4
+ # platform can distinguish quiet traffic from reduced client-side reporting.
5
+ class AddBackpressureFactorToIngestBatches < ActiveRecord::Migration[8.1]
6
+ def change
7
+ add_column :ingest_batches, :backpressure_factor, :float, null: false, default: 1.0
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The gem version a process reports. Column renamed with the platform; the
4
+ # ingest mapper accepts both wire spellings while environments still run the
5
+ # lantern gem.
6
+ class RenameLanternVersionOnProcesses < ActiveRecord::Migration[8.1]
7
+ def change
8
+ rename_column :processes, :lantern_version, :nightrail_version
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The gem version a process reports. Column renamed with the platform; the
4
+ # ingest mapper still accepts the pinned lantern gem's wire spelling.
5
+ class RenameNightrailVersionOnProcesses < ActiveRecord::Migration[8.1]
6
+ def change
7
+ rename_column :processes, :nightrail_version, :railwatch_version
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Telemetry half of the primary migration of the same name: #84's
4
+ # rollup_cursors and source_maps, and #60's release_health_finalizations,
5
+ # stayed behind in every tenant after their code was reverted or replaced.
6
+ class DropOrphanDurableIngestTables < ActiveRecord::Migration[8.1]
7
+ ORPHAN_TABLES = %w[release_health_finalizations rollup_cursors source_maps].freeze
8
+ ORPHAN_VERSIONS = %w[20260904000013 20260905000100 20260905000200 20260905000400].freeze
9
+
10
+ def up
11
+ ORPHAN_TABLES.each { |table| drop_table table, if_exists: true }
12
+ execute "DELETE FROM schema_migrations WHERE version IN (#{ORPHAN_VERSIONS.map { |v| "'#{v}'" }.join(", ")})"
13
+ end
14
+
15
+ def down
16
+ raise ActiveRecord::IrreversibleMigration, "the dropped tables belonged to reverted code; there is nothing to restore"
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The column half of the #84 cleanup (see 20260914000000): its migrations
4
+ # also added delivery-state columns to ingest_batches and resolved-frame
5
+ # columns to exceptions in every tenant before the code was reverted in
6
+ # 5521379. Nothing reads them, and the weekly restore drill fails the
7
+ # column comparison on every restored tenant. Indexes go first: SQLite
8
+ # refuses to drop an indexed column.
9
+ class DropOrphanDurableIngestColumns < ActiveRecord::Migration[8.1]
10
+ def up
11
+ remove_index :ingest_batches, :followups_completed_at, if_exists: true
12
+ remove_index :ingest_batches, :delivery_key, if_exists: true
13
+ remove_index :ingest_batches, :batch_id, if_exists: true
14
+ %i[batch_id delivery_key followups followups_completed_at].each do |column|
15
+ remove_column :ingest_batches, column if column_exists?(:ingest_batches, column)
16
+ end
17
+ %i[resolved_frames resolved_frames_key].each do |column|
18
+ remove_column :exceptions, column if column_exists?(:exceptions, column)
19
+ end
20
+ end
21
+
22
+ def down
23
+ raise ActiveRecord::IrreversibleMigration, "the dropped columns belonged to reverted code; there is nothing to restore"
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # One row per RubyLLM call: a model call of any operation, or a tool
4
+ # invocation (operation "tool", which carries a name and a duration but no
5
+ # tokens or cost). Token counts keep RubyLLM's normalized buckets rather
6
+ # than a provider's own names, and cost is nanodollars so a month of rollup
7
+ # sums stays exact -- nil there means unpriced, never free.
8
+ class CreateLlmCalls < ActiveRecord::Migration[8.1]
9
+ def change
10
+ create_table :llm_calls do |t|
11
+ # The record envelope as it was when this migration shipped; inlined so
12
+ # the history never calls a model that has since changed.
13
+ t.datetime :occurred_at, null: false, precision: 6
14
+ t.string :deploy, limit: 128
15
+ t.string :server, limit: 255
16
+ t.string :group_hash, limit: 32
17
+ t.string :trace_id, limit: 36
18
+ t.string :execution_source, limit: 20
19
+ t.string :execution_id, limit: 36
20
+ t.string :execution_preview, limit: 255
21
+ t.string :execution_stage, limit: 32
22
+ t.string :user_ref, limit: 255
23
+ t.string :app_tenant, limit: 255
24
+ t.string :operation, null: false, limit: 20
25
+ t.string :provider, limit: 64
26
+ t.string :model, limit: 255
27
+ t.string :response_model, limit: 255
28
+ t.string :tool_name, limit: 255
29
+ t.integer :duration, null: false
30
+ t.string :status, limit: 16
31
+ t.string :error, limit: 255
32
+ t.boolean :streaming
33
+ t.integer :message_count
34
+ t.integer :tool_count
35
+ t.integer :input_tokens
36
+ t.integer :output_tokens
37
+ t.integer :cache_read_tokens
38
+ t.integer :cache_write_tokens
39
+ t.integer :thinking_tokens
40
+ t.bigint :cost_nanos
41
+ # Present only for calls made inside RubyLLM.workflow (2.0 and up).
42
+ # The parent step id is what reconstructs a nested agent run.
43
+ t.string :workflow_id, limit: 64
44
+ t.string :workflow_name, limit: 255
45
+ t.string :workflow_step_id, limit: 64
46
+ t.string :workflow_step_name, limit: 255
47
+ t.string :workflow_step_parent_id, limit: 64
48
+ t.text :prompt
49
+ t.text :completion
50
+ end
51
+ add_index :llm_calls, [ :group_hash, :occurred_at ]
52
+ add_index :llm_calls, [ :execution_id ]
53
+ add_index :llm_calls, [ :workflow_id, :occurred_at ]
54
+ end
55
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What an LLM call carried and how it was configured, alongside what it
4
+ # cost. finish_reason is the one that changes what you can see: max_tokens
5
+ # means the answer was cut off, and a truncated extraction otherwise reads
6
+ # exactly like a complete one.
7
+ class AddDetailToLlmCalls < ActiveRecord::Migration[8.1]
8
+ def change
9
+ change_table :llm_calls, bulk: true do |t|
10
+ t.string :finish_reason, limit: 32
11
+ t.string :provider_request_id, limit: 128
12
+ t.string :tools, limit: 1024
13
+ t.boolean :cost_reported
14
+ t.integer :attachments
15
+ t.string :attachment_types, limit: 128
16
+ t.string :attachment_names, limit: 1024
17
+ t.string :tool_call_id, limit: 128
18
+ t.json :params
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The queries page asks "which of these 200 shapes have a captured plan in
4
+ # the window?" -- WHERE occurred_at BETWEEN ? AND ? AND group_hash IN (200
5
+ # values) AND explain IS NOT NULL. The planner walks the (group_hash,
6
+ # occurred_at) index for every one of the 200 shapes and then checks
7
+ # `explain` on each row, which is 840 ms at one day of one environment's
8
+ # queries (1M rows), and that whole time is a GVL hold: sqlite3-ruby does
9
+ # not release the GVL inside sqlite3_step. Plans are only captured for slow
10
+ # queries, so a partial index over just the explained rows is tiny and
11
+ # answers the question in 4 ms.
12
+ class AddExplainedIndexToQueries < ActiveRecord::Migration[8.1]
13
+ def change
14
+ add_index :queries, [ :group_hash, :occurred_at ], name: "idx_queries_explained", where: "explain IS NOT NULL"
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The queries page's "slowest in window" list orders the window by duration
4
+ # and takes a page. Every existing index leads with occurred_at, so the
5
+ # planner scans the whole window and sorts it: 48 ms for a day of one
6
+ # environment's queries (1M rows) -- and sqlite3-ruby holds the GVL for the
7
+ # whole statement, so every other request thread waits too. An index that
8
+ # leads with (duration DESC, id DESC) matches the page's ORDER BY exactly, so
9
+ # SQLite walks it from the slowest row and stops at the page (microseconds,
10
+ # and it cannot get worse as the table grows); occurred_at rides along so
11
+ # the window filter is answered from the index too. The planner still
12
+ # prefers the occurred_at index on its own cost model, so CursorPage forces
13
+ # this one.
14
+ class AddSlowestIndexToQueries < ActiveRecord::Migration[8.1]
15
+ def change
16
+ add_index :queries, [ :duration, :id, :occurred_at ], name: "idx_queries_slowest", order: { duration: :desc, id: :desc }
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Delivery ledger for in-process ingest. The reporter gives every batch a
4
+ # stable id before the first attempt; recording it on the ingest_batches row
5
+ # inside the batch transaction lets a retry ask "did this commit?" instead of
6
+ # guessing, and lets the follow-up work the batch still owes (exception
7
+ # grouping, which writes the other database) be resumed after a crash.
8
+ # `followups` is the small outbox: nil once everything has run.
9
+ class AddBatchLedgerToIngestBatches < ActiveRecord::Migration[8.1]
10
+ def change
11
+ add_column :ingest_batches, :batch_id, :string, limit: 36
12
+ add_column :ingest_batches, :followups, :json
13
+ add_index :ingest_batches, :batch_id, unique: true
14
+ add_index :ingest_batches, :received_at, where: "followups IS NOT NULL", name: "index_ingest_batches_pending_followups"
15
+ end
16
+ end
@@ -21,6 +21,8 @@ initializer always win over the env var.
21
21
  | `server` | `RAILWATCH_SERVER` | `KAMAL_HOST`, else `Socket.gethostname` | Host stamped on every record. Under Kamal the container hostname carries a per-deploy container id, so the Kamal host wins; it is what the post-deploy hook registers as an expected server, which is what silent-host detection compares against. |
22
22
  | `environment` | — | resolved lazily from `Rails.env` | Set `c.environment = "staging"` to report under a name other than the actual Rails env. |
23
23
  | `ignored_request_paths` | `RAILWATCH_IGNORED_REQUEST_PATHS` (comma-separated) | `/up,/railwatch/beacon` | Exact request paths that bypass Railwatch's request execution entirely. In Ruby configuration, `Regexp` entries are also supported. Setting the env var replaces the defaults; append with `c.ignored_request_paths += ["/healthz"]` to keep them. |
24
+ | `beacon_allowed_origins` | `RAILWATCH_BEACON_ALLOWED_ORIGINS` | `[]` | Extra origins allowed to post to the beacon beyond the app's own, comma-separated: a full origin (`https://app.example.com`) or a bare host. Like Sentry's allowed domains this bounds abuse rather than authenticating, since an endpoint a browser posts to cannot hold a secret. |
25
+ | `beacon_global_rate_limit` | `RAILWATCH_BEACON_GLOBAL_RATE_LIMIT` | `6000` | Beacons accepted per minute across every client, so a rotating address cannot multiply past the per-client limit. 0 disables it. |
24
26
  | `beacon_rate_limit` | `RAILWATCH_BEACON_RATE_LIMIT` | `120` | Beacon POSTs accepted per client IP per minute before `POST /railwatch/beacon` answers 429. The beacon is unauthenticated and keeps every browser error it is sent, so this is what stops a script from spending the app's event quota. Counted in the app's cache store; `0` turns it off. |
25
27
 
26
28
  `Railwatch.enabled?` delegates to `config.enabled?`, which is `@enabled &&
@@ -858,6 +860,30 @@ in `lib/railwatch.rb`, unless noted:
858
860
  `instrument_outgoing`, `flush`, `debug { }`. `pause`/`resume` are the
859
861
  ignore block's building blocks, and are nestable.
860
862
 
863
+ ## Embedded mode
864
+
865
+ ```ruby
866
+ c.transport = :local # RAILWATCH_TRANSPORT; default "http"
867
+ c.issue_prefix = "SHOP" # RAILWATCH_ISSUE_PREFIX; default from the app name
868
+ c.repository_url = "..." # RAILWATCH_REPOSITORY_URL
869
+ c.retention_days = 7 # RAILWATCH_RETENTION_DAYS
870
+ c.http_basic_auth_enabled = true # RAILWATCH_HTTP_BASIC_AUTH_ENABLED; on and closed until credentials exist
871
+ c.http_basic_auth_user = "ops" # RAILWATCH_HTTP_BASIC_AUTH_USER, or credentials railwatch.http_basic_auth_user
872
+ c.http_basic_auth_password = "..." # RAILWATCH_HTTP_BASIC_AUTH_PASSWORD, or credentials railwatch.http_basic_auth_password
873
+ c.base_controller_class = "AdminController" # RAILWATCH_BASE_CONTROLLER_CLASS; default ActionController::Base
874
+ c.dashboard_open = false # RAILWATCH_DASHBOARD_OPEN; public on purpose
875
+ c.dashboard_user = ->(request) { { id:, name:, email: } or nil }
876
+ ```
877
+
878
+ With `transport = :local` the reporter writes each batch into the app's
879
+ own `railwatch_telemetry` database instead of POSTing it, and the engine
880
+ serves the dashboard at its mount. `enabled?` no longer needs a token.
881
+ The others only matter in that mode. The dashboard is behind HTTP Basic
882
+ by default and answers 401 until `bin/rails
883
+ railwatch:authentication:configure` has written credentials; a host with
884
+ its own admin auth turns Basic off and sets `base_controller_class` or a
885
+ routes constraint. Full walkthrough: [Embedded mode](embedded.md).
886
+
861
887
  ## Rake tasks
862
888
 
863
889
  Ship with the gem via Rails::Engine's default `lib/tasks` convention, in