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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -0
- data/README.md +10 -0
- data/app/channels/railwatch/environment_channel.rb +28 -0
- data/app/controllers/concerns/railwatch/telemetry_identity.rb +25 -0
- data/app/controllers/railwatch/alerts_controller.rb +48 -0
- data/app/controllers/railwatch/anomaly_rules_controller.rb +31 -0
- data/app/controllers/railwatch/attachments_controller.rb +22 -0
- data/app/controllers/railwatch/beacon_controller.rb +67 -7
- data/app/controllers/railwatch/broadcasts_controller.rb +15 -0
- data/app/controllers/railwatch/cache_events_controller.rb +30 -0
- data/app/controllers/railwatch/commands_controller.rb +16 -0
- data/app/controllers/railwatch/comments_controller.rb +11 -0
- data/app/controllers/railwatch/dashboard_controller.rb +69 -0
- data/app/controllers/railwatch/deploys_controller.rb +34 -0
- data/app/controllers/railwatch/deprecations_controller.rb +54 -0
- data/app/controllers/railwatch/environment_scoped.rb +99 -0
- data/app/controllers/railwatch/exceptions_controller.rb +53 -0
- data/app/controllers/railwatch/executions_controller.rb +13 -0
- data/app/controllers/railwatch/issues_controller.rb +258 -0
- data/app/controllers/railwatch/jobs_controller.rb +63 -0
- data/app/controllers/railwatch/llm_calls_controller.rb +124 -0
- data/app/controllers/railwatch/logs_controller.rb +40 -0
- data/app/controllers/railwatch/mails_controller.rb +19 -0
- data/app/controllers/railwatch/notifications_controller.rb +11 -0
- data/app/controllers/railwatch/outgoing_requests_controller.rb +19 -0
- data/app/controllers/railwatch/overview_controller.rb +38 -0
- data/app/controllers/railwatch/people_controller.rb +25 -0
- data/app/controllers/railwatch/processes_controller.rb +33 -0
- data/app/controllers/railwatch/profiles_controller.rb +50 -0
- data/app/controllers/railwatch/queries_controller.rb +59 -0
- data/app/controllers/railwatch/releases_controller.rb +75 -0
- data/app/controllers/railwatch/requests_controller.rb +55 -0
- data/app/controllers/railwatch/saved_views_controller.rb +53 -0
- data/app/controllers/railwatch/scheduled_tasks_controller.rb +45 -0
- data/app/controllers/railwatch/spans_controller.rb +47 -0
- data/app/controllers/railwatch/storage_ops_controller.rb +15 -0
- data/app/controllers/railwatch/tenants_controller.rb +44 -0
- data/app/controllers/railwatch/thresholds_controller.rb +40 -0
- data/app/controllers/railwatch/traces_controller.rb +72 -0
- data/app/controllers/railwatch/transactions_controller.rb +21 -0
- data/app/controllers/railwatch/view_renders_controller.rb +28 -0
- data/app/controllers/railwatch/visits_controller.rb +53 -0
- data/app/helpers/railwatch/assets_helper.rb +52 -0
- data/app/jobs/railwatch/anomaly_scan_job.rb +11 -0
- data/app/jobs/railwatch/application_job.rb +7 -0
- data/app/jobs/railwatch/auto_resolve_issues_job.rb +19 -0
- data/app/jobs/railwatch/check_scheduled_tasks_job.rb +113 -0
- data/app/jobs/railwatch/detect_anomalies_job.rb +171 -0
- data/app/jobs/railwatch/detect_performance_issues_job.rb +85 -0
- data/app/jobs/railwatch/group_exceptions_job.rb +91 -0
- data/app/jobs/railwatch/optimize_telemetry_job.rb +23 -0
- data/app/jobs/railwatch/performance_scan_job.rb +11 -0
- data/app/jobs/railwatch/prune_telemetry_job.rb +110 -0
- data/app/jobs/railwatch/release_health_rollup_job.rb +64 -0
- data/app/jobs/railwatch/rollup_catchup_job.rb +21 -0
- data/app/jobs/railwatch/rollup_job.rb +130 -0
- data/app/jobs/railwatch/scheduled_task_scan_job.rb +11 -0
- data/app/models/concerns/railwatch/detection_snapshotting.rb +20 -0
- data/app/models/railwatch/alert.rb +229 -0
- data/app/models/railwatch/alert_rule.rb +121 -0
- data/app/models/railwatch/anomaly_rule.rb +33 -0
- data/app/models/railwatch/application.rb +44 -0
- data/app/models/railwatch/application_record.rb +33 -0
- data/app/models/railwatch/comment.rb +36 -0
- data/app/models/railwatch/deploy.rb +67 -0
- data/app/models/railwatch/environment.rb +54 -0
- data/app/models/railwatch/execution_presenter.rb +185 -0
- data/app/models/railwatch/filter_query.rb +143 -0
- data/app/models/railwatch/followup_receipt.rb +27 -0
- data/app/models/railwatch/ingest/batch.rb +305 -0
- data/app/models/railwatch/ingest/mapper.rb +575 -0
- data/app/models/railwatch/ingest/payload.rb +96 -0
- data/app/models/railwatch/ingest/rollup_absorber.rb +170 -0
- data/app/models/railwatch/ingest/writer.rb +137 -0
- data/app/models/railwatch/issue.rb +277 -0
- data/app/models/railwatch/issue_activity.rb +23 -0
- data/app/models/railwatch/issue_detection_presenter.rb +309 -0
- data/app/models/railwatch/issue_detection_snapshot.rb +77 -0
- data/app/models/railwatch/maintenance_task.rb +53 -0
- data/app/models/railwatch/saved_view.rb +63 -0
- data/app/models/railwatch/telemetry/aggregations.rb +116 -0
- data/app/models/railwatch/telemetry/attachment.rb +31 -0
- data/app/models/railwatch/telemetry/bounded_gzip.rb +101 -0
- data/app/models/railwatch/telemetry/broadcast.rb +13 -0
- data/app/models/railwatch/telemetry/cache_event.rb +16 -0
- data/app/models/railwatch/telemetry/child.rb +53 -0
- data/app/models/railwatch/telemetry/cursor_page.rb +99 -0
- data/app/models/railwatch/telemetry/deprecation.rb +13 -0
- data/app/models/railwatch/telemetry/enqueued_job.rb +13 -0
- data/app/models/railwatch/telemetry/exception.rb +21 -0
- data/app/models/railwatch/telemetry/execution.rb +71 -0
- data/app/models/railwatch/telemetry/health_sample.rb +72 -0
- data/app/models/railwatch/telemetry/ingest_batch.rb +31 -0
- data/app/models/railwatch/telemetry/llm_call.rb +37 -0
- data/app/models/railwatch/telemetry/log.rb +85 -0
- data/app/models/railwatch/telemetry/mail.rb +13 -0
- data/app/models/railwatch/telemetry/n_plus_one.rb +92 -0
- data/app/models/railwatch/telemetry/notification.rb +13 -0
- data/app/models/railwatch/telemetry/outgoing_request.rb +13 -0
- data/app/models/railwatch/telemetry/person.rb +50 -0
- data/app/models/railwatch/telemetry/process.rb +10 -0
- data/app/models/railwatch/telemetry/profile.rb +37 -0
- data/app/models/railwatch/telemetry/query.rb +50 -0
- data/app/models/railwatch/telemetry/query_shape.rb +45 -0
- data/app/models/railwatch/telemetry/release_health.rb +63 -0
- data/app/models/railwatch/telemetry/rollup.rb +78 -0
- data/app/models/railwatch/telemetry/session.rb +24 -0
- data/app/models/railwatch/telemetry/span.rb +19 -0
- data/app/models/railwatch/telemetry/storage_op.rb +13 -0
- data/app/models/railwatch/telemetry/tenant.rb +221 -0
- data/app/models/railwatch/telemetry/transaction.rb +13 -0
- data/app/models/railwatch/telemetry/view_render.rb +13 -0
- data/app/models/railwatch/telemetry/visit.rb +24 -0
- data/app/models/railwatch/telemetry_record.rb +57 -0
- data/app/models/railwatch/threshold.rb +29 -0
- data/app/models/railwatch/user.rb +47 -0
- data/app/models/railwatch/viewer.rb +13 -0
- data/app/views/layouts/railwatch/dashboard.html.erb +25 -0
- data/config/routes.rb +59 -0
- data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +151 -0
- data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +18 -0
- data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +20 -0
- data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +71 -0
- data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +481 -0
- data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +14 -0
- data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +10 -0
- data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +82 -0
- data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +10 -0
- data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +60 -0
- data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +51 -0
- data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +11 -0
- data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +10 -0
- data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +37 -0
- data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +11 -0
- data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +15 -0
- data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +10 -0
- data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +18 -0
- data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +25 -0
- data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +55 -0
- data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +21 -0
- data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +16 -0
- data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +18 -0
- data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +16 -0
- data/docs/configuration.md +26 -0
- data/docs/embedded.md +352 -0
- data/docs/getting-started.md +5 -0
- data/lib/generators/railwatch/install/install_generator.rb +222 -1
- data/lib/generators/railwatch/install/templates/{initializer.rb → initializer.rb.tt} +39 -0
- data/lib/generators/railwatch/install/templates/post-deploy +8 -0
- data/lib/puma/plugin/railwatch.rb +170 -0
- data/lib/railwatch/authentication.rb +83 -0
- data/lib/railwatch/configuration.rb +134 -3
- data/lib/railwatch/dashboard_assets.rb +45 -0
- data/lib/railwatch/embedded.rb +54 -0
- data/lib/railwatch/engine.rb +89 -1
- data/lib/railwatch/ingest_request_body_limit.rb +10 -0
- data/lib/railwatch/json_compat.rb +60 -0
- data/lib/railwatch/maintenance.rb +183 -0
- data/lib/railwatch/patches/runner_command.rb +21 -1
- data/lib/railwatch/record.rb +33 -8
- data/lib/railwatch/reporter.rb +16 -0
- data/lib/railwatch/subscribers/process_info.rb +2 -1
- data/lib/railwatch/transport/local.rb +78 -0
- data/lib/railwatch/transport/socket.rb +183 -0
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch/writer.rb +370 -0
- data/lib/railwatch.rb +38 -1
- data/lib/tasks/railwatch_tasks.rake +128 -0
- data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
- data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
- data/public/railwatch/assets/app-layout-yh-sPWgK.js +1 -0
- data/public/railwatch/assets/app-wordmark-nbjkzxwQ.js +1 -0
- data/public/railwatch/assets/appearance-CDuRvQTB.js +1 -0
- data/public/railwatch/assets/application-C_kpBdnf.css +1 -0
- data/public/railwatch/assets/arrow-up-DVtOGdVA.js +1 -0
- data/public/railwatch/assets/auth-layout-TCwPpS1K.js +1 -0
- data/public/railwatch/assets/badge-Daw4Hvr8.js +1 -0
- data/public/railwatch/assets/braces-DhFbHPsz.js +1 -0
- data/public/railwatch/assets/card-BX_3HXcJ.js +1 -0
- data/public/railwatch/assets/chart-B14-N9g7.js +39 -0
- data/public/railwatch/assets/chart-hover-Vy52H4uD.js +1 -0
- data/public/railwatch/assets/chart-panel-Cb4S_ej_.js +1 -0
- data/public/railwatch/assets/checkbox-D3aRSsBj.js +1 -0
- data/public/railwatch/assets/code-KvW8k7Jr.js +1 -0
- data/public/railwatch/assets/copy-DaQMJWoT.js +1 -0
- data/public/railwatch/assets/copy-block-BKFGd11J.js +1 -0
- data/public/railwatch/assets/copy-id-Djks1fXB.js +1 -0
- data/public/railwatch/assets/cursor-load-more-BXV0f1D_.js +1 -0
- data/public/railwatch/assets/data-table-iv1bdF6u.js +1 -0
- data/public/railwatch/assets/edit-BZc_Iawe.js +1 -0
- data/public/railwatch/assets/edit-DMKUF8Zi.js +8 -0
- data/public/railwatch/assets/edit-__9yJlO3.js +1 -0
- data/public/railwatch/assets/empty-state-6j_0AaQQ.js +1 -0
- data/public/railwatch/assets/env-layout-DrT8rO6P.js +1 -0
- data/public/railwatch/assets/execution-path-CzgBUi5e.js +1 -0
- data/public/railwatch/assets/filter-bar-9SU5NrzX.js +1 -0
- data/public/railwatch/assets/flamegraph-qcekju8V.js +2 -0
- data/public/railwatch/assets/format-B9SDkrWj.js +1 -0
- data/public/railwatch/assets/frames-Cyu7KMxZ.js +1 -0
- data/public/railwatch/assets/google-sign-in-button-DsTSfmzY.js +1 -0
- data/public/railwatch/assets/index-1ol1-QWI.js +1 -0
- data/public/railwatch/assets/index-5jI4aFzC.js +1 -0
- data/public/railwatch/assets/index-9KTrVnrc.js +1 -0
- data/public/railwatch/assets/index-B0-8lcTp.js +1 -0
- data/public/railwatch/assets/index-B7jjfNfO.js +1 -0
- data/public/railwatch/assets/index-BBchRy0M.js +1 -0
- data/public/railwatch/assets/index-BRiq3SNR.js +1 -0
- data/public/railwatch/assets/index-BgKj9xhr.js +1 -0
- data/public/railwatch/assets/index-BkTZqqOu.js +1 -0
- data/public/railwatch/assets/index-BprKx8QO.js +1 -0
- data/public/railwatch/assets/index-C3jzvPs3.js +1 -0
- data/public/railwatch/assets/index-Cbs6gGyQ.js +1 -0
- data/public/railwatch/assets/index-CdRZ6AWF.js +1 -0
- data/public/railwatch/assets/index-CeYKnapu.js +1 -0
- data/public/railwatch/assets/index-Cmlwy1-V.js +1 -0
- data/public/railwatch/assets/index-Cwx6058d.js +1 -0
- data/public/railwatch/assets/index-D4CSdbHv.js +1 -0
- data/public/railwatch/assets/index-DEFMSkdG.js +1 -0
- data/public/railwatch/assets/index-DFiHEBSh.js +1 -0
- data/public/railwatch/assets/index-DL4vWdWJ.js +1 -0
- data/public/railwatch/assets/index-DU9F5b5d.js +1 -0
- data/public/railwatch/assets/index-DaXgPcGL.js +1 -0
- data/public/railwatch/assets/index-DbtaU-EE.js +1 -0
- data/public/railwatch/assets/index-DeOe83F4.js +1 -0
- data/public/railwatch/assets/index-DiucHN4B.js +1 -0
- data/public/railwatch/assets/index-DlnR_l9o.js +1 -0
- data/public/railwatch/assets/index-DlumCsWY.js +2 -0
- data/public/railwatch/assets/index-DmRd7aIG.js +1 -0
- data/public/railwatch/assets/index-DxSh2UpM.js +1 -0
- data/public/railwatch/assets/index-MIMGuFNt.js +1 -0
- data/public/railwatch/assets/index-OqI59zPb.js +1 -0
- data/public/railwatch/assets/index-P4rC7IlX.js +1 -0
- data/public/railwatch/assets/index-gpPOcFWq.js +1 -0
- data/public/railwatch/assets/index-oVkururr.js +1 -0
- data/public/railwatch/assets/index-p9puqVge.js +1 -0
- data/public/railwatch/assets/inertia-TViv6kNv.js +97 -0
- data/public/railwatch/assets/input-error-LxImUkxv.js +1 -0
- data/public/railwatch/assets/json-viewer-Ar4cjPDW.js +1 -0
- data/public/railwatch/assets/klass-CJ-J4INB.js +1 -0
- data/public/railwatch/assets/label-COUKWqE_.js +1 -0
- data/public/railwatch/assets/layout-DNSLAkw_.js +1 -0
- data/public/railwatch/assets/live-dot-ChfUtY3p.js +41 -0
- data/public/railwatch/assets/nav-CNnDqPlm.js +1 -0
- data/public/railwatch/assets/new-84S8ZJq9.js +1 -0
- data/public/railwatch/assets/new-Be55nmt9.js +1 -0
- data/public/railwatch/assets/new-Bi_xQiIb.js +1 -0
- data/public/railwatch/assets/new-BvCT8TMg.js +1 -0
- data/public/railwatch/assets/new-D05SajFR.js +1 -0
- data/public/railwatch/assets/new-D4uewYC8.js +1 -0
- data/public/railwatch/assets/onboarding-CYZi5Cqc.js +1 -0
- data/public/railwatch/assets/origin-identity-6q1-CBts.js +1 -0
- data/public/railwatch/assets/percentile-picker-gFZCXtdb.js +1 -0
- data/public/railwatch/assets/relative-time-IOOgl5n2.js +1 -0
- data/public/railwatch/assets/release-health-DC8oc7uw.js +1 -0
- data/public/railwatch/assets/route-Dv6LAWvT.js +1 -0
- data/public/railwatch/assets/segmented-h1VdDTqE.js +1 -0
- data/public/railwatch/assets/select-_AJsUa7X.js +1 -0
- data/public/railwatch/assets/separator-BwwTYtCF.js +1 -0
- data/public/railwatch/assets/series-chart-DaFPefku.js +1 -0
- data/public/railwatch/assets/show-B7NCgkEo.js +1 -0
- data/public/railwatch/assets/show-BKqyKjBK.js +1 -0
- data/public/railwatch/assets/show-BM6X2Mpo.js +1 -0
- data/public/railwatch/assets/show-BNw4tN5q.js +1 -0
- data/public/railwatch/assets/show-BO3bnG5h.js +1 -0
- data/public/railwatch/assets/show-BhrAVAEA.js +1 -0
- data/public/railwatch/assets/show-C4Ltf5i9.js +2 -0
- data/public/railwatch/assets/show-C8sHalnw.js +1 -0
- data/public/railwatch/assets/show-CeTL4B37.js +2 -0
- data/public/railwatch/assets/show-CpfgV1jP.js +1 -0
- data/public/railwatch/assets/show-DACku6AD.js +3 -0
- data/public/railwatch/assets/show-DIOSGcXV.js +6 -0
- data/public/railwatch/assets/show-DQp_1n-B.js +1 -0
- data/public/railwatch/assets/show-DVNz46RI.js +1 -0
- data/public/railwatch/assets/show-DYteoYWW.js +1 -0
- data/public/railwatch/assets/show-DgSIoRvA.js +1 -0
- data/public/railwatch/assets/show-JxFtB4eK.js +2 -0
- data/public/railwatch/assets/sort-header-DpFzXblu.js +1 -0
- data/public/railwatch/assets/source-link-B2183i2-.js +1 -0
- data/public/railwatch/assets/sparkline-cell-C3-5vFkP.js +1 -0
- data/public/railwatch/assets/stat-s4RpOS9w.js +1 -0
- data/public/railwatch/assets/status-badge-8jVV-LA4.js +1 -0
- data/public/railwatch/assets/tenant-path-G-6u9A-o.js +1 -0
- data/public/railwatch/assets/text-link-DfsiaCcP.js +1 -0
- data/public/railwatch/assets/textarea-Dye72uP7.js +1 -0
- data/public/railwatch/assets/timeline-CD7WHnbo.js +1 -0
- data/public/railwatch/assets/transition-B_AW8rMK.js +5 -0
- data/public/railwatch/assets/use-clipboard-ColgLyQ2.js +1 -0
- data/public/railwatch/icon.png +0 -0
- data/public/railwatch/icon.svg +5 -0
- data/public/railwatch/manifest.json +2171 -0
- data/public/railwatch/rails-vite.json +1 -0
- metadata +314 -4
data/docs/embedded.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Embedded mode: the dashboard inside your app
|
|
2
|
+
|
|
3
|
+
Railwatch can keep every record in your own application and serve the
|
|
4
|
+
full dashboard at `/railwatch`, with no token and no cloud. The gem's
|
|
5
|
+
reporter, buffer and sampling are the same; the only difference is
|
|
6
|
+
where a batch ends up. In embedded mode it is written straight into a
|
|
7
|
+
SQLite database your app owns, and the dashboard reads it back from
|
|
8
|
+
there.
|
|
9
|
+
|
|
10
|
+
Use it when one server runs the app. Telemetry lands in a file next to
|
|
11
|
+
your other SQLite databases, so several servers would each see only
|
|
12
|
+
their own slice. For more than one server, or for a team that wants one
|
|
13
|
+
place for many apps, point the gem at Railwatch Cloud instead
|
|
14
|
+
([Getting started](getting-started.md)); the two are switchable with
|
|
15
|
+
one setting.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bundle add railwatch
|
|
21
|
+
bin/rails generate railwatch:install --local
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Restart the app and open `/railwatch`. Then `bin/rails railwatch:doctor`
|
|
25
|
+
checks the wiring. The generator creates and migrates both databases
|
|
26
|
+
itself; `bin/rails db:prepare`, which a deploy already runs, migrates
|
|
27
|
+
them after every gem update.
|
|
28
|
+
|
|
29
|
+
The engine needs Active Job (its grouping and scan jobs are Active Job
|
|
30
|
+
classes even though embedded mode calls them directly) and loads it
|
|
31
|
+
itself. Action Cable is optional: with it the dashboard updates live,
|
|
32
|
+
without it (`rails new --minimal`) the pages refresh on navigation.
|
|
33
|
+
|
|
34
|
+
## Your application's own database
|
|
35
|
+
|
|
36
|
+
Embedded mode does not care what your application runs on. The two
|
|
37
|
+
databases it adds are SQLite files either way, so the generated entries
|
|
38
|
+
name `adapter: sqlite3` themselves rather than inheriting your default
|
|
39
|
+
block, and they need no `&default` anchor to exist.
|
|
40
|
+
|
|
41
|
+
On a PostgreSQL or MySQL app that means the install is two commands
|
|
42
|
+
rather than one, because SQLite's adapter gem will not be in your bundle:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
bin/rails generate railwatch:install --local # adds gem "sqlite3", writes the config
|
|
46
|
+
bundle install
|
|
47
|
+
bin/rails db:prepare # creates the two SQLite files
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Verified end to end on both. On a PostgreSQL app and on a MySQL app, the
|
|
51
|
+
application's own four databases stay where they were, Railwatch's two are
|
|
52
|
+
files under `storage/`, and neither server gains a single Railwatch table.
|
|
53
|
+
|
|
54
|
+
What `--local` writes, on top of the usual install:
|
|
55
|
+
|
|
56
|
+
- `config/initializers/railwatch.rb` with `c.transport = :local` and the
|
|
57
|
+
dashboard's own paths excluded from request capture.
|
|
58
|
+
- Two databases in every environment of `config/database.yml`:
|
|
59
|
+
`railwatch` (issues, comments, saved views, thresholds, deploys:
|
|
60
|
+
small, permanent) and `railwatch_telemetry` (everything the app
|
|
61
|
+
reports: written continuously, pruned nightly). A flat
|
|
62
|
+
`development:` entry is nested under `primary:` first, since named
|
|
63
|
+
databases need that form. Each entry's `migrations_paths` points into
|
|
64
|
+
the gem, so `db:prepare` builds the tables from the gem's own
|
|
65
|
+
migrations and nothing is copied into `db/`.
|
|
66
|
+
- `mount Railwatch::Engine, at: "/railwatch"`, as always.
|
|
67
|
+
|
|
68
|
+
Nothing touches your primary database.
|
|
69
|
+
|
|
70
|
+
## Upgrading
|
|
71
|
+
|
|
72
|
+
The engine's tables migrate the way Active Storage's do: the migrations
|
|
73
|
+
live in the gem and each database's `migrations_paths` points at them.
|
|
74
|
+
After `bundle update railwatch`, run `bin/rails db:prepare` (or
|
|
75
|
+
`db:migrate`) and whatever is new applies; a deploy that already runs
|
|
76
|
+
one of those needs nothing extra. `bin/rails railwatch:doctor` reports
|
|
77
|
+
pending migrations for both databases. Tables in the `railwatch` file
|
|
78
|
+
are prefixed `railwatch_`; the telemetry file's tables are the hosted
|
|
79
|
+
platform's schema and are unprefixed.
|
|
80
|
+
|
|
81
|
+
## What you get
|
|
82
|
+
|
|
83
|
+
Every page of Railwatch Cloud: requests, jobs, scheduled tasks,
|
|
84
|
+
commands, queries, spans, exceptions, logs, cache, mail, notifications,
|
|
85
|
+
broadcasts, outgoing requests, LLM calls, storage, views, transactions,
|
|
86
|
+
deprecations, processes, releases, deploys, users, tenants, visits,
|
|
87
|
+
thresholds; issues with comments, assignment, merge and split; saved
|
|
88
|
+
views; alert rules and the alert log. Live updates arrive over Action
|
|
89
|
+
Cable when the app has it. The dashboard is a prebuilt bundle shipped
|
|
90
|
+
inside the gem, so the app needs no Node, no Vite and no asset pipeline
|
|
91
|
+
integration.
|
|
92
|
+
|
|
93
|
+
Not in embedded mode: accounts and members (the operator is whoever your
|
|
94
|
+
app lets through), integrations (alerts are recorded, not delivered),
|
|
95
|
+
and the MCP server.
|
|
96
|
+
|
|
97
|
+
## Authentication
|
|
98
|
+
|
|
99
|
+
The dashboard shows every query, log line and exception your app
|
|
100
|
+
produced, so it works the way Mission Control Jobs does: **HTTP Basic
|
|
101
|
+
authentication is on and closed by default**. With no credentials
|
|
102
|
+
configured every dashboard request is 401, and `railwatch:doctor` says
|
|
103
|
+
so. Set them with
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
bin/rails railwatch:authentication:configure
|
|
107
|
+
RAILS_ENV=production bin/rails railwatch:authentication:configure
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
which writes them to that environment's Rails credentials:
|
|
111
|
+
|
|
112
|
+
```yml
|
|
113
|
+
railwatch:
|
|
114
|
+
http_basic_auth_user: ops
|
|
115
|
+
http_basic_auth_password: secret
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`RAILWATCH_HTTP_BASIC_AUTH_USER` and `RAILWATCH_HTTP_BASIC_AUTH_PASSWORD`,
|
|
119
|
+
or `c.http_basic_auth_user =` / `c.http_basic_auth_password =` in the
|
|
120
|
+
initializer, do the same. The live-update channel checks the same
|
|
121
|
+
credentials (the browser sends them on the WebSocket handshake).
|
|
122
|
+
|
|
123
|
+
### Your own authentication
|
|
124
|
+
|
|
125
|
+
Two ways, both from Mission Control's playbook. Either lets an admin of
|
|
126
|
+
your app in with no second password. Turn Basic off when you use one,
|
|
127
|
+
or both gates apply.
|
|
128
|
+
|
|
129
|
+
A base controller. Every dashboard controller inherits from it, so its
|
|
130
|
+
`before_action` runs first:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
c.http_basic_auth_enabled = false
|
|
134
|
+
c.base_controller_class = "AdminController" # requires an admin, or redirects to sign-in
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Your controller's code runs inside the engine, whose route helpers take
|
|
138
|
+
precedence; reach your app's with `main_app.root_path`.
|
|
139
|
+
|
|
140
|
+
Or a routes constraint, which keeps the engine out of it entirely (for
|
|
141
|
+
example with the sessions Rails' authentication generator creates):
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# config/routes.rb
|
|
145
|
+
constraints ->(request) { Session.find_by(id: request.cookie_jar.signed[:session_id])&.user&.admin? } do
|
|
146
|
+
mount Railwatch::Engine, at: "/railwatch"
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Requests that fail the constraint never reach the engine. A constraint is
|
|
151
|
+
invisible to the gem, though, so say that the engine's own gate is off on
|
|
152
|
+
purpose:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
c.http_basic_auth_enabled = false
|
|
156
|
+
c.dashboard_open = true # "something in front of the mount gates this"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Deliberately public
|
|
160
|
+
|
|
161
|
+
A dashboard on a private network or behind a VPN can be open, and saying
|
|
162
|
+
so is a setting rather than an omission:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
c.http_basic_auth_enabled = false
|
|
166
|
+
c.dashboard_open = true
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
With Basic off and none of `base_controller_class`, `dashboard_user` or
|
|
170
|
+
`dashboard_open` set, the gem cannot tell a deliberate choice from a
|
|
171
|
+
forgotten one. It serves the dashboard (a routes constraint it cannot see
|
|
172
|
+
is a legitimate answer) but logs a warning at every boot outside
|
|
173
|
+
development, `railwatch:doctor` reports the gate as undeclared, and live
|
|
174
|
+
updates are refused. Declaring any of the three settles it.
|
|
175
|
+
|
|
176
|
+
### Live updates and `/cable`
|
|
177
|
+
|
|
178
|
+
Action Cable runs on your application's own `/cable` endpoint, not under
|
|
179
|
+
the engine's mount, so a routes constraint around `/railwatch` does not
|
|
180
|
+
cover it and a base controller cannot reach it. The live-update channel
|
|
181
|
+
therefore follows what you declared: HTTP Basic credentials are checked
|
|
182
|
+
on the WebSocket handshake, a `dashboard_user` resolver is consulted,
|
|
183
|
+
`dashboard_open` and `base_controller_class` are taken at their word, and
|
|
184
|
+
an undeclared gate is refused. The channel carries an ingest ping (a
|
|
185
|
+
timestamp and per-type counts) and never telemetry records.
|
|
186
|
+
|
|
187
|
+
### Naming the operator, and what an id may be
|
|
188
|
+
|
|
189
|
+
Comments, saved views and issue activity record who did them. Give the
|
|
190
|
+
initializer a resolver and the dashboard shows that person instead of a
|
|
191
|
+
single "Operator":
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
c.dashboard_user = ->(request) do
|
|
195
|
+
user = User.find_by(id: request.session[:user_id])
|
|
196
|
+
user && { id: user.id, name: user.name, email: user.email }
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The `id` may be anything your application already uses: an integer, a
|
|
201
|
+
UUID, a ULID, an email. It is stored as an opaque string and handed back
|
|
202
|
+
to you; nothing joins on it and nothing parses it, because the engine has
|
|
203
|
+
no user table to check it against. Comments, saved views, issue activity
|
|
204
|
+
and assignment all key off whatever you return, so a person keeps their
|
|
205
|
+
own views and their name on their own comments however you identify them.
|
|
206
|
+
|
|
207
|
+
Returning `nil` refuses the request, which is what makes this an
|
|
208
|
+
authorisation rule as well as a label.
|
|
209
|
+
|
|
210
|
+
## The writer process
|
|
211
|
+
|
|
212
|
+
Puma forks one Railwatch writer from its master when `config/puma.rb`
|
|
213
|
+
carries the plugin (`--local` adds it):
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
plugin :railwatch if defined?(Railwatch)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Every web worker keeps its reporter thread, but instead of writing
|
|
220
|
+
SQLite it hands each batch to the writer over a Unix socket
|
|
221
|
+
(`tmp/sockets/railwatch-writer.sock`, `RAILWATCH_WRITER_SOCKET`). The
|
|
222
|
+
socket's directory is created mode 0700 and the socket 0600, so only
|
|
223
|
+
the app's own user can reach it; keep it that way if you move it. Linux
|
|
224
|
+
caps the whole path at 108 bytes, so an app checked out deep in the
|
|
225
|
+
filesystem should point this at a directory of its own under
|
|
226
|
+
`/run/user/$UID` or `/tmp` (not a bare file in `/tmp`). Single and
|
|
227
|
+
cluster mode alike: a default Rails 8 app runs Puma with no workers, and
|
|
228
|
+
its batches come off its request threads just the same. The
|
|
229
|
+
writer maps the records, writes both databases, folds the rollups,
|
|
230
|
+
groups exceptions into issues and runs the maintenance clock below. It
|
|
231
|
+
is the only process that ever holds the telemetry database's write lock,
|
|
232
|
+
and its Ruby interpreter is its own, so none of that work is ever
|
|
233
|
+
interleaved with a request. Same shape as Solid Queue's
|
|
234
|
+
`solid_queue_mode :fork`: it exits when Puma does, and Puma restarts it
|
|
235
|
+
if it dies. While it is down the reporter keeps batches in memory, with
|
|
236
|
+
the same byte ceiling and backoff as the HTTP transport, and every
|
|
237
|
+
batch carries an id the writer records inside the write transaction, so
|
|
238
|
+
a batch delivered twice is written once.
|
|
239
|
+
|
|
240
|
+
The writer judges its own health. A single batch write that runs past
|
|
241
|
+
sixty seconds is a stuck writer, not a slow one (a lock that never
|
|
242
|
+
clears, a lost connection), and the process exits so Puma restarts it;
|
|
243
|
+
the batch is retained on the worker and written by the new writer. The
|
|
244
|
+
doctor reports both whether the socket answers and when the last batch
|
|
245
|
+
was actually written, since a process that is alive and a process that
|
|
246
|
+
is doing its job are different questions.
|
|
247
|
+
|
|
248
|
+
Whether a writer is expected decides what a missing one means. Puma
|
|
249
|
+
workers under the plugin expect one: a socket that is absent or not
|
|
250
|
+
answering is a writer that is starting or restarting, and they retain
|
|
251
|
+
batches and retry for as long as it takes. A process with no plugin
|
|
252
|
+
(`bin/rails runner`, a Solid Queue worker, a `rails server` without it,
|
|
253
|
+
the test suite) expects none, says so once under `RAILWATCH_DEBUG`, and
|
|
254
|
+
writes its batches in-process instead. That fallback is provisional: the
|
|
255
|
+
socket is tried again every 30 seconds, so a process that started before
|
|
256
|
+
the writer did hands the work back as soon as one is listening. What
|
|
257
|
+
changes is which process pays for the write, not whether the write
|
|
258
|
+
happens.
|
|
259
|
+
|
|
260
|
+
Retention while a writer is away is bounded, not infinite. A writer that
|
|
261
|
+
is restarting is back in seconds; one that is missing for a minute (an
|
|
262
|
+
unwritable socket directory, a fork that keeps failing) is treated as
|
|
263
|
+
absent and the worker writes its own batches again, still re-checking, so
|
|
264
|
+
the records are kept rather than retained to the reporter's retry cap. A
|
|
265
|
+
batch that does exhaust that ladder is counted as dropped and reported
|
|
266
|
+
through `Railwatch.on_unrecoverable`, so loss is never silent. A Puma
|
|
267
|
+
phased restart stops the writer and starts a fresh one once the new
|
|
268
|
+
workers are up.
|
|
269
|
+
|
|
270
|
+
## Maintenance
|
|
271
|
+
|
|
272
|
+
Railwatch needs no job worker and nothing in `config/recurring.yml`.
|
|
273
|
+
The work that keeps the dashboard current happens in two places:
|
|
274
|
+
|
|
275
|
+
- **As each batch lands.** The writer writes the batch, folds its rows
|
|
276
|
+
into the hour's rollups, and groups any exceptions into issues, all
|
|
277
|
+
before it picks up the next batch. Counts, percentiles and the issues
|
|
278
|
+
list move with every batch.
|
|
279
|
+
- **On Railwatch's own clock.** The writer runs a `railwatch-maintenance`
|
|
280
|
+
thread that wakes every 30 seconds. Without a writer, every web and
|
|
281
|
+
worker process runs one, and one process at a time runs each task,
|
|
282
|
+
claimed through a lease row in the `railwatch` database, so a Puma
|
|
283
|
+
cluster and a Solid Queue worker on the same server do not all prune
|
|
284
|
+
at once.
|
|
285
|
+
|
|
286
|
+
| Task | Cadence | What it does |
|
|
287
|
+
| --- | --- | --- |
|
|
288
|
+
| drain follow-ups | every minute | Finishes the exception grouping of any batch whose process died right after the batch committed |
|
|
289
|
+
| release health | every minute | Hourly crash-free aggregates for the current and previous hour |
|
|
290
|
+
| rollup reconcile | hourly | Recomputes the previous hour's rollups from raw rows, for records that arrived after their hour closed |
|
|
291
|
+
| performance scan | every 5 minutes | Threshold breaches become issues |
|
|
292
|
+
| anomaly scan | every 5 minutes | Anomaly rules, when any are enabled |
|
|
293
|
+
| scheduled tasks | every 10 minutes | Missed and late scheduled tasks |
|
|
294
|
+
| auto-resolve | daily | Resolves issues quiet for 14 days |
|
|
295
|
+
| prune | daily | Deletes telemetry older than `retention_days`, then `ANALYZE` |
|
|
296
|
+
|
|
297
|
+
Because the clock lives in the web process, it keeps running when the
|
|
298
|
+
job worker is down, which is exactly when "scheduled task X missed its
|
|
299
|
+
run" needs to be raised. With Solid Queue, the issue also says which of
|
|
300
|
+
three things happened: the scheduler never enqueued the run, it was
|
|
301
|
+
enqueued but no worker is running, or a worker is alive and it is
|
|
302
|
+
waiting behind a backlog.
|
|
303
|
+
|
|
304
|
+
Every batch is written exactly once. The reporter gives each batch an
|
|
305
|
+
id before its first attempt and the write records it in the same
|
|
306
|
+
transaction as the rows, so a write that fails (the file locked by a
|
|
307
|
+
backup, say) is retried with backoff and a retry of a batch that did
|
|
308
|
+
commit is a no-op. `bin/rails railwatch:doctor` reports the last
|
|
309
|
+
tick. If you installed a pre-release that added `Railwatch::*` entries to
|
|
310
|
+
`config/recurring.yml`, remove them; the doctor says so too.
|
|
311
|
+
|
|
312
|
+
## Settings
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
Railwatch.configure do |c|
|
|
316
|
+
c.transport = :local # RAILWATCH_TRANSPORT=local
|
|
317
|
+
c.issue_prefix = "SHOP" # RAILWATCH_ISSUE_PREFIX; keys like SHOP-12
|
|
318
|
+
c.repository_url = "https://github.com/you/shop" # RAILWATCH_REPOSITORY_URL
|
|
319
|
+
c.retention_days = 7 # RAILWATCH_RETENTION_DAYS
|
|
320
|
+
c.http_basic_auth_enabled = true # RAILWATCH_HTTP_BASIC_AUTH_ENABLED; credentials from Rails credentials or env
|
|
321
|
+
c.base_controller_class = "ActionController::Base" # RAILWATCH_BASE_CONTROLLER_CLASS
|
|
322
|
+
c.dashboard_open = false # RAILWATCH_DASHBOARD_OPEN; "yes, public, on purpose"
|
|
323
|
+
c.dashboard_user = ->(request) { ... }
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Every other option (sampling, redaction, ignored record types) applies
|
|
328
|
+
unchanged. The default samples every execution; set `c.sample` lower on
|
|
329
|
+
a busy app.
|
|
330
|
+
|
|
331
|
+
## Deploys
|
|
332
|
+
|
|
333
|
+
`bin/rails railwatch:deploy` records the marker in the app's own
|
|
334
|
+
database instead of posting it, and the Kamal post-deploy hook does the
|
|
335
|
+
same when `RAILWATCH_TRANSPORT=local` is set on the deployer.
|
|
336
|
+
|
|
337
|
+
## Storage and overhead
|
|
338
|
+
|
|
339
|
+
Telemetry is written by the reporter thread in batches, never on a
|
|
340
|
+
request. The cost on the request path is the same instrumentation the
|
|
341
|
+
cloud transport pays; the write itself adds under a millisecond at the
|
|
342
|
+
99th percentile at a hundred requests per second on a single Puma. The
|
|
343
|
+
telemetry database grows with traffic and sampling and is pruned to
|
|
344
|
+
`retention_days`; the `railwatch` database stays small. Both are plain
|
|
345
|
+
SQLite files in `storage/`, so Litestream or a volume snapshot covers
|
|
346
|
+
them.
|
|
347
|
+
|
|
348
|
+
## Switching to the cloud later
|
|
349
|
+
|
|
350
|
+
Set a token and drop `c.transport = :local` (or set
|
|
351
|
+
`RAILWATCH_TRANSPORT=http`). The local databases can stay; the dashboard
|
|
352
|
+
at `/railwatch` keeps reading what is there until it is pruned.
|
data/docs/getting-started.md
CHANGED
|
@@ -4,6 +4,11 @@ Five minutes from `bundle add` to a request on the dashboard, on a
|
|
|
4
4
|
Rails 8 app. Everything below is the gem's own generator and rake tasks;
|
|
5
5
|
nothing else has to be wired by hand.
|
|
6
6
|
|
|
7
|
+
Running one server and want the dashboard inside the app itself, with
|
|
8
|
+
no token and no cloud? That is `bin/rails generate railwatch:install
|
|
9
|
+
--local`; see [Embedded mode](embedded.md). The rest of this page is
|
|
10
|
+
the cloud install.
|
|
11
|
+
|
|
7
12
|
## 1. Add the gem
|
|
8
13
|
|
|
9
14
|
```sh
|
|
@@ -8,8 +8,11 @@ module Railwatch
|
|
|
8
8
|
class InstallGenerator < Rails::Generators::Base
|
|
9
9
|
source_root File.expand_path("templates", __dir__)
|
|
10
10
|
|
|
11
|
-
desc "Creates config/initializers/railwatch.rb, a Kamal post-deploy hook, the browser client, and wires the test helpers."
|
|
11
|
+
desc "Creates config/initializers/railwatch.rb, a Kamal post-deploy hook, the browser client, and wires the test helpers. " \
|
|
12
|
+
"With --local, also the two SQLite databases the in-app dashboard needs."
|
|
12
13
|
|
|
14
|
+
class_option :local, type: :boolean, default: false,
|
|
15
|
+
desc: "Keep telemetry in this app and serve the dashboard at /railwatch: no token, no cloud."
|
|
13
16
|
class_option :token, type: :string,
|
|
14
17
|
desc: "Deprecated: token in process arguments. Prefer --prompt-token, --token-stdin, or RAILWATCH_TOKEN."
|
|
15
18
|
class_option :prompt_token, type: :boolean, default: false,
|
|
@@ -43,6 +46,75 @@ module Railwatch
|
|
|
43
46
|
template "initializer.rb", "config/initializers/railwatch.rb"
|
|
44
47
|
end
|
|
45
48
|
|
|
49
|
+
# Not a gemspec dependency: the breakage is the host application's
|
|
50
|
+
# either way (its own sessions are failing), so this offers the pin in
|
|
51
|
+
# the app's Gemfile, where the app can drop it the day Rails ships the
|
|
52
|
+
# fix, rather than constraining every bundle that installs this gem.
|
|
53
|
+
def pin_json_when_it_cannot_decode
|
|
54
|
+
return unless Railwatch::JsonCompat.broken?
|
|
55
|
+
return say("#{Railwatch::JsonCompat.advice} (no Gemfile here to add it to)", :yellow) unless File.exist?("Gemfile")
|
|
56
|
+
|
|
57
|
+
contents = File.read("Gemfile")
|
|
58
|
+
return say_status(:identical, "Gemfile (json pin)", :blue) if contents.match?(/^\s*gem ["']json["']/)
|
|
59
|
+
|
|
60
|
+
append_to_file "Gemfile", "#{contents.end_with?("\n") ? "" : "\n"}\n" \
|
|
61
|
+
"# #{Railwatch::JsonCompat::ISSUE}: Rails cannot decode with json 3 yet. Remove when it can.\n" \
|
|
62
|
+
"#{Railwatch::JsonCompat::PIN}\n"
|
|
63
|
+
@needs_bundle = true
|
|
64
|
+
say "#{Railwatch::JsonCompat.advice} Added the pin to your Gemfile; run `bundle install`, then " \
|
|
65
|
+
"`bin/rails db:prepare`.", :yellow
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Embedded mode stores telemetry in SQLite files whatever the app's own
|
|
69
|
+
# database is, so an app on PostgreSQL or MySQL needs the adapter gem
|
|
70
|
+
# added before those files can be created.
|
|
71
|
+
def ensure_sqlite3_gem
|
|
72
|
+
return unless options[:local]
|
|
73
|
+
return if Gem.loaded_specs.key?("sqlite3")
|
|
74
|
+
return say("--local needs the sqlite3 gem for its two databases; add `gem \"sqlite3\"` and re-run.", :yellow) unless File.exist?("Gemfile")
|
|
75
|
+
|
|
76
|
+
contents = File.read("Gemfile")
|
|
77
|
+
unless contents.match?(/^\s*gem ["']sqlite3["']/)
|
|
78
|
+
append_to_file "Gemfile", %(#{contents.end_with?("\n") ? "" : "\n"}\n# Railwatch (embedded) keeps its telemetry in two SQLite files of its own.\ngem "sqlite3"\n)
|
|
79
|
+
end
|
|
80
|
+
@needs_bundle = true
|
|
81
|
+
say "Added `gem \"sqlite3\"` to the Gemfile: Railwatch's two databases are SQLite files whatever this app's " \
|
|
82
|
+
"own database is. Run `bundle install`, then `bin/rails db:prepare` to create them.", :yellow
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Two databases of its own, never the app's primary: `railwatch` for
|
|
87
|
+
# what people author (issues, comments, saved views, thresholds) and
|
|
88
|
+
# `railwatch_telemetry` for what the app reports, which is written
|
|
89
|
+
# continuously and pruned. Their migrations live in the gem; the
|
|
90
|
+
# database entries point migrations_paths at them, so db:prepare
|
|
91
|
+
# creates the tables now and migrates them after every gem update.
|
|
92
|
+
# Nothing is copied into the app.
|
|
93
|
+
def configure_local_databases
|
|
94
|
+
return unless options[:local]
|
|
95
|
+
|
|
96
|
+
return say("--local: no config/database.yml found; add railwatch and railwatch_telemetry databases yourself (docs/embedded.md).", :yellow) unless File.exist?("config/database.yml")
|
|
97
|
+
|
|
98
|
+
contents = File.read("config/database.yml")
|
|
99
|
+
updated = self.class.database_yml_with_railwatch(contents)
|
|
100
|
+
return say_status(:identical, "config/database.yml", :blue) if updated == contents
|
|
101
|
+
|
|
102
|
+
create_file "config/database.yml", updated, force: true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The writer process: one per Puma master, forked by the gem's Puma
|
|
106
|
+
# plugin, so batches are mapped and written outside the web workers.
|
|
107
|
+
def configure_local_writer
|
|
108
|
+
return unless options[:local]
|
|
109
|
+
return say("--local: no config/puma.rb found; add `plugin :railwatch` to your Puma config yourself (docs/embedded.md).", :yellow) unless File.exist?("config/puma.rb")
|
|
110
|
+
|
|
111
|
+
contents = File.read("config/puma.rb")
|
|
112
|
+
updated = self.class.puma_rb_with_railwatch(contents)
|
|
113
|
+
return say_status(:identical, "config/puma.rb", :blue) if updated == contents
|
|
114
|
+
|
|
115
|
+
create_file "config/puma.rb", updated, force: true
|
|
116
|
+
end
|
|
117
|
+
|
|
46
118
|
def create_kamal_hook
|
|
47
119
|
return unless File.exist?("config/deploy.yml")
|
|
48
120
|
template "post-deploy", ".kamal/hooks/post-deploy"
|
|
@@ -92,6 +164,8 @@ module Railwatch
|
|
|
92
164
|
# A token lands in .env only when Git confirms the file is ignored.
|
|
93
165
|
# URLs are not secret and can still be written to a tracked dotenv file.
|
|
94
166
|
def write_env
|
|
167
|
+
return if options[:local]
|
|
168
|
+
|
|
95
169
|
token = resolved_token
|
|
96
170
|
if options[:token]
|
|
97
171
|
say("--token exposes #{Railwatch::SecretSafety.token_preview(options[:token])} in process arguments; " \
|
|
@@ -142,7 +216,53 @@ module Railwatch
|
|
|
142
216
|
create_file "config/deploy.yml", updated, force: true
|
|
143
217
|
end
|
|
144
218
|
|
|
219
|
+
# The two databases exist and are migrated when the generator returns:
|
|
220
|
+
# config/database.yml was just rewritten, so this re-reads it and runs
|
|
221
|
+
# the same prepare a deploy runs, for both databases only. The host's
|
|
222
|
+
# own databases are not touched, and a schema file is never written.
|
|
223
|
+
def prepare_local_databases
|
|
224
|
+
return unless options[:local]
|
|
225
|
+
return unless File.exist?("config/database.yml")
|
|
226
|
+
return if @needs_bundle
|
|
227
|
+
return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
228
|
+
|
|
229
|
+
say "\nbin/rails db:prepare (railwatch, railwatch_telemetry)", :green
|
|
230
|
+
require "active_record"
|
|
231
|
+
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
|
|
232
|
+
%w[railwatch railwatch_telemetry].each do |name|
|
|
233
|
+
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: name)
|
|
234
|
+
next say(" #{name}: not in config/database.yml for #{Rails.env}", :yellow) unless db_config
|
|
235
|
+
|
|
236
|
+
ActiveRecord::Tasks::DatabaseTasks.with_temporary_pool_for_each(env: Rails.env, name: name) do
|
|
237
|
+
ActiveRecord::Tasks::DatabaseTasks.migrate
|
|
238
|
+
end
|
|
239
|
+
say_status :prepared, "#{name} (#{db_config.database})", :green
|
|
240
|
+
end
|
|
241
|
+
@prepared = true
|
|
242
|
+
rescue StandardError => e
|
|
243
|
+
say "Could not prepare the Railwatch databases here (#{e.class}: #{e.message}). Run `bin/rails db:prepare` yourself.", :yellow
|
|
244
|
+
end
|
|
245
|
+
|
|
145
246
|
def show_next_steps
|
|
247
|
+
if options[:local]
|
|
248
|
+
say <<~STEPS, :green
|
|
249
|
+
|
|
250
|
+
Next steps
|
|
251
|
+
1. Set the dashboard's HTTP Basic credentials (it is closed until
|
|
252
|
+
you do): bin/rails railwatch:authentication:configure
|
|
253
|
+
Using your own admin auth instead? See docs/embedded.md,
|
|
254
|
+
Authentication (base_controller_class or a routes constraint).
|
|
255
|
+
2. Restart the app and open /railwatch.
|
|
256
|
+
3. #{@prepared ? "Nothing else to run. Both databases were created just now and" : "Create the two databases: bin/rails db:prepare\n Then"}
|
|
257
|
+
`bin/rails db:prepare` (which a deploy already runs) migrates
|
|
258
|
+
them after every gem update. With `plugin :railwatch` in
|
|
259
|
+
config/puma.rb Puma forks one Railwatch writer process that
|
|
260
|
+
writes every batch and runs the maintenance clock, so no web
|
|
261
|
+
process ever holds the telemetry database. No job worker.
|
|
262
|
+
STEPS
|
|
263
|
+
return
|
|
264
|
+
end
|
|
265
|
+
|
|
146
266
|
say <<~STEPS, :green
|
|
147
267
|
|
|
148
268
|
Next steps
|
|
@@ -166,6 +286,10 @@ module Railwatch
|
|
|
166
286
|
# note below says so rather than letting a ✗ look like a broken install.
|
|
167
287
|
def run_doctor
|
|
168
288
|
return unless options[:doctor]
|
|
289
|
+
# This process read its configuration before the initializer was
|
|
290
|
+
# written, so in local mode the doctor would report an http transport
|
|
291
|
+
# with no token. The databases it would check were prepared above.
|
|
292
|
+
return if options[:local]
|
|
169
293
|
return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
170
294
|
|
|
171
295
|
say "\nbin/rails railwatch:doctor", :green
|
|
@@ -199,6 +323,103 @@ module Railwatch
|
|
|
199
323
|
insert_lines(lines, block_end(lines, secret_start), " - #{name}\n")
|
|
200
324
|
end
|
|
201
325
|
|
|
326
|
+
# Spelled out rather than `<<: *default`, on purpose. The host's default
|
|
327
|
+
# block carries ITS adapter: on a PostgreSQL or MySQL app inheriting it
|
|
328
|
+
# would ask that server for a database called
|
|
329
|
+
# "storage/production_railwatch.sqlite3". These two are always SQLite
|
|
330
|
+
# files the app owns, whatever the app's own database is, so they name
|
|
331
|
+
# their adapter themselves -- which also means the file needs no
|
|
332
|
+
# `&default` anchor to exist at all.
|
|
333
|
+
RAILWATCH_DATABASES = <<~YAML
|
|
334
|
+
railwatch:
|
|
335
|
+
adapter: sqlite3
|
|
336
|
+
database: storage/%<env>s_railwatch.sqlite3
|
|
337
|
+
pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %%>
|
|
338
|
+
timeout: 5000
|
|
339
|
+
migrations_paths: <%%= Railwatch.migrations_path(:railwatch) %%>
|
|
340
|
+
schema_dump: false
|
|
341
|
+
railwatch_telemetry:
|
|
342
|
+
adapter: sqlite3
|
|
343
|
+
database: storage/%<env>s_railwatch_telemetry.sqlite3
|
|
344
|
+
pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %%>
|
|
345
|
+
timeout: 5000
|
|
346
|
+
migrations_paths: <%%= Railwatch.migrations_path(:railwatch_telemetry) %%>
|
|
347
|
+
schema_dump: false
|
|
348
|
+
pragmas:
|
|
349
|
+
journal_mode: wal
|
|
350
|
+
synchronous: normal
|
|
351
|
+
mmap_size: 134217728
|
|
352
|
+
cache_size: -65536
|
|
353
|
+
temp_store: memory
|
|
354
|
+
YAML
|
|
355
|
+
|
|
356
|
+
# Adds the railwatch and railwatch_telemetry databases to every
|
|
357
|
+
# environment in config/database.yml. A flat environment
|
|
358
|
+
# (`development:` straight to `<<: *default`) becomes a `primary:`
|
|
359
|
+
# entry first, since named databases need the nested form. Text
|
|
360
|
+
# insertion rather than a YAML round trip, for the same reason as
|
|
361
|
+
# deploy_yml_with_secret: the comments are most of the file.
|
|
362
|
+
def self.database_yml_with_railwatch(contents)
|
|
363
|
+
lines = contents.lines
|
|
364
|
+
# Rails 8.1's non-Docker template leaves every production `database:`
|
|
365
|
+
# commented out ("path/to/persistent/storage/..."), so db:prepare
|
|
366
|
+
# cannot run in production at all until the host fills them in. The
|
|
367
|
+
# storage/ paths are what the Docker template writes and what
|
|
368
|
+
# config/deploy.yml mounts; use them.
|
|
369
|
+
lines = lines.map do |line|
|
|
370
|
+
line.sub(%r{\A(\s+)# database: path/to/persistent/storage/(\S+)$}, '\1database: storage/\2')
|
|
371
|
+
end
|
|
372
|
+
environments_in(lines).each do |env|
|
|
373
|
+
start = lines.index { |line| line.match?(/\A#{env}:\s*(#.*)?$/) }
|
|
374
|
+
next unless start
|
|
375
|
+
|
|
376
|
+
stop = block_end(lines, start)
|
|
377
|
+
block = lines[(start + 1)...stop]
|
|
378
|
+
next if block.any? { |line| line.match?(/\A\s+railwatch_telemetry:\s*$/) }
|
|
379
|
+
|
|
380
|
+
nested = block.any? { |line| line.match?(/\A [a-z_]+:\s*$/) }
|
|
381
|
+
unless nested
|
|
382
|
+
lines[(start + 1)...stop] = block.map { |line| line.strip.empty? ? line : " #{line}" }
|
|
383
|
+
lines.insert(start + 1, " primary:\n")
|
|
384
|
+
stop += 1
|
|
385
|
+
end
|
|
386
|
+
entries = format(RAILWATCH_DATABASES, env: env).lines.map { |line| " #{line}" }
|
|
387
|
+
lines = insert_lines(lines, stop, entries.join).lines
|
|
388
|
+
end
|
|
389
|
+
lines.join
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Unconditional: `bundle exec puma` evaluates this file before it loads
|
|
393
|
+
# the Rack app, so `if defined?(Railwatch)` would be false there and the
|
|
394
|
+
# writer would never start. The plugin itself is inert when Railwatch is
|
|
395
|
+
# absent, off, or not in embedded mode.
|
|
396
|
+
PUMA_PLUGIN_LINES = <<~RUBY
|
|
397
|
+
|
|
398
|
+
# Railwatch (embedded): fork one writer process from Puma so telemetry
|
|
399
|
+
# is mapped and written outside the processes serving requests.
|
|
400
|
+
plugin :railwatch
|
|
401
|
+
RUBY
|
|
402
|
+
|
|
403
|
+
# Appends the plugin line once. Puma's config is plain Ruby evaluated top
|
|
404
|
+
# to bottom, so the end of the file is always a valid place for it.
|
|
405
|
+
def self.puma_rb_with_railwatch(contents)
|
|
406
|
+
return contents if contents.include?("plugin :railwatch")
|
|
407
|
+
|
|
408
|
+
"#{contents.sub(/\n*\z/, "\n")}#{PUMA_PLUGIN_LINES}"
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Every environment the file defines, not a fixed three: an app with a
|
|
412
|
+
# `staging` (or `review`, or `qa`) environment needs the databases
|
|
413
|
+
# there too, and hardcoding names silently left it without them. A
|
|
414
|
+
# top-level key with a block under it, minus YAML's own anchors.
|
|
415
|
+
NON_ENVIRONMENT_KEYS = %w[default shared].freeze
|
|
416
|
+
|
|
417
|
+
def self.environments_in(lines)
|
|
418
|
+
lines.filter_map { |line| line[/\A([a-z_][a-z0-9_]*):\s*(?:&\S+\s*)?(?:#.*)?$/, 1] }
|
|
419
|
+
.reject { |name| NON_ENVIRONMENT_KEYS.include?(name) }
|
|
420
|
+
.uniq
|
|
421
|
+
end
|
|
422
|
+
|
|
202
423
|
# Index of the first line after the block opened at `start`: the next
|
|
203
424
|
# line indented no more deeply than it, ignoring blanks and comments,
|
|
204
425
|
# then backed up over any trailing blank lines so an insertion lands
|
|
@@ -3,7 +3,46 @@
|
|
|
3
3
|
# Railwatch: first-class monitoring for Rails. Every option here can also be
|
|
4
4
|
# set by the RAILWATCH_* env var named in the comment.
|
|
5
5
|
Railwatch.configure do |c|
|
|
6
|
+
<% if options[:local] -%>
|
|
7
|
+
# Telemetry stays in this app's own railwatch_telemetry database and the
|
|
8
|
+
# dashboard is served at /railwatch. No token, no cloud. Put the mount
|
|
9
|
+
# behind your own authentication; this only names who is looking.
|
|
10
|
+
c.transport = :local # RAILWATCH_TRANSPORT
|
|
11
|
+
c.ignored_request_paths += ["/railwatch", %r{\A/railwatch/}]
|
|
12
|
+
# c.issue_prefix = "APP" # RAILWATCH_ISSUE_PREFIX; issue keys like APP-12
|
|
13
|
+
# c.repository_url = "https://github.com/you/app" # RAILWATCH_REPOSITORY_URL; source links from stack traces
|
|
14
|
+
# c.retention_days = 7 # RAILWATCH_RETENTION_DAYS; PruneTelemetryJob keeps this much
|
|
15
|
+
# Access. The dashboard shows every query, log line and exception this app
|
|
16
|
+
# records, so pick one of these. Out of the box it is HTTP Basic, on and
|
|
17
|
+
# closed until credentials exist:
|
|
18
|
+
# bin/rails railwatch:authentication:configure (writes Rails credentials)
|
|
19
|
+
# or RAILWATCH_HTTP_BASIC_AUTH_USER / _PASSWORD.
|
|
20
|
+
#
|
|
21
|
+
# Using your own admin auth instead? Turn Basic off and say which, so the
|
|
22
|
+
# gem knows the mount is covered and live updates can follow your rule:
|
|
23
|
+
# c.http_basic_auth_enabled = false # RAILWATCH_HTTP_BASIC_AUTH_ENABLED
|
|
24
|
+
# c.base_controller_class = "AdminController" # every dashboard page inherits it
|
|
25
|
+
# c.dashboard_user = ->(request) { ... } # or decide per request, below
|
|
26
|
+
#
|
|
27
|
+
# Genuinely public (a private network, a VPN, a constraint the gem cannot
|
|
28
|
+
# see)? Say so on purpose. Nothing else makes the dashboard public, and
|
|
29
|
+
# without one of the above the app logs a warning at every boot:
|
|
30
|
+
# c.dashboard_open = false # RAILWATCH_DASHBOARD_OPEN
|
|
31
|
+
# Who the dashboard names on comments and saved views. Returning nil also
|
|
32
|
+
# refuses the request, so this doubles as your authorisation rule:
|
|
33
|
+
# c.dashboard_user = ->(request) { user = Current.user; user&.admin? && { id: user.id, name: user.name, email: user.email } }
|
|
34
|
+
|
|
35
|
+
# The browser beacon (Inertia visit timings and JavaScript errors) is a
|
|
36
|
+
# public endpoint, like every browser telemetry endpoint: it is bounded by
|
|
37
|
+
# an origin allowlist, a per-client rate limit and a ceiling for the whole
|
|
38
|
+
# endpoint, not by a credential.
|
|
39
|
+
# c.beacon_enabled = false # RAILWATCH_BEACON
|
|
40
|
+
# c.beacon_allowed_origins = ["https://app.example.com"] # beyond this app's own
|
|
41
|
+
# c.beacon_rate_limit = 120 # per client IP per minute
|
|
42
|
+
# c.beacon_global_rate_limit = 6_000 # for the endpoint, per minute
|
|
43
|
+
<% else -%>
|
|
6
44
|
# c.token = ENV["RAILWATCH_TOKEN"] # RAILWATCH_TOKEN (required)
|
|
45
|
+
<% end -%>
|
|
7
46
|
# c.ingest_url = "https://railwatch.rebulk.com" # RAILWATCH_INGEST_URL
|
|
8
47
|
# c.deploy = "release-name" # RAILWATCH_DEPLOY; platform/Git auto-detected
|
|
9
48
|
# c.detect_deploy = false # RAILWATCH_DETECT_DEPLOY; default true
|