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
|
@@ -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
|
data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb
ADDED
|
@@ -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
|
data/docs/configuration.md
CHANGED
|
@@ -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
|