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,309 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Builds the same discriminated performance/anomaly detail contract for the
|
|
5
|
-
# Inertia page, REST API, and MCP. The durable detector snapshot remains useful
|
|
6
|
-
# after raw data is pruned; live rollups add trend and deploy comparison when
|
|
7
|
-
# they are still available.
|
|
8
|
-
class IssueDetectionPresenter
|
|
9
|
-
TREND_DAYS = 30
|
|
10
|
-
DEPLOY_LIMIT = 20
|
|
11
|
-
EXECUTION_TYPES = IssueDetectionSnapshot::EXECUTION_TYPES
|
|
12
|
-
SCHEMA_VERSION = 1
|
|
13
|
-
NUMERIC_FIELDS = {
|
|
14
|
-
"measurement" => %w[value limit],
|
|
15
|
-
"baseline" => %w[mean stddev sigmas days deviation]
|
|
16
|
-
}.freeze
|
|
17
|
-
RULE_STRING_FIELDS = %w[type target_kind target task_key schedule last_run expected].freeze
|
|
18
|
-
EXECUTION_SOURCES = %w[request job job_attempt scheduled_task command].freeze
|
|
19
|
-
CHILD_TYPES = %w[query outgoing_request].freeze
|
|
20
|
-
TARGET_KIND_FOR = {
|
|
21
|
-
"request" => "requests", "job_attempt" => "jobs", "command" => "commands",
|
|
22
|
-
"query" => "queries", "scheduled_task" => "scheduled_tasks",
|
|
23
|
-
"outgoing_request" => "outgoing_requests"
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
|
-
def initialize(issue)
|
|
27
|
-
@issue = issue
|
|
28
|
-
@details = issue.sample["detection"]
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def as_json
|
|
32
|
-
return unless @issue.kind.in?(%w[performance anomaly])
|
|
33
|
-
return unavailable_json unless @details.is_a?(Hash)
|
|
34
|
-
|
|
35
|
-
validate_details!
|
|
36
|
-
live = @issue.environment.with_telemetry { live_details }
|
|
37
|
-
@details.deep_symbolize_keys.merge(
|
|
38
|
-
available: true,
|
|
39
|
-
count_label: "Breached evaluation windows",
|
|
40
|
-
breached_windows: @issue.occurrences,
|
|
41
|
-
event_count: live[:event_count],
|
|
42
|
-
trend: live[:trend],
|
|
43
|
-
deploy_comparison: live[:deploy_comparison],
|
|
44
|
-
representative_references: representative_references,
|
|
45
|
-
representative_records: representatives
|
|
46
|
-
)
|
|
47
|
-
rescue KeyError, ArgumentError, TypeError
|
|
48
|
-
unavailable_json
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def unavailable_json
|
|
54
|
-
{
|
|
55
|
-
kind: @issue.kind, available: false,
|
|
56
|
-
count_label: "Breached evaluation windows", breached_windows: @issue.occurrences,
|
|
57
|
-
message: "Typed detector context was not captured for this historical issue."
|
|
58
|
-
}
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def validate_details!
|
|
62
|
-
raise ArgumentError unless @details.fetch("schema_version") == SCHEMA_VERSION
|
|
63
|
-
raise ArgumentError unless @details.fetch("kind") == @issue.kind
|
|
64
|
-
telemetry_type = @details.fetch("telemetry_type")
|
|
65
|
-
raise ArgumentError unless telemetry_type.in?(EXECUTION_TYPES + %w[query outgoing_request])
|
|
66
|
-
target_kind = TARGET_KIND_FOR.fetch(telemetry_type)
|
|
67
|
-
allowed_target_kinds = @issue.kind == "anomaly" ? AnomalyRule::TARGET_KINDS : Threshold::TARGET_KINDS
|
|
68
|
-
raise ArgumentError unless target_kind.in?(allowed_target_kinds)
|
|
69
|
-
%w[telemetry_group_hash target metric unit].each do |key|
|
|
70
|
-
raise ArgumentError unless @details.fetch(key).is_a?(String) && @details.fetch(key).present?
|
|
71
|
-
end
|
|
72
|
-
metric = @details.fetch("metric")
|
|
73
|
-
allowed_metrics = @issue.kind == "anomaly" ? AnomalyRule::METRICS : Threshold::METRICS
|
|
74
|
-
raise ArgumentError unless metric.in?(allowed_metrics)
|
|
75
|
-
raise ArgumentError if metric == "missed" && @details.fetch("telemetry_type") != "scheduled_task"
|
|
76
|
-
expected_unit = IssueDetectionSnapshot::UNITS[metric]
|
|
77
|
-
raise ArgumentError unless expected_unit && @details.fetch("unit") == expected_unit
|
|
78
|
-
validate_window!
|
|
79
|
-
NUMERIC_FIELDS.each { |key, fields| validate_numeric_object!(key, fields) }
|
|
80
|
-
validate_rule!
|
|
81
|
-
validate_detector_shape!
|
|
82
|
-
validate_representatives!
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def validate_window!
|
|
86
|
-
window = @details.fetch("window")
|
|
87
|
-
raise ArgumentError unless window.is_a?(Hash)
|
|
88
|
-
from = Time.iso8601(window.fetch("from"))
|
|
89
|
-
to = Time.iso8601(window.fetch("to"))
|
|
90
|
-
minutes = window.fetch("minutes")
|
|
91
|
-
raise ArgumentError unless finite_number?(minutes) && minutes.positive? && to > from
|
|
92
|
-
raise ArgumentError unless (minutes - ((to - from) / 60.0).round(2)).abs <= 0.01
|
|
93
|
-
return if @details.fetch("metric") == "missed"
|
|
94
|
-
|
|
95
|
-
raise ArgumentError unless minutes == minutes.to_i
|
|
96
|
-
maximum = @issue.kind == "anomaly" ? AnomalyRule::MAX_WINDOW_MINUTES : Threshold::MAX_WINDOW_MINUTES
|
|
97
|
-
raise ArgumentError if minutes > maximum
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def validate_numeric_object!(key, fields)
|
|
101
|
-
value = @details[key]
|
|
102
|
-
return if value.nil?
|
|
103
|
-
raise ArgumentError unless value.is_a?(Hash)
|
|
104
|
-
fields.each { |field| raise ArgumentError if value.key?(field) && !finite_number?(value[field]) }
|
|
105
|
-
if key == "measurement"
|
|
106
|
-
%w[value limit].each { |field| raise ArgumentError if value.key?(field) && value[field].negative? }
|
|
107
|
-
raise ArgumentError if value.key?("limit") && !value["limit"].positive?
|
|
108
|
-
else
|
|
109
|
-
%w[mean stddev].each { |field| raise ArgumentError if value.key?(field) && value[field].negative? }
|
|
110
|
-
raise ArgumentError if value.key?("days") && (!value["days"].is_a?(Integer) || !value["days"].positive?)
|
|
111
|
-
raise ArgumentError if value.key?("deviation") && !value["deviation"].positive?
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def validate_rule!
|
|
116
|
-
rule = @details["rule"]
|
|
117
|
-
if rule.nil?
|
|
118
|
-
raise ArgumentError if @details.fetch("metric") == "missed"
|
|
119
|
-
return
|
|
120
|
-
end
|
|
121
|
-
raise ArgumentError unless rule.is_a?(Hash)
|
|
122
|
-
RULE_STRING_FIELDS.each do |field|
|
|
123
|
-
raise ArgumentError if rule.key?(field) && (!rule[field].is_a?(String) || rule[field].blank?)
|
|
124
|
-
end
|
|
125
|
-
raise ArgumentError if rule.key?("id") && (!rule["id"].is_a?(Integer) || !rule["id"].positive?)
|
|
126
|
-
%w[last_run expected].each { |field| Time.iso8601(rule[field]) if rule.key?(field) }
|
|
127
|
-
return unless @details.fetch("metric") == "missed"
|
|
128
|
-
|
|
129
|
-
raise ArgumentError unless rule["type"] == "schedule"
|
|
130
|
-
%w[task_key schedule last_run expected].each { |field| raise ArgumentError unless rule[field].is_a?(String) && rule[field].present? }
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def validate_detector_shape!
|
|
134
|
-
if @issue.kind == "anomaly"
|
|
135
|
-
require_numeric_fields!("measurement", %w[value])
|
|
136
|
-
baseline = require_numeric_fields!("baseline", %w[mean stddev sigmas days deviation])
|
|
137
|
-
raise ArgumentError unless baseline.fetch("sigmas").positive?
|
|
138
|
-
raise ArgumentError unless baseline.fetch("days").between?(
|
|
139
|
-
AnomalyRule::MIN_BASELINE_DAYS, AnomalyRule::MAX_BASELINE_DAYS
|
|
140
|
-
)
|
|
141
|
-
raise ArgumentError if baseline.fetch("deviation") > AnomalyRule::MAX_DEVIATION
|
|
142
|
-
require_detector_rule!("anomaly")
|
|
143
|
-
elsif @details.fetch("metric") != "missed"
|
|
144
|
-
measurement = require_numeric_fields!("measurement", %w[value limit])
|
|
145
|
-
raise ArgumentError if measurement.fetch("limit") > Threshold::MAX_LIMIT
|
|
146
|
-
raise ArgumentError if @details.fetch("metric").end_with?("rate") && measurement.fetch("limit") > 100
|
|
147
|
-
require_detector_rule!("threshold")
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def require_numeric_fields!(key, fields)
|
|
152
|
-
object = @details.fetch(key)
|
|
153
|
-
raise ArgumentError unless object.is_a?(Hash)
|
|
154
|
-
fields.each { |field| raise ArgumentError unless finite_number?(object.fetch(field)) }
|
|
155
|
-
object
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def require_detector_rule!(type)
|
|
159
|
-
rule = @details.fetch("rule")
|
|
160
|
-
raise ArgumentError unless rule.is_a?(Hash) && rule.fetch("type") == type
|
|
161
|
-
raise ArgumentError unless rule.fetch("id").is_a?(Integer) && rule.fetch("id").positive?
|
|
162
|
-
raise ArgumentError unless rule.fetch("target").is_a?(String) && rule.fetch("target").present?
|
|
163
|
-
expected_target_kind = TARGET_KIND_FOR.fetch(@details.fetch("telemetry_type"))
|
|
164
|
-
raise ArgumentError unless rule.fetch("target_kind") == expected_target_kind
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def validate_representatives!
|
|
168
|
-
references = @details.fetch("representative_records", [])
|
|
169
|
-
raise ArgumentError unless references.is_a?(Array) && references.size <= IssueDetectionSnapshot::REPRESENTATIVE_LIMIT
|
|
170
|
-
|
|
171
|
-
references.each do |reference|
|
|
172
|
-
raise ArgumentError unless reference.is_a?(Hash)
|
|
173
|
-
raise ArgumentError unless reference["record_type"] == @details.fetch("telemetry_type")
|
|
174
|
-
raise ArgumentError unless reference["record_id"].is_a?(Integer) && reference["record_id"].positive?
|
|
175
|
-
raise ArgumentError unless reference["group_hash"] == @details.fetch("telemetry_group_hash")
|
|
176
|
-
execution_id = reference["execution_id"]
|
|
177
|
-
raise ArgumentError unless execution_id.nil? || (execution_id.is_a?(String) && execution_id.present?)
|
|
178
|
-
raise ArgumentError if reference["record_type"] != "query" && execution_id.blank?
|
|
179
|
-
source = reference["execution_source"]
|
|
180
|
-
if CHILD_TYPES.include?(reference["record_type"])
|
|
181
|
-
raise ArgumentError unless source.is_a?(String) && source.present? && source.in?(EXECUTION_SOURCES)
|
|
182
|
-
else
|
|
183
|
-
raise ArgumentError unless source == reference["record_type"]
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def finite_number?(value)
|
|
189
|
-
value.is_a?(Numeric) && (!value.respond_to?(:finite?) || value.finite?)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def live_details
|
|
193
|
-
type = @details.fetch("telemetry_type")
|
|
194
|
-
group_hash = @details.fetch("telemetry_group_hash")
|
|
195
|
-
window = @details.fetch("window")
|
|
196
|
-
from = Time.iso8601(window.fetch("from"))
|
|
197
|
-
to = Time.iso8601(window.fetch("to"))
|
|
198
|
-
rollups = Telemetry::Rollup.for_type(type).where(group_hash: group_hash)
|
|
199
|
-
window_summary = Telemetry::Rollup.summarize(rollups.between(from, to))
|
|
200
|
-
now = Time.current
|
|
201
|
-
# Rollup.between includes the whole lower-bound hour, so the denominator
|
|
202
|
-
# for count rates must begin at that same boundary.
|
|
203
|
-
trend_from = (now - TREND_DAYS.days).beginning_of_hour
|
|
204
|
-
trend_rows = rollups.between(trend_from, now).order(:bucket).to_a.group_by { |row| row.bucket.to_date }
|
|
205
|
-
{
|
|
206
|
-
event_count: window_summary[:count],
|
|
207
|
-
trend: trend_rows.map { |day, rows| trend_point(day, rows, trend_from, now) },
|
|
208
|
-
deploy_comparison: deploy_comparison(type, group_hash)
|
|
209
|
-
}
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def trend_point(day, rows, trend_from, now)
|
|
213
|
-
summary = Telemetry::Rollup.summarize(rows)
|
|
214
|
-
day_start = day.in_time_zone.beginning_of_day
|
|
215
|
-
minutes = ([ day_start + 1.day, now ].min - [ day_start, trend_from ].max) / 60.0
|
|
216
|
-
{ day: day.iso8601, value: metric_value(@details.fetch("metric"), summary, minutes: minutes), count: summary[:count] }
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def metric_value(metric, summary, minutes: nil)
|
|
220
|
-
case metric
|
|
221
|
-
when "p95" then milliseconds(summary[:p95])
|
|
222
|
-
when "max" then milliseconds(summary[:max])
|
|
223
|
-
when "avg" then milliseconds(summary[:avg])
|
|
224
|
-
when "error_rate", "failure_rate"
|
|
225
|
-
summary[:count].zero? ? nil : (summary[:errors] * 100.0 / summary[:count]).round(2)
|
|
226
|
-
when "count" then minutes&.positive? ? (summary[:count] / minutes).round(2) : nil
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def deploy_comparison(type, group_hash)
|
|
231
|
-
scope = record_scope(type, group_hash).where(occurred_at: TREND_DAYS.days.ago..).where.not(deploy: nil)
|
|
232
|
-
scope.group(:deploy).pluck(:deploy, Arel.sql("COUNT(*)"), Arel.sql("AVG(duration)"), Arel.sql("MAX(duration)"))
|
|
233
|
-
.map { |deploy, count, avg, max| { deploy: deploy, events: count, avg_ms: milliseconds(avg), max_ms: milliseconds(max) } }
|
|
234
|
-
.sort_by { |row| -row[:events] }.first(DEPLOY_LIMIT)
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def record_scope(type, group_hash)
|
|
238
|
-
if type.in?(EXECUTION_TYPES)
|
|
239
|
-
Telemetry::Execution.of_kind(type).where(group_hash: group_hash)
|
|
240
|
-
elsif type == "query"
|
|
241
|
-
Telemetry::Query.where(group_hash: group_hash).with_sql
|
|
242
|
-
elsif type == "outgoing_request"
|
|
243
|
-
Telemetry::OutgoingRequest.where(group_hash: group_hash)
|
|
244
|
-
else
|
|
245
|
-
raise ArgumentError, "unsupported detection telemetry type: #{type}"
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def representatives
|
|
250
|
-
references = Array(@details["representative_records"]).select { |record| record.is_a?(Hash) }
|
|
251
|
-
return [] if references.empty?
|
|
252
|
-
|
|
253
|
-
type = @details.fetch("telemetry_type")
|
|
254
|
-
group_hash = @details.fetch("telemetry_group_hash")
|
|
255
|
-
records = @issue.environment.with_telemetry do
|
|
256
|
-
record_scope(type, group_hash).where(id: references.filter_map { |record| record["record_id"] }).index_by(&:id)
|
|
257
|
-
end
|
|
258
|
-
references.filter_map do |reference|
|
|
259
|
-
record = records[reference["record_id"]]
|
|
260
|
-
record_json(record, type)&.merge(drilldown: drilldown(reference))
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def representative_references
|
|
265
|
-
Array(@details["representative_records"]).filter_map do |reference|
|
|
266
|
-
next unless reference.is_a?(Hash)
|
|
267
|
-
reference.slice("record_type", "record_id", "group_hash", "execution_id", "execution_source").deep_symbolize_keys
|
|
268
|
-
.merge(drilldown: drilldown(reference))
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def record_json(record, type)
|
|
273
|
-
return unless record
|
|
274
|
-
|
|
275
|
-
common = {
|
|
276
|
-
record_type: type, record_id: record.id, group_hash: record.group_hash,
|
|
277
|
-
execution_id: record.execution_id, execution_source: record.execution_source,
|
|
278
|
-
execution_preview: record.execution_preview, occurred_at: record.occurred_at,
|
|
279
|
-
duration_ms: milliseconds(record.duration), deploy: record.deploy,
|
|
280
|
-
user_ref: record.user_ref, tenant: record.app_tenant
|
|
281
|
-
}
|
|
282
|
-
case record
|
|
283
|
-
when Telemetry::Execution
|
|
284
|
-
common.merge(name: record.name, status: record.status, outcome: record.outcome)
|
|
285
|
-
when Telemetry::Query
|
|
286
|
-
common.merge(name: record.sql.to_s.first(300), source: record.source,
|
|
287
|
-
connection: record.connection, role: record.role)
|
|
288
|
-
when Telemetry::OutgoingRequest
|
|
289
|
-
common.merge(name: "#{record.method} #{record.host}", status: record.status_code,
|
|
290
|
-
source: record.source)
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# Identifiers only: app/frontend/lib/execution-path.ts turns these into
|
|
295
|
-
# links, and it already accepts both the "job" child wire name and the
|
|
296
|
-
# "job_attempt" parent kind.
|
|
297
|
-
def drilldown(record)
|
|
298
|
-
if record["record_type"] == "query"
|
|
299
|
-
{ kind: "query", group_hash: record["group_hash"] }
|
|
300
|
-
elsif record["execution_id"].present?
|
|
301
|
-
{ kind: record["execution_source"].presence || record["record_type"], execution_id: record["execution_id"] }
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def milliseconds(value)
|
|
306
|
-
value.nil? ? nil : (value.to_f / 1000.0).round(3)
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Durable, typed context for synthetic performance/anomaly issues. Detector
|
|
5
|
-
# group hashes identify the rule, not the telemetry rows behind the breach;
|
|
6
|
-
# this snapshot preserves that original identity and a bounded set of
|
|
7
|
-
# drill-down identifiers even after raw telemetry is pruned. Record payloads
|
|
8
|
-
# remain subject to the telemetry retention policy and are never copied into
|
|
9
|
-
# the primary database.
|
|
10
|
-
class IssueDetectionSnapshot
|
|
11
|
-
REPRESENTATIVE_LIMIT = 10
|
|
12
|
-
EXECUTION_TYPES = %w[request job_attempt scheduled_task command].freeze
|
|
13
|
-
UNITS = {
|
|
14
|
-
"p95" => "milliseconds", "max" => "milliseconds", "avg" => "milliseconds",
|
|
15
|
-
"error_rate" => "percent", "failure_rate" => "percent", "count" => "events_per_minute",
|
|
16
|
-
"missed" => "schedule"
|
|
17
|
-
}.freeze
|
|
18
|
-
|
|
19
|
-
def self.capture(environment:, kind:, telemetry_type:, telemetry_group_hash:, target:, metric:, from:, to:,
|
|
20
|
-
value: nil, limit: nil, baseline: nil, rule: {}, representative_from: nil)
|
|
21
|
-
new(environment, telemetry_type, telemetry_group_hash).capture(
|
|
22
|
-
kind: kind, target: target, metric: metric, from: from, to: to,
|
|
23
|
-
value: value, limit: limit, baseline: baseline, rule: rule, representative_from: representative_from
|
|
24
|
-
)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def initialize(environment, telemetry_type, telemetry_group_hash)
|
|
28
|
-
@environment = environment
|
|
29
|
-
@telemetry_type = telemetry_type
|
|
30
|
-
@telemetry_group_hash = telemetry_group_hash
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def capture(kind:, target:, metric:, from:, to:, value:, limit:, baseline:, rule:, representative_from:)
|
|
34
|
-
representatives = @environment.with_telemetry do
|
|
35
|
-
representative_scope(representative_from || from, to).order(duration: :desc).limit(REPRESENTATIVE_LIMIT)
|
|
36
|
-
.map { |record| record_json(record) }
|
|
37
|
-
end
|
|
38
|
-
{
|
|
39
|
-
schema_version: 1, kind: kind, telemetry_type: @telemetry_type,
|
|
40
|
-
telemetry_group_hash: @telemetry_group_hash, target: target, metric: metric,
|
|
41
|
-
unit: UNITS.fetch(metric, "value"),
|
|
42
|
-
window: { from: from.iso8601(6), to: to.iso8601(6), minutes: ((to - from) / 60.0).round(2) },
|
|
43
|
-
measurement: { value: value, limit: limit }.compact,
|
|
44
|
-
baseline: baseline&.compact,
|
|
45
|
-
rule: rule.compact,
|
|
46
|
-
representative_records: representatives
|
|
47
|
-
}.compact
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def representative_scope(from, to)
|
|
53
|
-
model = record_model
|
|
54
|
-
# SQLite compares its stored six-digit timestamp strings lexically. A
|
|
55
|
-
# frozen whole-second upper bound serializes without the fraction and
|
|
56
|
-
# would otherwise exclude a record at that exact instant.
|
|
57
|
-
scope = model.where(group_hash: @telemetry_group_hash, occurred_at: from...(to + 1.second))
|
|
58
|
-
EXECUTION_TYPES.include?(@telemetry_type) ? scope.of_kind(@telemetry_type) : scope
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def record_model
|
|
62
|
-
return Telemetry::Execution if EXECUTION_TYPES.include?(@telemetry_type)
|
|
63
|
-
return Telemetry::Query if @telemetry_type == "query"
|
|
64
|
-
return Telemetry::OutgoingRequest if @telemetry_type == "outgoing_request"
|
|
65
|
-
|
|
66
|
-
raise ArgumentError, "unsupported detection telemetry type: #{@telemetry_type}"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def record_json(record)
|
|
70
|
-
{
|
|
71
|
-
record_type: @telemetry_type, record_id: record.id, group_hash: record.group_hash,
|
|
72
|
-
execution_id: record.execution_id,
|
|
73
|
-
execution_source: record.is_a?(Telemetry::Execution) ? record.kind : record.execution_source
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# One row per maintenance task (Railwatch::Maintenance::TASKS): when it last
|
|
5
|
-
# ran and who holds it now. Every process on the host runs the maintenance
|
|
6
|
-
# clock; this is how exactly one of them runs each task. The claim is a
|
|
7
|
-
# single conditional UPDATE, so SQLite's write lock is the arbiter and there
|
|
8
|
-
# is no window between checking and taking the lease.
|
|
9
|
-
class MaintenanceTask < ApplicationRecord
|
|
10
|
-
self.table_name = "railwatch_maintenance_tasks"
|
|
11
|
-
|
|
12
|
-
# The token for a claim this process won, or nil: the task is due
|
|
13
|
-
# (last_run_at older than `every`, or never) and nobody holds a live
|
|
14
|
-
# lease on it. A lease left by a process that died expires on its own.
|
|
15
|
-
# The token is per claim, not per process, so a claim that outlived its
|
|
16
|
-
# lease cannot later release the lease the next claimant holds.
|
|
17
|
-
def self.claim(name, every:, lease:, owner:, now: Time.current)
|
|
18
|
-
ensure_row(name)
|
|
19
|
-
token = "#{owner}:#{SecureRandom.hex(8)}"
|
|
20
|
-
won = where(name: name)
|
|
21
|
-
.where("lease_expires_at IS NULL OR lease_expires_at < ?", now)
|
|
22
|
-
.where("last_run_at IS NULL OR last_run_at <= ?", now - every)
|
|
23
|
-
.update_all(lease_owner: token, lease_expires_at: now + lease, updated_at: now) == 1
|
|
24
|
-
won ? token : nil
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Releases only the lease this token holds. A task that succeeded records
|
|
28
|
-
# the run so the interval starts again; one that failed records nothing,
|
|
29
|
-
# so it is eligible on the next tick rather than a full interval later.
|
|
30
|
-
def self.release(name, token:, ran_at:, succeeded:)
|
|
31
|
-
changes = { lease_owner: nil, lease_expires_at: nil, updated_at: ran_at }
|
|
32
|
-
changes[:last_run_at] = ran_at if succeeded
|
|
33
|
-
where(name: name, lease_owner: token).update_all(changes)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# The newest tick across every task: what the doctor reports as "last
|
|
37
|
-
# maintenance", and the signal that the clock is alive somewhere.
|
|
38
|
-
def self.last_tick_at
|
|
39
|
-
maximum(:last_run_at)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Two processes racing to create the same row: one loses on the unique
|
|
43
|
-
# index, and the row it wanted now exists.
|
|
44
|
-
def self.ensure_row(name)
|
|
45
|
-
return if exists?(name: name)
|
|
46
|
-
|
|
47
|
-
create!(name: name)
|
|
48
|
-
rescue ActiveRecord::RecordNotUnique
|
|
49
|
-
nil
|
|
50
|
-
end
|
|
51
|
-
private_class_method :ensure_row
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# A named, shareable filter on one environment page: "5xx checkout requests",
|
|
5
|
-
# "acme tenant logs". Pinned views appear in the sidebar under the page.
|
|
6
|
-
class SavedView < ApplicationRecord
|
|
7
|
-
self.table_name = "railwatch_saved_views"
|
|
8
|
-
# page => the index route helper the view links back to. The link is built
|
|
9
|
-
# server-side so the sidebar, the menu, and a pasted URL all agree.
|
|
10
|
-
PAGE_PATHS = {
|
|
11
|
-
"requests" => :application_environment_requests_path,
|
|
12
|
-
"jobs" => :application_environment_jobs_path,
|
|
13
|
-
"scheduled_tasks" => :application_environment_scheduled_tasks_path,
|
|
14
|
-
"commands" => :application_environment_commands_path,
|
|
15
|
-
"exceptions" => :application_environment_exceptions_path,
|
|
16
|
-
"queries" => :application_environment_queries_path,
|
|
17
|
-
"spans" => :application_environment_spans_path,
|
|
18
|
-
"logs" => :application_environment_logs_path,
|
|
19
|
-
"visits" => :application_environment_visits_path,
|
|
20
|
-
"people" => :application_environment_people_path,
|
|
21
|
-
"tenants" => :application_environment_tenants_path,
|
|
22
|
-
"llm_calls" => :application_environment_llm_calls_path,
|
|
23
|
-
"outgoing_requests" => :application_environment_outgoing_requests_path,
|
|
24
|
-
"cache_events" => :application_environment_cache_events_path,
|
|
25
|
-
"mails" => :application_environment_mails_path,
|
|
26
|
-
"notifications" => :application_environment_notifications_path
|
|
27
|
-
}.freeze
|
|
28
|
-
PAGES = PAGE_PATHS.keys.freeze
|
|
29
|
-
|
|
30
|
-
def environment = Environment.current
|
|
31
|
-
def user
|
|
32
|
-
User.find_by(id: viewer_id)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def user=(u)
|
|
36
|
-
self.viewer_id = u&.id&.to_s
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
validates :name, presence: true, length: { maximum: 80 }
|
|
40
|
-
validates :page, inclusion: { in: PAGES }
|
|
41
|
-
|
|
42
|
-
scope :pinned, -> { where(pinned: true) }
|
|
43
|
-
scope :visible_to, ->(user) { where(shared: true).or(where(viewer_id: user.id.to_s)) }
|
|
44
|
-
|
|
45
|
-
# The props every environment page shares, ready for the sidebar and the
|
|
46
|
-
# Views menu: one entry per view this user is allowed to see.
|
|
47
|
-
def self.props_for(environment, user)
|
|
48
|
-
where(environment_id: environment.id).visible_to(user).order(:name).map { |view| view.props(environment, user) }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def props(environment, user)
|
|
52
|
-
{ id: id, name: name, page: page, query: query, window: window, params: params, pinned: pinned, shared: shared,
|
|
53
|
-
mine: viewer_id == user&.id, url: path_for(environment) }
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# This page's index path with the view's window, query, and extra params
|
|
57
|
-
# (sort/dir) applied -- the shareable URL.
|
|
58
|
-
def path_for(environment)
|
|
59
|
-
Railwatch.url_helpers.public_send(PAGE_PATHS.fetch(page), environment.application_id, environment.id,
|
|
60
|
-
{ window: window.presence, q: query.presence, **params.to_h.symbolize_keys }.compact)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|