railwatch 0.9.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +37 -33
- data/CHANGELOG.md +66 -0
- data/README.md +51 -60
- data/config/routes.rb +0 -59
- data/docs/ai-and-mcp.md +9 -8
- data/docs/configuration.md +57 -71
- data/docs/faq.md +17 -25
- data/docs/getting-started.md +105 -117
- data/docs/replacing-nightwatch.md +8 -7
- data/docs/replacing-sentry.md +12 -16
- data/docs/security.md +14 -20
- data/docs/self-hosting.md +24 -22
- data/docs/testing.md +6 -14
- data/docs/troubleshooting.md +12 -46
- data/lib/generators/railwatch/install/install_generator.rb +12 -240
- data/lib/generators/railwatch/install/templates/initializer.rb.tt +10 -40
- data/lib/generators/railwatch/install/templates/post-deploy +0 -7
- data/lib/railwatch/authentication.rb +6 -5
- data/lib/railwatch/buffer.rb +9 -0
- data/lib/railwatch/configuration.rb +21 -151
- data/lib/railwatch/engine.rb +9 -118
- data/lib/railwatch/patches.rb +0 -11
- data/lib/railwatch/reporter.rb +92 -21
- data/lib/railwatch/sampler.rb +10 -2
- data/lib/railwatch/subscribers/process_info.rb +1 -2
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch.rb +8 -64
- data/lib/tasks/railwatch_tasks.rake +3 -325
- data/llms.txt +23 -25
- metadata +3 -453
- data/app/channels/railwatch/environment_channel.rb +0 -62
- data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
- data/app/controllers/railwatch/alerts_controller.rb +0 -48
- data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
- data/app/controllers/railwatch/attachments_controller.rb +0 -22
- data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
- data/app/controllers/railwatch/cache_events_controller.rb +0 -30
- data/app/controllers/railwatch/commands_controller.rb +0 -16
- data/app/controllers/railwatch/comments_controller.rb +0 -11
- data/app/controllers/railwatch/dashboard_controller.rb +0 -70
- data/app/controllers/railwatch/deploys_controller.rb +0 -34
- data/app/controllers/railwatch/deprecations_controller.rb +0 -54
- data/app/controllers/railwatch/environment_scoped.rb +0 -81
- data/app/controllers/railwatch/exceptions_controller.rb +0 -52
- data/app/controllers/railwatch/executions_controller.rb +0 -13
- data/app/controllers/railwatch/issues_controller.rb +0 -258
- data/app/controllers/railwatch/jobs_controller.rb +0 -63
- data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
- data/app/controllers/railwatch/logs_controller.rb +0 -40
- data/app/controllers/railwatch/mails_controller.rb +0 -19
- data/app/controllers/railwatch/notifications_controller.rb +0 -11
- data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
- data/app/controllers/railwatch/overview_controller.rb +0 -38
- data/app/controllers/railwatch/people_controller.rb +0 -25
- data/app/controllers/railwatch/processes_controller.rb +0 -33
- data/app/controllers/railwatch/profiles_controller.rb +0 -65
- data/app/controllers/railwatch/queries_controller.rb +0 -59
- data/app/controllers/railwatch/releases_controller.rb +0 -75
- data/app/controllers/railwatch/requests_controller.rb +0 -37
- data/app/controllers/railwatch/saved_views_controller.rb +0 -53
- data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
- data/app/controllers/railwatch/spans_controller.rb +0 -47
- data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
- data/app/controllers/railwatch/tenants_controller.rb +0 -44
- data/app/controllers/railwatch/thresholds_controller.rb +0 -47
- data/app/controllers/railwatch/traces_controller.rb +0 -72
- data/app/controllers/railwatch/transactions_controller.rb +0 -21
- data/app/controllers/railwatch/view_renders_controller.rb +0 -28
- data/app/controllers/railwatch/visits_controller.rb +0 -53
- data/app/frontend/components/app-content.tsx +0 -26
- data/app/frontend/components/app-logo-icon.tsx +0 -23
- data/app/frontend/components/app-logo.tsx +0 -14
- data/app/frontend/components/app-shell.tsx +0 -38
- data/app/frontend/components/app-sidebar-header.tsx +0 -54
- data/app/frontend/components/app-sidebar.tsx +0 -94
- data/app/frontend/components/app-wordmark.tsx +0 -29
- data/app/frontend/components/breadcrumbs.tsx +0 -46
- data/app/frontend/components/icon.tsx +0 -16
- data/app/frontend/components/input-error.tsx +0 -18
- data/app/frontend/components/nav-footer.tsx +0 -46
- data/app/frontend/components/nav-main.tsx +0 -41
- data/app/frontend/components/nav-user.tsx +0 -50
- data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
- data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
- data/app/frontend/components/railwatch/code.tsx +0 -28
- data/app/frontend/components/railwatch/command-palette.tsx +0 -132
- data/app/frontend/components/railwatch/copy-block.tsx +0 -52
- data/app/frontend/components/railwatch/copy-id.tsx +0 -49
- data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
- data/app/frontend/components/railwatch/data-table.tsx +0 -195
- data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
- data/app/frontend/components/railwatch/empty-state.tsx +0 -159
- data/app/frontend/components/railwatch/env-nav.tsx +0 -130
- data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
- data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
- data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
- data/app/frontend/components/railwatch/frames.tsx +0 -295
- data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
- data/app/frontend/components/railwatch/live-dot.tsx +0 -78
- data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
- data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
- data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
- data/app/frontend/components/railwatch/onboarding.tsx +0 -91
- data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
- data/app/frontend/components/railwatch/page-header.tsx +0 -42
- data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
- data/app/frontend/components/railwatch/relative-time.tsx +0 -18
- data/app/frontend/components/railwatch/saved-views.tsx +0 -232
- data/app/frontend/components/railwatch/segmented.tsx +0 -64
- data/app/frontend/components/railwatch/series-chart.tsx +0 -400
- data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
- data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
- data/app/frontend/components/railwatch/sort-header.tsx +0 -37
- data/app/frontend/components/railwatch/source-link.tsx +0 -57
- data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
- data/app/frontend/components/railwatch/sql-block.tsx +0 -116
- data/app/frontend/components/railwatch/stat.tsx +0 -185
- data/app/frontend/components/railwatch/status-badge.tsx +0 -79
- data/app/frontend/components/railwatch/step-picker.tsx +0 -57
- data/app/frontend/components/railwatch/timeline.tsx +0 -405
- data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
- data/app/frontend/components/railwatch/window-picker.tsx +0 -147
- data/app/frontend/components/ui/avatar.tsx +0 -51
- data/app/frontend/components/ui/badge.tsx +0 -46
- data/app/frontend/components/ui/breadcrumb.tsx +0 -109
- data/app/frontend/components/ui/button.tsx +0 -60
- data/app/frontend/components/ui/card.tsx +0 -92
- data/app/frontend/components/ui/chart.tsx +0 -374
- data/app/frontend/components/ui/checkbox.tsx +0 -32
- data/app/frontend/components/ui/collapsible.tsx +0 -31
- data/app/frontend/components/ui/command.tsx +0 -182
- data/app/frontend/components/ui/dialog.tsx +0 -158
- data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
- data/app/frontend/components/ui/hover-card.tsx +0 -42
- data/app/frontend/components/ui/input.tsx +0 -21
- data/app/frontend/components/ui/label.tsx +0 -24
- data/app/frontend/components/ui/popover.tsx +0 -87
- data/app/frontend/components/ui/select.tsx +0 -185
- data/app/frontend/components/ui/separator.tsx +0 -28
- data/app/frontend/components/ui/sheet.tsx +0 -137
- data/app/frontend/components/ui/sidebar.tsx +0 -726
- data/app/frontend/components/ui/skeleton.tsx +0 -13
- data/app/frontend/components/ui/sonner.tsx +0 -41
- data/app/frontend/components/ui/table.tsx +0 -114
- data/app/frontend/components/ui/tabs.tsx +0 -91
- data/app/frontend/components/ui/textarea.tsx +0 -18
- data/app/frontend/components/ui/tooltip.tsx +0 -61
- data/app/frontend/components/user-info.tsx +0 -32
- data/app/frontend/components/user-menu-content.tsx +0 -94
- data/app/frontend/entrypoints/application.css +0 -244
- data/app/frontend/entrypoints/inertia.tsx +0 -92
- data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
- data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
- data/app/frontend/hooks/use-appearance.tsx +0 -67
- data/app/frontend/hooks/use-clipboard.ts +0 -32
- data/app/frontend/hooks/use-flash.tsx +0 -23
- data/app/frontend/hooks/use-hotkeys.ts +0 -63
- data/app/frontend/hooks/use-initials.tsx +0 -15
- data/app/frontend/hooks/use-live.ts +0 -205
- data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
- data/app/frontend/hooks/use-mobile.ts +0 -21
- data/app/frontend/hooks/use-percentile.ts +0 -24
- data/app/frontend/hooks/use-reduced-motion.ts +0 -25
- data/app/frontend/hooks/use-row-nav.ts +0 -106
- data/app/frontend/hooks/use-step.ts +0 -27
- data/app/frontend/hooks/use-ticker.ts +0 -12
- data/app/frontend/hooks/use-window.ts +0 -58
- data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
- data/app/frontend/layouts/app-layout.tsx +0 -21
- data/app/frontend/layouts/env-layout.tsx +0 -53
- data/app/frontend/layouts/persistent-layout.tsx +0 -18
- data/app/frontend/lib/cable.ts +0 -10
- data/app/frontend/lib/env-pages.ts +0 -236
- data/app/frontend/lib/execution-path.ts +0 -55
- data/app/frontend/lib/explain.ts +0 -50
- data/app/frontend/lib/filter.ts +0 -25
- data/app/frontend/lib/flamegraph.ts +0 -155
- data/app/frontend/lib/format.ts +0 -71
- data/app/frontend/lib/keyboard.ts +0 -18
- data/app/frontend/lib/mount.ts +0 -23
- data/app/frontend/lib/source-link.ts +0 -217
- data/app/frontend/lib/utils.ts +0 -6
- data/app/frontend/pages/alerts/index.tsx +0 -171
- data/app/frontend/pages/broadcasts/index.tsx +0 -171
- data/app/frontend/pages/cache_events/index.tsx +0 -127
- data/app/frontend/pages/commands/index.tsx +0 -143
- data/app/frontend/pages/deploys/index.tsx +0 -94
- data/app/frontend/pages/deploys/show.tsx +0 -309
- data/app/frontend/pages/deprecations/index.tsx +0 -176
- data/app/frontend/pages/exceptions/index.tsx +0 -271
- data/app/frontend/pages/executions/show.tsx +0 -821
- data/app/frontend/pages/issues/index.tsx +0 -399
- data/app/frontend/pages/issues/show.tsx +0 -1367
- data/app/frontend/pages/jobs/index.tsx +0 -381
- data/app/frontend/pages/jobs/klass.tsx +0 -215
- data/app/frontend/pages/llm_calls/index.tsx +0 -664
- data/app/frontend/pages/logs/index.tsx +0 -303
- data/app/frontend/pages/mails/index.tsx +0 -218
- data/app/frontend/pages/notifications/index.tsx +0 -161
- data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
- data/app/frontend/pages/overview/show.tsx +0 -333
- data/app/frontend/pages/people/index.tsx +0 -102
- data/app/frontend/pages/people/show.tsx +0 -149
- data/app/frontend/pages/processes/index.tsx +0 -335
- data/app/frontend/pages/profiles/index.tsx +0 -110
- data/app/frontend/pages/profiles/show.tsx +0 -105
- data/app/frontend/pages/queries/index.tsx +0 -492
- data/app/frontend/pages/queries/show.tsx +0 -276
- data/app/frontend/pages/releases/index.tsx +0 -190
- data/app/frontend/pages/releases/release-health.tsx +0 -68
- data/app/frontend/pages/releases/release-path.ts +0 -25
- data/app/frontend/pages/releases/show.tsx +0 -262
- data/app/frontend/pages/requests/index.tsx +0 -235
- data/app/frontend/pages/requests/route.tsx +0 -159
- data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
- data/app/frontend/pages/spans/index.tsx +0 -229
- data/app/frontend/pages/spans/show.tsx +0 -220
- data/app/frontend/pages/storage_ops/index.tsx +0 -150
- data/app/frontend/pages/tenants/index.tsx +0 -236
- data/app/frontend/pages/tenants/show.tsx +0 -477
- data/app/frontend/pages/tenants/tenant-path.ts +0 -26
- data/app/frontend/pages/thresholds/index.tsx +0 -414
- data/app/frontend/pages/traces/show.tsx +0 -208
- data/app/frontend/pages/transactions/index.tsx +0 -220
- data/app/frontend/pages/view_renders/index.tsx +0 -241
- data/app/frontend/pages/visits/index.tsx +0 -321
- data/app/frontend/public/icon.png +0 -0
- data/app/frontend/public/icon.svg +0 -5
- data/app/frontend/routes/index.d.ts +0 -2136
- data/app/frontend/routes/index.js +0 -2112
- data/app/frontend/types/globals.d.ts +0 -9
- data/app/frontend/types/index.ts +0 -288
- data/app/frontend/types/vite-env.d.ts +0 -9
- data/app/helpers/railwatch/assets_helper.rb +0 -52
- data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
- data/app/jobs/railwatch/application_job.rb +0 -7
- data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
- data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
- data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
- data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
- data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
- data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
- data/app/jobs/railwatch/performance_scan_job.rb +0 -11
- data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
- data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
- data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
- data/app/jobs/railwatch/rollup_job.rb +0 -132
- data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
- data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
- data/app/models/railwatch/alert.rb +0 -229
- data/app/models/railwatch/alert_rule.rb +0 -121
- data/app/models/railwatch/anomaly_rule.rb +0 -51
- data/app/models/railwatch/application.rb +0 -44
- data/app/models/railwatch/application_record.rb +0 -33
- data/app/models/railwatch/comment.rb +0 -36
- data/app/models/railwatch/deploy.rb +0 -67
- data/app/models/railwatch/environment.rb +0 -68
- data/app/models/railwatch/execution_presenter.rb +0 -185
- data/app/models/railwatch/filter_query.rb +0 -184
- data/app/models/railwatch/followup_receipt.rb +0 -27
- data/app/models/railwatch/ingest/batch.rb +0 -355
- data/app/models/railwatch/ingest/mapper.rb +0 -585
- data/app/models/railwatch/ingest/payload.rb +0 -96
- data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
- data/app/models/railwatch/ingest/writer.rb +0 -137
- data/app/models/railwatch/issue.rb +0 -277
- data/app/models/railwatch/issue_activity.rb +0 -23
- data/app/models/railwatch/issue_detection_presenter.rb +0 -309
- data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
- data/app/models/railwatch/maintenance_task.rb +0 -53
- data/app/models/railwatch/saved_view.rb +0 -63
- data/app/models/railwatch/telemetry/aggregations.rb +0 -251
- data/app/models/railwatch/telemetry/attachment.rb +0 -31
- data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
- data/app/models/railwatch/telemetry/broadcast.rb +0 -13
- data/app/models/railwatch/telemetry/cache_event.rb +0 -16
- data/app/models/railwatch/telemetry/child.rb +0 -53
- data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
- data/app/models/railwatch/telemetry/deprecation.rb +0 -13
- data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
- data/app/models/railwatch/telemetry/exception.rb +0 -28
- data/app/models/railwatch/telemetry/execution.rb +0 -152
- data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
- data/app/models/railwatch/telemetry/export_destination.rb +0 -66
- data/app/models/railwatch/telemetry/health_sample.rb +0 -72
- data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
- data/app/models/railwatch/telemetry/llm_call.rb +0 -37
- data/app/models/railwatch/telemetry/log.rb +0 -91
- data/app/models/railwatch/telemetry/mail.rb +0 -13
- data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
- data/app/models/railwatch/telemetry/notification.rb +0 -13
- data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
- data/app/models/railwatch/telemetry/person.rb +0 -50
- data/app/models/railwatch/telemetry/process.rb +0 -10
- data/app/models/railwatch/telemetry/profile.rb +0 -37
- data/app/models/railwatch/telemetry/query.rb +0 -57
- data/app/models/railwatch/telemetry/query_shape.rb +0 -45
- data/app/models/railwatch/telemetry/release_health.rb +0 -69
- data/app/models/railwatch/telemetry/rollup.rb +0 -157
- data/app/models/railwatch/telemetry/session.rb +0 -24
- data/app/models/railwatch/telemetry/span.rb +0 -19
- data/app/models/railwatch/telemetry/storage_op.rb +0 -13
- data/app/models/railwatch/telemetry/tenant.rb +0 -240
- data/app/models/railwatch/telemetry/transaction.rb +0 -13
- data/app/models/railwatch/telemetry/view_render.rb +0 -13
- data/app/models/railwatch/telemetry/visit.rb +0 -24
- data/app/models/railwatch/telemetry_record.rb +0 -107
- data/app/models/railwatch/threshold.rb +0 -66
- data/app/models/railwatch/user.rb +0 -47
- data/app/models/railwatch/viewer.rb +0 -13
- data/app/models/railwatch/window.rb +0 -66
- data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
- data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
- data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
- data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
- data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
- data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
- data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
- data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
- data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
- data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
- data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
- data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
- data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
- data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
- data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
- data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
- data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
- data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
- data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
- data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
- data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
- data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
- data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
- data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
- data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
- data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
- data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
- data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
- data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
- data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
- data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
- data/docs/embedded.md +0 -503
- data/lib/puma/plugin/railwatch.rb +0 -215
- data/lib/railwatch/dashboard_assets.rb +0 -45
- data/lib/railwatch/embedded.rb +0 -50
- data/lib/railwatch/export/client.rb +0 -124
- data/lib/railwatch/export/lease.rb +0 -56
- data/lib/railwatch/export/outbox.rb +0 -361
- data/lib/railwatch/export/policy.rb +0 -79
- data/lib/railwatch/export/sender.rb +0 -130
- data/lib/railwatch/ingest_request_body_limit.rb +0 -10
- data/lib/railwatch/maintenance.rb +0 -194
- data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
- data/lib/railwatch/transport/local.rb +0 -78
- data/lib/railwatch/transport/socket.rb +0 -183
- data/lib/railwatch/writer.rb +0 -370
- 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-by7G3sa1.js +0 -41
- data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
- data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
- data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
- data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
- data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
- data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
- data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
- data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
- data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
- data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
- data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
- data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
- data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
- data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
- data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
- data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
- data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
- data/public/railwatch/assets/index--NQ7us-L.js +0 -1
- data/public/railwatch/assets/index-2kgYe_99.js +0 -1
- data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
- data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
- data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
- data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
- data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
- data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
- data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
- data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
- data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
- data/public/railwatch/assets/index-C2j50AL2.js +0 -1
- data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
- data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
- data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
- data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
- data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
- data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
- data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
- data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
- data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
- data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
- data/public/railwatch/assets/index-Deq14BxB.js +0 -1
- data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
- data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
- data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
- data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
- data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
- data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
- data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
- data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
- data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
- data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
- data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
- data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
- data/public/railwatch/assets/route-CWh58C7K.js +0 -1
- data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
- data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
- data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
- data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
- data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
- data/public/railwatch/assets/show-BYPB74t4.js +0 -2
- data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
- data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
- data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
- data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
- data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
- data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
- data/public/railwatch/assets/show-g__jnN36.js +0 -1
- data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
- data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
- data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
- data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
- data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
- data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
- data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
- data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
- data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
- data/public/railwatch/icon.png +0 -0
- data/public/railwatch/icon.svg +0 -5
- data/public/railwatch/manifest.json +0 -1245
- data/public/railwatch/rails-vite.json +0 -1
- /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
module Telemetry
|
|
5
|
-
class Visit < TelemetryRecord
|
|
6
|
-
include Child
|
|
7
|
-
|
|
8
|
-
# Core Web Vitals reported by the browser client, with Google's
|
|
9
|
-
# [good, poor] thresholds: at or below good is "good", above poor is
|
|
10
|
-
# "poor". lcp/inp/ttfb are milliseconds, cls is unitless.
|
|
11
|
-
VITALS = { lcp: [ 2500, 4000 ], cls: [ 0.1, 0.25 ], inp: [ 200, 500 ], ttfb: [ 800, 1800 ] }.freeze
|
|
12
|
-
|
|
13
|
-
def self.rating(metric, value)
|
|
14
|
-
good, poor = VITALS.fetch(metric)
|
|
15
|
-
return "good" if value <= good
|
|
16
|
-
(value > poor) ? "poor" : "needs-improvement"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def timeline_label
|
|
20
|
-
component.to_s.first(120)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Base class for everything the engine stores about the host application.
|
|
5
|
-
# An embedded install monitors exactly one application, so this is a plain
|
|
6
|
-
# second database (`railwatch_telemetry` in the host's database.yml),
|
|
7
|
-
# separate from the host's own primary and from the engine's meta tables.
|
|
8
|
-
# The hosted platform keeps one SQLite file per monitored environment
|
|
9
|
-
# instead: it calls `Railwatch::TelemetryRecord.tenanted("telemetry")`
|
|
10
|
-
# (activerecord-tenanted) from an initializer, which replaces the
|
|
11
|
-
# connection below with a per-tenant one, and every telemetry query runs
|
|
12
|
-
# inside Environment#with_telemetry so the code path is the same in both.
|
|
13
|
-
class TelemetryRecord < ActiveRecord::Base
|
|
14
|
-
self.abstract_class = true
|
|
15
|
-
begin
|
|
16
|
-
connects_to database: { writing: :railwatch_telemetry, reading: :railwatch_telemetry }
|
|
17
|
-
rescue ActiveRecord::AdapterNotSpecified, LoadError
|
|
18
|
-
# No `railwatch_telemetry` entry in this environment's database.yml, or an entry
|
|
19
|
-
# whose adapter gem is not in the bundle yet (LoadError). A cloud-transport
|
|
20
|
-
# app has none and still eager-loads this class in production, and so
|
|
21
|
-
# does the installer's own boot, before it has written the
|
|
22
|
-
# entry -- so loading must not raise. Using it must, though: without
|
|
23
|
-
# connects_to this class would inherit ActiveRecord::Base's PRIMARY
|
|
24
|
-
# connection, and its tables are unprefixed, so a query would read and
|
|
25
|
-
# a write would corrupt the host application's own tables. Every route
|
|
26
|
-
# into the connection goes through connection_pool, so refusing here
|
|
27
|
-
# fails closed for reads and writes alike.
|
|
28
|
-
def self.connection_pool
|
|
29
|
-
raise Railwatch::DatabaseNotConfigured,
|
|
30
|
-
"the `railwatch_telemetry` database (everything the app reports) is not configured for the " \
|
|
31
|
-
"#{Rails.env} environment; run `bin/rails generate railwatch:install` " \
|
|
32
|
-
"or add it to config/database.yml (docs/embedded.md)"
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# SQLite's auto_vacuum modes. Deleted pages only leave the file in
|
|
37
|
-
# :incremental, and only when PRAGMA incremental_vacuum asks for them;
|
|
38
|
-
# :none (the default, and what every database created before
|
|
39
|
-
# EnableIncrementalVacuum is in) keeps them on the freelist forever.
|
|
40
|
-
AUTO_VACUUM_MODES = { 0 => :none, 1 => :full, 2 => :incremental }.freeze
|
|
41
|
-
|
|
42
|
-
# Everything below reads and writes THIS database, never the host's. The
|
|
43
|
-
# pragmas are per-database and there is no Active Record wrapper for them,
|
|
44
|
-
# so they go through this class's own connection on purpose: through
|
|
45
|
-
# ActiveRecord::Base they would report on, and vacuum, the application's
|
|
46
|
-
# primary database instead.
|
|
47
|
-
def self.sqlite? = connection.adapter_name.match?(/sqlite/i)
|
|
48
|
-
|
|
49
|
-
def self.auto_vacuum_mode = sqlite? ? AUTO_VACUUM_MODES.fetch(connection.select_value("PRAGMA auto_vacuum").to_i, :unknown) : nil
|
|
50
|
-
|
|
51
|
-
def self.freelist_pages = sqlite? ? connection.select_value("PRAGMA freelist_count").to_i : 0
|
|
52
|
-
|
|
53
|
-
def self.page_size = sqlite? ? connection.select_value("PRAGMA page_size").to_i : 0
|
|
54
|
-
|
|
55
|
-
# Hands freelist pages back to the filesystem, a slice at a time. Each
|
|
56
|
-
# PRAGMA is its own implicit transaction, so the write lock is taken and
|
|
57
|
-
# released once per slice rather than held for the whole reclaim -- the
|
|
58
|
-
# same reason PruneTelemetryJob deletes in bounded batches instead of one
|
|
59
|
-
# statement. Returns the pages actually reclaimed, which is 0 on a
|
|
60
|
-
# database that is not in incremental mode: there the pragma is accepted
|
|
61
|
-
# and does nothing, and calling it would look like work that happened.
|
|
62
|
-
def self.reclaim_freelist!(slice:, slices:)
|
|
63
|
-
return 0 unless sqlite? && auto_vacuum_mode == :incremental
|
|
64
|
-
|
|
65
|
-
before = freelist_pages
|
|
66
|
-
slices.times do
|
|
67
|
-
break if freelist_pages.zero?
|
|
68
|
-
|
|
69
|
-
incremental_vacuum(slice.to_i)
|
|
70
|
-
end
|
|
71
|
-
before - freelist_pages
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Through the raw connection on purpose. incremental_vacuum frees one page
|
|
75
|
-
# per sqlite3_step, and Active Record's execute steps a statement with no
|
|
76
|
-
# result columns exactly once -- so through it this pragma frees a single
|
|
77
|
-
# page whatever count it is handed, which is the kind of no-op that reads
|
|
78
|
-
# as work done. The sqlite3 gem's own execute steps to completion. Same
|
|
79
|
-
# raw_connection route Ingest::Writer already takes for its inserts.
|
|
80
|
-
def self.incremental_vacuum(pages = nil)
|
|
81
|
-
connection.raw_connection.execute("PRAGMA incremental_vacuum#{"(#{pages})" if pages}")
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Records arrive as the gem's wire hashes; this is the shared envelope.
|
|
85
|
-
def self.envelope_columns(t)
|
|
86
|
-
t.datetime :occurred_at, null: false, precision: 6
|
|
87
|
-
t.string :deploy, limit: 128
|
|
88
|
-
t.string :server, limit: 255
|
|
89
|
-
t.string :group_hash, limit: 32
|
|
90
|
-
t.string :trace_id, limit: 36
|
|
91
|
-
t.string :execution_source, limit: 20
|
|
92
|
-
t.string :execution_id, limit: 36
|
|
93
|
-
t.string :execution_preview, limit: 255
|
|
94
|
-
t.string :execution_stage, limit: 32
|
|
95
|
-
t.string :user_ref, limit: 255
|
|
96
|
-
t.string :app_tenant, limit: 255
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Platform parity: callers wrap telemetry work in with_tenant. There is one
|
|
100
|
-
# tenant here, so it is just the block.
|
|
101
|
-
def self.with_tenant(_slug)
|
|
102
|
-
yield
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def self.tenant_exist?(_slug) = true
|
|
106
|
-
end
|
|
107
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# A performance rule: "requests to * must keep p95 under 2000 ms over 5
|
|
5
|
-
# minutes". Evaluated by DetectPerformanceIssuesJob against rollups.
|
|
6
|
-
class Threshold < ApplicationRecord
|
|
7
|
-
self.table_name = "railwatch_thresholds"
|
|
8
|
-
MAX_LIMIT = 1_000_000_000
|
|
9
|
-
MAX_WINDOW_MINUTES = 1_440
|
|
10
|
-
TARGET_KINDS = %w[requests jobs commands queries scheduled_tasks outgoing_requests
|
|
11
|
-
llm_calls llm_tools].freeze
|
|
12
|
-
METRICS = %w[p95 max avg error_rate failure_rate missed spend tokens truncation_rate].freeze
|
|
13
|
-
|
|
14
|
-
# Duration and rate metrics read the same on any timed record, so they are
|
|
15
|
-
# allowed everywhere. Money and tokens only mean something where a record
|
|
16
|
-
# carries them: a spend rule on tool calls could never fire, and a rule
|
|
17
|
-
# that cannot fire is worse than no rule -- it reads as coverage.
|
|
18
|
-
SHARED_METRICS = %w[p95 max avg error_rate failure_rate missed].freeze
|
|
19
|
-
LLM_METRICS = %w[spend tokens truncation_rate].freeze
|
|
20
|
-
|
|
21
|
-
# Money leads with its symbol; milliseconds and percent follow the number.
|
|
22
|
-
# The old code assumed two units and branched on the metric name ending in
|
|
23
|
-
# "rate", which cannot express a dollar sign in front.
|
|
24
|
-
UNITS = { "spend" => "$", "tokens" => "", "truncation_rate" => "%",
|
|
25
|
-
"error_rate" => "%", "failure_rate" => "%" }.freeze
|
|
26
|
-
|
|
27
|
-
def environment = Environment.current
|
|
28
|
-
|
|
29
|
-
validates :target_kind, inclusion: { in: TARGET_KINDS }
|
|
30
|
-
validates :metric, inclusion: { in: METRICS }
|
|
31
|
-
validates :limit, numericality: { greater_than: 0, less_than_or_equal_to: MAX_LIMIT }
|
|
32
|
-
validates :window_minutes, numericality: { only_integer: true, greater_than: 0,
|
|
33
|
-
less_than_or_equal_to: MAX_WINDOW_MINUTES }
|
|
34
|
-
validates :target, presence: true, length: { maximum: 256 }
|
|
35
|
-
validates :limit, numericality: { less_than_or_equal_to: 100 }, if: -> { metric&.end_with?("rate") }
|
|
36
|
-
validates :target, uniqueness: { scope: %i[environment_id target_kind metric] }
|
|
37
|
-
validate :metric_applies_to_target_kind
|
|
38
|
-
|
|
39
|
-
def self.metrics_for(target_kind)
|
|
40
|
-
target_kind == "llm_calls" ? SHARED_METRICS + LLM_METRICS : SHARED_METRICS
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def unit = UNITS.fetch(metric, "ms")
|
|
44
|
-
|
|
45
|
-
# "$4.50", "2000ms", "5%", "150000" -- the symbol's position is part of
|
|
46
|
-
# the unit, not something a caller should have to know.
|
|
47
|
-
def format_value(value)
|
|
48
|
-
return "$#{value.is_a?(Float) ? format('%.2f', value) : value}" if unit == "$"
|
|
49
|
-
|
|
50
|
-
"#{value}#{unit}"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def description
|
|
54
|
-
"#{target_kind} #{target == '*' ? 'all' : target}: #{metric} over #{format_value(limit)} in #{window_minutes}m"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def metric_applies_to_target_kind
|
|
60
|
-
return if metric.blank? || target_kind.blank?
|
|
61
|
-
return if Threshold.metrics_for(target_kind).include?(metric)
|
|
62
|
-
|
|
63
|
-
errors.add(:metric, "is not available for #{target_kind}")
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Who is looking at the dashboard. The platform has accounts and memberships;
|
|
5
|
-
# an embedded install sits behind the host's own authentication, so this is
|
|
6
|
-
# whatever the host says it is (Railwatch.config.dashboard_user), defaulting
|
|
7
|
-
# to a single anonymous operator. Comments, saved views and issue activity
|
|
8
|
-
# record this id.
|
|
9
|
-
class User
|
|
10
|
-
ID = 1
|
|
11
|
-
|
|
12
|
-
attr_reader :id, :name, :email
|
|
13
|
-
|
|
14
|
-
def self.current = Viewer.user
|
|
15
|
-
def self.find(id) = find_by(id: id) || raise(ActiveRecord::RecordNotFound, "User #{id}")
|
|
16
|
-
|
|
17
|
-
# A host's dashboard_user resolver hands back its own ids, and those ids
|
|
18
|
-
# are what comments, saved views and issue activity store. Resolving only
|
|
19
|
-
# ID meant a comment written by user 7 had no author, and assigning an
|
|
20
|
-
# issue to them raised RecordNotFound. There is no user table to look
|
|
21
|
-
# anyone up in, so: the viewer when it is them, the anonymous operator
|
|
22
|
-
# for the default id, and a bare identity carrying the stored id
|
|
23
|
-
# otherwise, which is everything that is actually known about them.
|
|
24
|
-
def self.find_by(id:)
|
|
25
|
-
return nil if id.nil? || id.to_s.empty?
|
|
26
|
-
viewer = Viewer.user
|
|
27
|
-
return viewer if viewer && viewer.id.to_s == id.to_s
|
|
28
|
-
return default if id.to_i == ID
|
|
29
|
-
|
|
30
|
-
new(id: id, name: "Operator", email: nil)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def self.default = @default ||= new(id: ID, name: "Operator", email: nil)
|
|
34
|
-
|
|
35
|
-
def initialize(id:, name:, email: nil)
|
|
36
|
-
@id = id
|
|
37
|
-
@name = name
|
|
38
|
-
@email = email
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def as_json(*) = { id: id, name: name, email: email, provider: nil, verified: true, editor: "vscode", editor_root: nil }
|
|
42
|
-
def accounts = [ Railwatch::Embedded::Account ]
|
|
43
|
-
def ==(other) = other.is_a?(User) && other.id == id
|
|
44
|
-
alias eql? ==
|
|
45
|
-
def hash = id.hash
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Who is looking at the dashboard, request-scoped. The platform's code
|
|
5
|
-
# reads Current.user; the gem's own Railwatch::Current is the running
|
|
6
|
-
# execution, so this has a different name. Set from the host's
|
|
7
|
-
# dashboard_user resolver on every dashboard request.
|
|
8
|
-
class Viewer < ActiveSupport::CurrentAttributes
|
|
9
|
-
attribute :user, :environment, :account
|
|
10
|
-
|
|
11
|
-
def account = Railwatch::Embedded::Account
|
|
12
|
-
end
|
|
13
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# The time range a telemetry page, API call, or MCP tool looks at: one of the
|
|
5
|
-
# fixed presets ("1h" .. "30d") ending now, or a custom [from, to] pair. The
|
|
6
|
-
# dashboard, the JSON API, and MCP all resolve their ?window= / "window"
|
|
7
|
-
# argument through here so the presets and the default agree everywhere.
|
|
8
|
-
class Window
|
|
9
|
-
PRESETS = { "1h" => 1.hour, "6h" => 6.hours, "24h" => 24.hours, "7d" => 7.days, "30d" => 30.days }.freeze
|
|
10
|
-
DEFAULT = "24h"
|
|
11
|
-
MAX_CUSTOM_RANGE = 90.days
|
|
12
|
-
|
|
13
|
-
attr_reader :key, :from, :to
|
|
14
|
-
|
|
15
|
-
# A preset key, or anything else for the default.
|
|
16
|
-
def self.preset(key)
|
|
17
|
-
key = PRESETS.key?(key.to_s) ? key.to_s : DEFAULT
|
|
18
|
-
to = Time.current
|
|
19
|
-
new(key, to - PRESETS[key], to)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# A custom range from ?from=&to= when both parse, are in order, and span
|
|
23
|
-
# no more than MAX_CUSTOM_RANGE; otherwise the preset (or default).
|
|
24
|
-
def self.parse(window: nil, from: nil, to: nil)
|
|
25
|
-
custom(from, to) || preset(window)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.custom(from, to)
|
|
29
|
-
from = parse_time(from) or return
|
|
30
|
-
to = to.blank? ? Time.current : parse_time(to) or return
|
|
31
|
-
new("custom", from, to) if to > from && (to - from) <= MAX_CUSTOM_RANGE
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.parse_time(value)
|
|
35
|
-
return if value.blank?
|
|
36
|
-
Time.zone.parse(value.to_s)
|
|
37
|
-
rescue ArgumentError, TypeError
|
|
38
|
-
nil
|
|
39
|
-
end
|
|
40
|
-
private_class_method :parse_time
|
|
41
|
-
|
|
42
|
-
def initialize(key, from, to)
|
|
43
|
-
@key = key
|
|
44
|
-
@from = from
|
|
45
|
-
@to = to
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def custom? = key == "custom"
|
|
49
|
-
def range = [ from, to ]
|
|
50
|
-
def span = to - from
|
|
51
|
-
|
|
52
|
-
# The same length immediately before this one, for "vs. previous period".
|
|
53
|
-
def previous
|
|
54
|
-
self.class.new(key, from - span, from)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def to_h
|
|
58
|
-
{ from: from.iso8601(6), to: to.iso8601(6) }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Query params that reproduce this window on another page.
|
|
62
|
-
def to_params
|
|
63
|
-
custom? ? to_h : { window: key }
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<title inertia><%= content_for(:title) || "Railwatch" %></title>
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<meta name="application-name" content="Railwatch">
|
|
7
|
-
<meta name="railwatch-mount" content="<%= request.script_name %>">
|
|
8
|
-
<link rel="icon" href="/railwatch/icon.svg" type="image/svg+xml">
|
|
9
|
-
<link rel="apple-touch-icon" href="/railwatch/icon.png">
|
|
10
|
-
<%= csrf_meta_tags %>
|
|
11
|
-
<%= csp_meta_tag %>
|
|
12
|
-
<script nonce="<%= content_security_policy_nonce %>">
|
|
13
|
-
document.documentElement.classList.toggle(
|
|
14
|
-
"dark",
|
|
15
|
-
localStorage.appearance === "dark" ||
|
|
16
|
-
(!("appearance" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
|
|
17
|
-
);
|
|
18
|
-
</script>
|
|
19
|
-
<%= railwatch_font_preloads %>
|
|
20
|
-
<%= railwatch_asset_tags(nonce: content_security_policy_nonce) %>
|
|
21
|
-
</head>
|
|
22
|
-
<body>
|
|
23
|
-
<%= yield %>
|
|
24
|
-
</body>
|
|
25
|
-
</html>
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# The engine's own record of the monitored application: issues and what
|
|
4
|
-
# people did about them, saved views, thresholds, anomaly and alert rules,
|
|
5
|
-
# deploy markers. Authored and permanent where telemetry is derived and
|
|
6
|
-
# pruned, so it lives in the host's `railwatch` database, never its primary.
|
|
7
|
-
#
|
|
8
|
-
# Nothing here references the host's tables. `viewer_id` on comments,
|
|
9
|
-
# activity and saved views is whatever the host's dashboard_user resolver
|
|
10
|
-
# returned as `id`, stored as an integer and never joined; `assignee_id`
|
|
11
|
-
# on issues is the same. application_id and environment_id are always 1 in
|
|
12
|
-
# an embedded install and exist so the dashboard's URLs and the hosted
|
|
13
|
-
# platform's code stay identical.
|
|
14
|
-
class CreateRailwatchTables < ActiveRecord::Migration[8.1]
|
|
15
|
-
def change
|
|
16
|
-
create_table :railwatch_issues do |t|
|
|
17
|
-
t.integer :application_id, null: false
|
|
18
|
-
t.integer :environment_id, null: false
|
|
19
|
-
t.integer :number, null: false
|
|
20
|
-
t.string :group_hash, limit: 32, null: false
|
|
21
|
-
t.string :kind, default: "exception", null: false
|
|
22
|
-
t.string :title, null: false
|
|
23
|
-
t.string :culprit
|
|
24
|
-
t.string :source, limit: 128
|
|
25
|
-
t.string :status, default: "open", null: false
|
|
26
|
-
t.string :priority, default: "normal", null: false
|
|
27
|
-
t.integer :assignee_id
|
|
28
|
-
t.integer :merged_into_id
|
|
29
|
-
t.bigint :occurrences, default: 0, null: false
|
|
30
|
-
t.integer :affected_users, default: 0, null: false
|
|
31
|
-
t.datetime :first_seen_at, null: false
|
|
32
|
-
t.datetime :last_seen_at, null: false
|
|
33
|
-
t.datetime :resolved_at
|
|
34
|
-
t.string :resolved_in_deploy, limit: 128
|
|
35
|
-
t.datetime :regressed_at
|
|
36
|
-
t.json :sample, default: {}, null: false
|
|
37
|
-
t.timestamps
|
|
38
|
-
end
|
|
39
|
-
add_index :railwatch_issues, [ :application_id, :number ], unique: true
|
|
40
|
-
add_index :railwatch_issues, [ :environment_id, :group_hash ], unique: true
|
|
41
|
-
add_index :railwatch_issues, [ :environment_id, :status, :last_seen_at ]
|
|
42
|
-
add_index :railwatch_issues, :merged_into_id
|
|
43
|
-
|
|
44
|
-
create_table :railwatch_comments do |t|
|
|
45
|
-
t.integer :issue_id, null: false
|
|
46
|
-
t.integer :viewer_id
|
|
47
|
-
t.string :author_name
|
|
48
|
-
t.text :body, null: false
|
|
49
|
-
t.string :source, default: "railwatch", null: false
|
|
50
|
-
t.string :external_id
|
|
51
|
-
t.datetime :external_updated_at
|
|
52
|
-
t.timestamps
|
|
53
|
-
end
|
|
54
|
-
add_index :railwatch_comments, :issue_id
|
|
55
|
-
|
|
56
|
-
create_table :railwatch_issue_activities do |t|
|
|
57
|
-
t.integer :issue_id, null: false
|
|
58
|
-
t.integer :viewer_id
|
|
59
|
-
t.string :kind, null: false
|
|
60
|
-
t.json :data, default: {}, null: false
|
|
61
|
-
t.datetime :created_at, null: false
|
|
62
|
-
end
|
|
63
|
-
add_index :railwatch_issue_activities, [ :issue_id, :created_at ]
|
|
64
|
-
|
|
65
|
-
create_table :railwatch_deploys do |t|
|
|
66
|
-
t.integer :environment_id, null: false
|
|
67
|
-
t.string :deploy, limit: 128, null: false
|
|
68
|
-
t.string :ref, limit: 128
|
|
69
|
-
t.string :previous_ref, limit: 128
|
|
70
|
-
t.string :name
|
|
71
|
-
t.string :url
|
|
72
|
-
t.string :server
|
|
73
|
-
t.datetime :deployed_at, null: false
|
|
74
|
-
t.json :commits, default: [], null: false
|
|
75
|
-
t.json :detail, default: {}, null: false
|
|
76
|
-
t.timestamps
|
|
77
|
-
end
|
|
78
|
-
add_index :railwatch_deploys, [ :environment_id, :deploy ], unique: true
|
|
79
|
-
add_index :railwatch_deploys, [ :environment_id, :deployed_at ]
|
|
80
|
-
|
|
81
|
-
create_table :railwatch_saved_views do |t|
|
|
82
|
-
t.integer :environment_id, null: false
|
|
83
|
-
t.integer :viewer_id, null: false
|
|
84
|
-
t.string :name, null: false
|
|
85
|
-
t.string :page, null: false
|
|
86
|
-
t.string :query
|
|
87
|
-
t.string :window, limit: 8
|
|
88
|
-
t.json :params, default: {}, null: false
|
|
89
|
-
t.boolean :pinned, default: true, null: false
|
|
90
|
-
t.boolean :shared, default: true, null: false
|
|
91
|
-
t.timestamps
|
|
92
|
-
end
|
|
93
|
-
add_index :railwatch_saved_views, [ :environment_id, :page ]
|
|
94
|
-
|
|
95
|
-
create_table :railwatch_thresholds do |t|
|
|
96
|
-
t.integer :environment_id, null: false
|
|
97
|
-
t.string :target_kind, null: false
|
|
98
|
-
t.string :target, default: "*", null: false
|
|
99
|
-
t.string :metric, default: "p95", null: false
|
|
100
|
-
t.float :limit, null: false
|
|
101
|
-
t.integer :window_minutes, default: 5, null: false
|
|
102
|
-
t.timestamps
|
|
103
|
-
end
|
|
104
|
-
add_index :railwatch_thresholds, [ :environment_id, :target_kind, :target, :metric ], unique: true, name: "index_railwatch_thresholds_unique"
|
|
105
|
-
|
|
106
|
-
create_table :railwatch_anomaly_rules do |t|
|
|
107
|
-
t.integer :environment_id, null: false
|
|
108
|
-
t.string :target_kind, null: false
|
|
109
|
-
t.string :target, default: "*", null: false
|
|
110
|
-
t.string :metric, null: false
|
|
111
|
-
t.float :deviation, default: 3.0, null: false
|
|
112
|
-
t.integer :baseline_days, default: 7, null: false
|
|
113
|
-
t.integer :window_minutes, default: 15, null: false
|
|
114
|
-
t.boolean :enabled, default: true, null: false
|
|
115
|
-
t.datetime :last_fired_at
|
|
116
|
-
t.timestamps
|
|
117
|
-
end
|
|
118
|
-
add_index :railwatch_anomaly_rules, :environment_id
|
|
119
|
-
|
|
120
|
-
create_table :railwatch_alert_rules do |t|
|
|
121
|
-
t.integer :application_id, null: false
|
|
122
|
-
t.string :event, null: false
|
|
123
|
-
t.json :filters, default: {}, null: false
|
|
124
|
-
t.timestamps
|
|
125
|
-
end
|
|
126
|
-
add_index :railwatch_alert_rules, :application_id
|
|
127
|
-
|
|
128
|
-
create_table :railwatch_alerts do |t|
|
|
129
|
-
t.integer :alert_rule_id
|
|
130
|
-
t.integer :issue_id
|
|
131
|
-
t.integer :summary_alert_id
|
|
132
|
-
t.string :event, null: false
|
|
133
|
-
t.string :status, default: "pending", null: false
|
|
134
|
-
t.json :payload, default: {}, null: false
|
|
135
|
-
t.text :error
|
|
136
|
-
t.datetime :sent_at
|
|
137
|
-
t.string :burst_key
|
|
138
|
-
t.string :delivery_key
|
|
139
|
-
t.integer :delivery_attempts, default: 0, null: false
|
|
140
|
-
t.datetime :next_delivery_at
|
|
141
|
-
t.datetime :delivery_enqueued_until
|
|
142
|
-
t.string :delivery_lease_id
|
|
143
|
-
t.datetime :delivery_lease_expires_at
|
|
144
|
-
t.timestamps
|
|
145
|
-
end
|
|
146
|
-
add_index :railwatch_alerts, :burst_key, unique: true
|
|
147
|
-
add_index :railwatch_alerts, :delivery_key, unique: true
|
|
148
|
-
add_index :railwatch_alerts, [ :issue_id, :event, :created_at ]
|
|
149
|
-
add_index :railwatch_alerts, [ :status, :created_at, :alert_rule_id ], name: "index_railwatch_alerts_for_burst_reconciliation"
|
|
150
|
-
end
|
|
151
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# The embedded install's maintenance lease table: one row per task in
|
|
4
|
-
# Railwatch::Maintenance::TASKS, recording when it last ran and which process
|
|
5
|
-
# holds it now. Lives in the railwatch database with the other permanent,
|
|
6
|
-
# operator-facing state rather than in the pruned telemetry file.
|
|
7
|
-
class CreateRailwatchMaintenanceTasks < ActiveRecord::Migration[8.1]
|
|
8
|
-
def change
|
|
9
|
-
create_table :railwatch_maintenance_tasks do |t|
|
|
10
|
-
t.string :name, null: false
|
|
11
|
-
t.datetime :last_run_at
|
|
12
|
-
t.string :lease_owner
|
|
13
|
-
t.datetime :lease_expires_at
|
|
14
|
-
t.timestamps
|
|
15
|
-
end
|
|
16
|
-
add_index :railwatch_maintenance_tasks, :name, unique: true
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# What makes replaying a batch's follow-ups safe. Exception grouping bumps an
|
|
4
|
-
# issue's occurrence count in this database, but the "done" mark lives on the
|
|
5
|
-
# batch's ledger row in the OTHER database, so a crash between the two (or
|
|
6
|
-
# the inline drain racing the maintenance drain) would count the same
|
|
7
|
-
# exceptions twice. A receipt per (batch, group) is inserted in the same
|
|
8
|
-
# transaction as the issue update; the unique index turns a second attempt
|
|
9
|
-
# into a no-op.
|
|
10
|
-
class CreateRailwatchFollowupReceipts < ActiveRecord::Migration[8.1]
|
|
11
|
-
def change
|
|
12
|
-
create_table :railwatch_followup_receipts do |t|
|
|
13
|
-
t.string :batch_id, limit: 36, null: false
|
|
14
|
-
t.string :group_hash, limit: 32, null: false
|
|
15
|
-
t.datetime :created_at, null: false
|
|
16
|
-
end
|
|
17
|
-
add_index :railwatch_followup_receipts, [ :batch_id, :group_hash ], unique: true
|
|
18
|
-
add_index :railwatch_followup_receipts, :created_at
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# The ids on comments, issue activity, saved views and issue assignment are
|
|
4
|
-
# whatever the host's `dashboard_user` resolver returned. They were integer
|
|
5
|
-
# columns, which quietly assumed every application numbers its users. An app
|
|
6
|
-
# with UUID primary keys could not store a comment at all, and one with ids
|
|
7
|
-
# past the signed range could not either.
|
|
8
|
-
#
|
|
9
|
-
# Nothing joins on these columns and nothing indexes them; they are an opaque
|
|
10
|
-
# handle the host gave us and hands back. A string says that, and costs
|
|
11
|
-
# nothing in a database that is a few thousand rows of authored content.
|
|
12
|
-
#
|
|
13
|
-
# Existing values convert cleanly: SQLite rewrites 7 as "7", and the models
|
|
14
|
-
# compare with to_s on both sides, so a row written before this migration
|
|
15
|
-
# still matches the same viewer after it.
|
|
16
|
-
class WidenHostUserIds < ActiveRecord::Migration[8.1]
|
|
17
|
-
COLUMNS = {
|
|
18
|
-
railwatch_issues: :assignee_id,
|
|
19
|
-
railwatch_comments: :viewer_id,
|
|
20
|
-
railwatch_issue_activities: :viewer_id,
|
|
21
|
-
railwatch_saved_views: :viewer_id
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
|
-
# SQLite rebuilds a table to change a column type, and a rebuild inside a
|
|
25
|
-
# transaction ignores `PRAGMA foreign_keys = OFF`. This database declares no
|
|
26
|
-
# foreign keys, so there is nothing to cascade, but the rebuild is kept out
|
|
27
|
-
# of the migrator's transaction anyway: it is the cheap half of a habit
|
|
28
|
-
# whose expensive half is deleted rows.
|
|
29
|
-
disable_ddl_transaction!
|
|
30
|
-
|
|
31
|
-
def up
|
|
32
|
-
COLUMNS.each do |table, column|
|
|
33
|
-
null = column == :viewer_id && table == :railwatch_saved_views ? false : true
|
|
34
|
-
change_column table, column, :string, limit: 255, null: null
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Reversible only while every stored id still looks like a number. Once a
|
|
39
|
-
# host with UUIDs (or emails, or anything else) has written one, there is
|
|
40
|
-
# no integer to go back to: the column would either refuse the value or
|
|
41
|
-
# quietly coerce it to something that is no longer that person. Say so and
|
|
42
|
-
# stop, rather than losing the identity on the way down. These tables are
|
|
43
|
-
# authored content, a few thousand rows at most, so reading them is cheap.
|
|
44
|
-
INTEGERISH = /\A-?\d+\z/
|
|
45
|
-
MAX_REPORTED = 3
|
|
46
|
-
|
|
47
|
-
def down
|
|
48
|
-
blocking = COLUMNS.filter_map do |table, column|
|
|
49
|
-
next unless connection.table_exists?(table)
|
|
50
|
-
|
|
51
|
-
values = connection.select_values(
|
|
52
|
-
"SELECT DISTINCT #{connection.quote_column_name(column)} FROM #{connection.quote_table_name(table)} " \
|
|
53
|
-
"WHERE #{connection.quote_column_name(column)} IS NOT NULL"
|
|
54
|
-
)
|
|
55
|
-
offenders = values.reject { |value| INTEGERISH.match?(value.to_s) }
|
|
56
|
-
"#{table}.#{column} (#{offenders.first(MAX_REPORTED).join(', ')}#{"..." if offenders.size > MAX_REPORTED})" if offenders.any?
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
if blocking.any?
|
|
60
|
-
raise ActiveRecord::IrreversibleMigration,
|
|
61
|
-
"cannot narrow host user ids back to integers: #{blocking.join('; ')}. " \
|
|
62
|
-
"Those ids came from this application's own dashboard_user resolver and have no integer form; " \
|
|
63
|
-
"rolling back would discard them."
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
COLUMNS.each do |table, column|
|
|
67
|
-
null = column == :viewer_id && table == :railwatch_saved_views ? false : true
|
|
68
|
-
change_column table, column, :integer, null: null
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|