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,229 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class Alert < ApplicationRecord
|
|
5
|
-
self.table_name = "railwatch_alerts"
|
|
6
|
-
MAX_DELIVERY_ATTEMPTS = 5
|
|
7
|
-
DELIVERY_LEASE = 5.minutes
|
|
8
|
-
ENQUEUE_LEASE = 5.minutes
|
|
9
|
-
RETRY_DELAYS = [ 1.minute, 5.minutes, 15.minutes, 1.hour ].freeze
|
|
10
|
-
|
|
11
|
-
# An alert is always addressed to an integration; the rule is only why it
|
|
12
|
-
# fired, and a "Send test" delivery has no rule behind it at all.
|
|
13
|
-
# No integrations in an embedded install yet: alerts are recorded, not delivered.
|
|
14
|
-
def integration = nil
|
|
15
|
-
|
|
16
|
-
def integration=(_)
|
|
17
|
-
nil
|
|
18
|
-
end
|
|
19
|
-
belongs_to :alert_rule, optional: true
|
|
20
|
-
belongs_to :issue, optional: true
|
|
21
|
-
belongs_to :summary_alert, class_name: "Alert", optional: true
|
|
22
|
-
has_many :summarized_alerts, class_name: "Alert", foreign_key: :summary_alert_id,
|
|
23
|
-
dependent: :nullify, inverse_of: :summary_alert
|
|
24
|
-
|
|
25
|
-
# `delivering` is a leased claim. A process that disappears while holding
|
|
26
|
-
# one is recovered by ReconcileAlertDeliveriesJob after DELIVERY_LEASE.
|
|
27
|
-
STATUSES = %w[pending delivering sent failed skipped collapsed summarized].freeze
|
|
28
|
-
TERMINAL_STATUSES = %w[sent failed skipped summarized].freeze
|
|
29
|
-
|
|
30
|
-
attr_accessor :suppress_auto_enqueue
|
|
31
|
-
|
|
32
|
-
before_validation :inherit_integration, on: :create
|
|
33
|
-
before_validation :assign_delivery_metadata, on: :create
|
|
34
|
-
after_create_commit :enqueue_initial_work, unless: :suppress_auto_enqueue
|
|
35
|
-
|
|
36
|
-
# How far back reconciliation looks. An alert older than this that is
|
|
37
|
-
# still pending or collapsed predates the outbox (a row an old container
|
|
38
|
-
# wrote, or one left over from before this shipped); delivering it now
|
|
39
|
-
# would page someone about something that happened hours ago.
|
|
40
|
-
RECONCILE_WINDOW = 1.hour
|
|
41
|
-
|
|
42
|
-
scope :due_for_delivery, ->(now = Time.current) {
|
|
43
|
-
where(status: "pending").where(created_at: (now - RECONCILE_WINDOW)..)
|
|
44
|
-
.where("next_delivery_at IS NULL OR next_delivery_at <= ?", now)
|
|
45
|
-
.where("delivery_enqueued_until IS NULL OR delivery_enqueued_until <= ?", now)
|
|
46
|
-
}
|
|
47
|
-
scope :with_stale_delivery_lease, ->(now = Time.current) {
|
|
48
|
-
where(status: "delivering").where("delivery_lease_expires_at <= ?", now)
|
|
49
|
-
}
|
|
50
|
-
scope :overdue_collapsed, ->(now = Time.current) {
|
|
51
|
-
where(status: "collapsed").where(created_at: (now - RECONCILE_WINDOW)..(now - AlertRule::BURST_WINDOW))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# Reserve a queue hand-off. If the process dies after this write and before
|
|
55
|
-
# perform_later, reconciliation retries once this short reservation expires.
|
|
56
|
-
def enqueue_delivery!(wait_until: nil)
|
|
57
|
-
reserved_until = nil
|
|
58
|
-
target = nil
|
|
59
|
-
with_lock do
|
|
60
|
-
return false unless status == "pending"
|
|
61
|
-
|
|
62
|
-
target = [ wait_until, next_delivery_at, Time.current ].compact.max
|
|
63
|
-
return false if delivery_enqueued_until&.future?
|
|
64
|
-
|
|
65
|
-
reserved_until = target + ENQUEUE_LEASE
|
|
66
|
-
update!(delivery_enqueued_until: reserved_until)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
if target.future?
|
|
70
|
-
DeliverAlertJob.set(wait_until: target).perform_later(self)
|
|
71
|
-
else
|
|
72
|
-
DeliverAlertJob.perform_later(self)
|
|
73
|
-
end
|
|
74
|
-
true
|
|
75
|
-
rescue StandardError
|
|
76
|
-
Alert.where(id: id, status: "pending", delivery_enqueued_until: reserved_until)
|
|
77
|
-
.update_all(delivery_enqueued_until: nil, updated_at: Time.current) if reserved_until
|
|
78
|
-
raise
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Atomically turns one queued job into the only worker allowed to deliver.
|
|
82
|
-
# Duplicate jobs and jobs for terminal alerts return nil without I/O.
|
|
83
|
-
def claim_delivery!
|
|
84
|
-
with_lock do
|
|
85
|
-
return unless status == "pending"
|
|
86
|
-
return if next_delivery_at.present? && next_delivery_at.future?
|
|
87
|
-
|
|
88
|
-
token = SecureRandom.uuid
|
|
89
|
-
update!(status: "delivering", delivery_attempts: delivery_attempts + 1,
|
|
90
|
-
delivery_key: delivery_key.presence || SecureRandom.uuid,
|
|
91
|
-
delivery_lease_id: token, delivery_lease_expires_at: DELIVERY_LEASE.from_now,
|
|
92
|
-
delivery_enqueued_until: nil)
|
|
93
|
-
token
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def perform_claimed_delivery!(token)
|
|
98
|
-
if integration.enabled?
|
|
99
|
-
integration.deliver(self)
|
|
100
|
-
finish_claim!(token, "sent")
|
|
101
|
-
else
|
|
102
|
-
finish_claim!(token, "skipped")
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Persists retry intent before another job is scheduled. A crash at the
|
|
107
|
-
# following queue boundary is recovered by ReconcileAlertDeliveriesJob.
|
|
108
|
-
def fail_claim!(token, exception)
|
|
109
|
-
message = "#{exception.class}: #{exception.message}".first(1000)
|
|
110
|
-
retry_at = nil
|
|
111
|
-
with_lock do
|
|
112
|
-
return unless owns_delivery_claim?(token)
|
|
113
|
-
|
|
114
|
-
if delivery_attempts >= MAX_DELIVERY_ATTEMPTS
|
|
115
|
-
update!(status: "failed", error: message, next_delivery_at: nil,
|
|
116
|
-
delivery_lease_id: nil, delivery_lease_expires_at: nil, delivery_enqueued_until: nil)
|
|
117
|
-
else
|
|
118
|
-
retry_at = RETRY_DELAYS.fetch(delivery_attempts - 1).from_now
|
|
119
|
-
update!(status: "pending", error: message, next_delivery_at: retry_at,
|
|
120
|
-
delivery_lease_id: nil, delivery_lease_expires_at: nil, delivery_enqueued_until: nil)
|
|
121
|
-
end
|
|
122
|
-
integration.update_columns(last_error: message)
|
|
123
|
-
end
|
|
124
|
-
retry_at
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# A lease expiry is indistinguishable from a worker dying after the remote
|
|
128
|
-
# endpoint accepted the message but before `sent` committed. Retrying is
|
|
129
|
-
# required for at-least-once delivery; webhook consumers can deduplicate by
|
|
130
|
-
# delivery_key / Idempotency-Key.
|
|
131
|
-
def recover_stale_delivery!
|
|
132
|
-
retryable = false
|
|
133
|
-
with_lock do
|
|
134
|
-
return false unless status == "delivering" && delivery_lease_expires_at&.past?
|
|
135
|
-
|
|
136
|
-
message = error.presence || "Delivery lease expired before completion"
|
|
137
|
-
if delivery_attempts >= MAX_DELIVERY_ATTEMPTS
|
|
138
|
-
update!(status: "failed", error: message, next_delivery_at: nil,
|
|
139
|
-
delivery_lease_id: nil, delivery_lease_expires_at: nil, delivery_enqueued_until: nil)
|
|
140
|
-
else
|
|
141
|
-
update!(status: "pending", error: message, next_delivery_at: Time.current,
|
|
142
|
-
delivery_lease_id: nil, delivery_lease_expires_at: nil, delivery_enqueued_until: nil)
|
|
143
|
-
retryable = true
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
retryable
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Explicit operator action: terminal failures get a fresh, bounded attempt
|
|
150
|
-
# budget. Sent/skipped alerts are intentionally not replayed by this path.
|
|
151
|
-
def retry_delivery!
|
|
152
|
-
with_lock do
|
|
153
|
-
return false unless status == "failed"
|
|
154
|
-
|
|
155
|
-
update!(status: "pending", error: nil, delivery_attempts: 0,
|
|
156
|
-
next_delivery_at: Time.current, delivery_enqueued_until: nil,
|
|
157
|
-
delivery_lease_id: nil, delivery_lease_expires_at: nil)
|
|
158
|
-
end
|
|
159
|
-
enqueue_delivery!
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Synchronous delivery is retained for "Send test", whose HTTP response
|
|
163
|
-
# must report the real destination outcome. Normal alerts use the leased job
|
|
164
|
-
# path above.
|
|
165
|
-
def deliver!
|
|
166
|
-
return self if TERMINAL_STATUSES.include?(status)
|
|
167
|
-
return update!(status: "skipped") unless integration.enabled?
|
|
168
|
-
|
|
169
|
-
integration.deliver(self)
|
|
170
|
-
mark_sent!
|
|
171
|
-
self
|
|
172
|
-
rescue StandardError => e
|
|
173
|
-
message = "#{e.class}: #{e.message}".first(1000)
|
|
174
|
-
update!(status: "failed", error: message)
|
|
175
|
-
integration.update_columns(last_error: message)
|
|
176
|
-
raise
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
private
|
|
180
|
-
|
|
181
|
-
def enqueue_initial_work
|
|
182
|
-
# An embedded install has no integration to deliver to and no delivery
|
|
183
|
-
# jobs to enqueue on; the alert is recorded and that is the end of it.
|
|
184
|
-
return if integration.nil?
|
|
185
|
-
|
|
186
|
-
if status == "pending"
|
|
187
|
-
enqueue_delivery!
|
|
188
|
-
elsif status == "collapsed" && alert_rule
|
|
189
|
-
SummarizeAlertBurstJob.set(wait_until: created_at + AlertRule::BURST_WINDOW).perform_later(alert_rule)
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def assign_delivery_metadata
|
|
194
|
-
self.delivery_key ||= SecureRandom.uuid
|
|
195
|
-
self.next_delivery_at ||= Time.current if status.blank? || status == "pending"
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def inherit_integration
|
|
199
|
-
self.integration ||= alert_rule&.integration
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def owns_delivery_claim?(token)
|
|
203
|
-
status == "delivering" && delivery_lease_id == token
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def finish_claim!(token, outcome)
|
|
207
|
-
with_lock do
|
|
208
|
-
return false unless owns_delivery_claim?(token)
|
|
209
|
-
|
|
210
|
-
attributes = { status: outcome, error: nil, next_delivery_at: nil,
|
|
211
|
-
delivery_lease_id: nil, delivery_lease_expires_at: nil,
|
|
212
|
-
delivery_enqueued_until: nil }
|
|
213
|
-
attributes[:sent_at] = Time.current if outcome == "sent"
|
|
214
|
-
update!(attributes)
|
|
215
|
-
if outcome == "sent"
|
|
216
|
-
integration.update_columns(last_delivered_at: Time.current, last_error: nil)
|
|
217
|
-
issue&.activities&.create!(kind: "alert", data: { event: event, integration: integration.kind })
|
|
218
|
-
end
|
|
219
|
-
true
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def mark_sent!
|
|
224
|
-
update!(status: "sent", sent_at: Time.current, error: nil)
|
|
225
|
-
integration.update_columns(last_delivered_at: Time.current, last_error: nil)
|
|
226
|
-
issue&.activities&.create!(kind: "alert", data: { event: event, integration: integration.kind })
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
end
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class AlertRule < ApplicationRecord
|
|
5
|
-
self.table_name = "railwatch_alert_rules"
|
|
6
|
-
TITLE_PATTERN_MAX_LENGTH = 256
|
|
7
|
-
TITLE_PATTERN_TIMEOUT = 0.01
|
|
8
|
-
EVENTS = %w[new_issue regressed_issue resolved_issue ignored_issue assigned_issue threshold anomaly silent_host quota crash_free_drop].freeze
|
|
9
|
-
|
|
10
|
-
# A deploy that breaks one page opens dozens of issues in a minute, and
|
|
11
|
-
# each one used to be its own Slack message. These two events are the ones
|
|
12
|
-
# that arrive in bursts.
|
|
13
|
-
BURST_EVENTS = %w[new_issue regressed_issue].freeze
|
|
14
|
-
BURST_WINDOW = 60.seconds
|
|
15
|
-
# Deliver this many individually, then fold the rest of the window into one
|
|
16
|
-
# summary message.
|
|
17
|
-
BURST_LIMIT = 5
|
|
18
|
-
|
|
19
|
-
def application = Application.current
|
|
20
|
-
def integration = nil
|
|
21
|
-
has_many :alerts, dependent: :destroy
|
|
22
|
-
|
|
23
|
-
validates :event, inclusion: { in: EVENTS }
|
|
24
|
-
validate :valid_title_pattern
|
|
25
|
-
|
|
26
|
-
# De-duplicate: one alert per issue per event per 30 minutes.
|
|
27
|
-
def fire!(event:, issue: nil, payload: {})
|
|
28
|
-
# Serialize the duplicate/burst decision per rule. The alert is born in
|
|
29
|
-
# its final outbox state, so its after-commit hook can never enqueue a row
|
|
30
|
-
# that is about to become collapsed.
|
|
31
|
-
with_lock do
|
|
32
|
-
return if issue && alerts.where(issue: issue, event: event).where("created_at > ?", 30.minutes.ago).exists?
|
|
33
|
-
|
|
34
|
-
status = bursting?(event, issue) ? "collapsed" : "pending"
|
|
35
|
-
alerts.create!(event: event, issue: issue, payload: payload, status: status)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Optional narrowing of what this rule notifies about, stored in `filters`:
|
|
40
|
-
# environment_ids, kinds, min_priority, title_pattern. An absent or empty
|
|
41
|
-
# filter matches everything; an issue-less alert (quota, silent_host) is
|
|
42
|
-
# only narrowed by environment, via payload[:environment_id].
|
|
43
|
-
def matches?(issue:, payload: {})
|
|
44
|
-
return false unless environment_matches?(issue, payload)
|
|
45
|
-
return true unless issue
|
|
46
|
-
kind_matches?(issue) && priority_matches?(issue) && title_matches?(issue)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def valid_title_pattern
|
|
52
|
-
pattern = filters["title_pattern"].to_s
|
|
53
|
-
return if pattern.blank?
|
|
54
|
-
|
|
55
|
-
if pattern.length > TITLE_PATTERN_MAX_LENGTH
|
|
56
|
-
errors.add(:filters, "title pattern is too long (maximum is #{TITLE_PATTERN_MAX_LENGTH} characters)")
|
|
57
|
-
return
|
|
58
|
-
end
|
|
59
|
-
return unless regular_expression_pattern?(pattern)
|
|
60
|
-
|
|
61
|
-
compile_title_pattern(pattern)
|
|
62
|
-
rescue RegexpError => e
|
|
63
|
-
errors.add(:filters, "title pattern is not a valid regular expression: #{e.message}")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# True once this rule has already fired BURST_LIMIT issue alerts inside the
|
|
67
|
-
# window. Only alerts that carry an issue are counted, so summary rows can
|
|
68
|
-
# never inflate the next window's tally.
|
|
69
|
-
def bursting?(event, issue)
|
|
70
|
-
return false unless BURST_EVENTS.include?(event) && issue
|
|
71
|
-
alerts.where(event: event, created_at: BURST_WINDOW.ago..).where.not(issue_id: nil).count >= BURST_LIMIT
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def environment_matches?(issue, payload)
|
|
75
|
-
ids = Array(filters["environment_ids"]).compact_blank.map(&:to_i)
|
|
76
|
-
return true if ids.empty?
|
|
77
|
-
ids.include?((issue&.environment_id || payload[:environment_id] || payload["environment_id"]).to_i)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def kind_matches?(issue)
|
|
81
|
-
kinds = Array(filters["kinds"]).compact_blank
|
|
82
|
-
kinds.empty? || kinds.include?(issue.kind)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def priority_matches?(issue)
|
|
86
|
-
minimum = filters["min_priority"]
|
|
87
|
-
return true if minimum.blank? || Issue::PRIORITIES.exclude?(minimum)
|
|
88
|
-
Issue::PRIORITIES.index(issue.priority) >= Issue::PRIORITIES.index(minimum)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# A pattern wrapped in slashes is a regular expression, anything else is a
|
|
92
|
-
# case-insensitive substring.
|
|
93
|
-
def title_matches?(issue)
|
|
94
|
-
pattern = filters["title_pattern"].to_s
|
|
95
|
-
return true if pattern.blank?
|
|
96
|
-
if regular_expression_pattern?(pattern)
|
|
97
|
-
begin
|
|
98
|
-
return issue.title.match?(compile_title_pattern(pattern))
|
|
99
|
-
rescue Regexp::TimeoutError
|
|
100
|
-
# A pattern that backtracks past its budget matches nothing rather
|
|
101
|
-
# than stalling every alert behind it.
|
|
102
|
-
Rails.logger.warn("alert rule title pattern timed out alert_rule_id=#{id}")
|
|
103
|
-
return false
|
|
104
|
-
rescue RegexpError
|
|
105
|
-
# An unparsable regexp degrades to a substring match on its source, so
|
|
106
|
-
# a rule saved before this was validated does not silently go dark.
|
|
107
|
-
pattern = pattern[1..-2]
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
issue.title.downcase.include?(pattern.downcase)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def regular_expression_pattern?(pattern)
|
|
114
|
-
pattern.start_with?("/") && pattern.end_with?("/") && pattern.length > 2
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def compile_title_pattern(pattern)
|
|
118
|
-
Regexp.new(pattern[1..-2], Regexp::IGNORECASE, timeout: TITLE_PATTERN_TIMEOUT)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# "Alert when p95 of GET /checkout is more than 3 standard deviations above
|
|
5
|
-
# its 7-day baseline for the same time of day." Evaluated by
|
|
6
|
-
# DetectAnomaliesJob against hourly rollups.
|
|
7
|
-
class AnomalyRule < ApplicationRecord
|
|
8
|
-
self.table_name = "railwatch_anomaly_rules"
|
|
9
|
-
MIN_BASELINE_DAYS = 3
|
|
10
|
-
MAX_BASELINE_DAYS = 30
|
|
11
|
-
MAX_DEVIATION = 10
|
|
12
|
-
MAX_WINDOW_MINUTES = 1_440
|
|
13
|
-
TARGET_KINDS = %w[requests jobs queries outgoing_requests scheduled_tasks llm_calls].freeze
|
|
14
|
-
# spend included because "we are spending well above baseline for this
|
|
15
|
-
# hour of the week" is the LLM alert that needs no threshold picked.
|
|
16
|
-
METRICS = %w[p95 avg count error_rate spend].freeze
|
|
17
|
-
SHARED_METRICS = %w[p95 avg count error_rate].freeze
|
|
18
|
-
LLM_METRICS = %w[spend].freeze
|
|
19
|
-
|
|
20
|
-
def self.metrics_for(target_kind)
|
|
21
|
-
target_kind == "llm_calls" ? SHARED_METRICS + LLM_METRICS : SHARED_METRICS
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def environment = Environment.current
|
|
25
|
-
|
|
26
|
-
validates :target_kind, inclusion: { in: TARGET_KINDS }
|
|
27
|
-
validates :metric, inclusion: { in: METRICS }
|
|
28
|
-
validate :metric_applies_to_target_kind
|
|
29
|
-
validates :target, presence: true, length: { maximum: 256 }
|
|
30
|
-
validates :deviation, numericality: { greater_than: 0, less_than_or_equal_to: MAX_DEVIATION }
|
|
31
|
-
validates :window_minutes, numericality: { only_integer: true, greater_than: 0,
|
|
32
|
-
less_than_or_equal_to: MAX_WINDOW_MINUTES }
|
|
33
|
-
validates :baseline_days, numericality: { only_integer: true, greater_than_or_equal_to: MIN_BASELINE_DAYS,
|
|
34
|
-
less_than_or_equal_to: MAX_BASELINE_DAYS }
|
|
35
|
-
|
|
36
|
-
scope :enabled, -> { where(enabled: true) }
|
|
37
|
-
|
|
38
|
-
def description
|
|
39
|
-
"#{target_kind} #{target == '*' ? 'all' : target}: #{metric} > #{deviation}σ above #{baseline_days}-day baseline"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def metric_applies_to_target_kind
|
|
45
|
-
return if metric.blank? || target_kind.blank?
|
|
46
|
-
return if AnomalyRule.metrics_for(target_kind).include?(metric)
|
|
47
|
-
|
|
48
|
-
errors.add(:metric, "is not available for #{target_kind}")
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# The one application an embedded install monitors: the host. Not a table.
|
|
5
|
-
# Issue keys need a prefix and a counter; the rest of what the platform's
|
|
6
|
-
# Application carries (account, repository links, alert rules) is either
|
|
7
|
-
# fixed here or empty.
|
|
8
|
-
class Application
|
|
9
|
-
ID = 1
|
|
10
|
-
|
|
11
|
-
attr_reader :id, :name, :slug
|
|
12
|
-
|
|
13
|
-
def self.current = @current ||= new
|
|
14
|
-
def self.find(id)
|
|
15
|
-
raise ActiveRecord::RecordNotFound, "Application #{id}" unless id.to_i == ID
|
|
16
|
-
current
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def initialize
|
|
20
|
-
@id = ID
|
|
21
|
-
@name = Rails.application.class.module_parent_name
|
|
22
|
-
@slug = @name.parameterize
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def issue_prefix = Railwatch.config.issue_prefix || @name.upcase.gsub(/[^A-Z0-9]/, "")[0, 4].presence || "APP"
|
|
26
|
-
def repository_url = Railwatch.config.repository_url
|
|
27
|
-
def default_branch = "main"
|
|
28
|
-
def environments = [ Environment.current ]
|
|
29
|
-
def alert_rules = AlertRule.where(application_id: ID)
|
|
30
|
-
def account = Railwatch::Embedded::Account
|
|
31
|
-
def issues = Issue.where(application_id: ID)
|
|
32
|
-
|
|
33
|
-
# Issue numbers are per application on the platform (APP-1, APP-2, ...),
|
|
34
|
-
# allocated under a row lock on the application. There is no row here, so
|
|
35
|
-
# the meta database's own write lock serialises it: Issue creation already
|
|
36
|
-
# runs inside a transaction, and SQLite allows one writer.
|
|
37
|
-
def next_issue_number!
|
|
38
|
-
(Issue.where(application_id: ID).maximum(:number) || 0) + 1
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
include GlobalID::Identification
|
|
42
|
-
def to_global_id(...) = GlobalID.create(self, app: "railwatch")
|
|
43
|
-
end
|
|
44
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Base class for what the engine owns about the monitored application besides
|
|
5
|
-
# telemetry: issues and their comments and activity, deploys, saved views,
|
|
6
|
-
# thresholds, anomaly rules, alert rules and alerts. These are authored and
|
|
7
|
-
# permanent where telemetry is derived and pruned, so they live in their own
|
|
8
|
-
# database (`railwatch` in the host's database.yml) and never in the host's
|
|
9
|
-
# primary.
|
|
10
|
-
class ApplicationRecord < ActiveRecord::Base
|
|
11
|
-
self.abstract_class = true
|
|
12
|
-
begin
|
|
13
|
-
connects_to database: { writing: :railwatch, reading: :railwatch }
|
|
14
|
-
rescue ActiveRecord::AdapterNotSpecified, LoadError
|
|
15
|
-
# No `railwatch` entry in this environment's database.yml, or an entry
|
|
16
|
-
# whose adapter gem is not in the bundle yet (LoadError). A cloud-transport
|
|
17
|
-
# app has none and still eager-loads this class in production, and so
|
|
18
|
-
# does the installer's own boot, before it has written the
|
|
19
|
-
# entry -- so loading must not raise. Using it must, though: without
|
|
20
|
-
# connects_to this class would inherit ActiveRecord::Base's PRIMARY
|
|
21
|
-
# connection, and its tables are unprefixed, so a query would read and
|
|
22
|
-
# a write would corrupt the host application's own tables. Every route
|
|
23
|
-
# into the connection goes through connection_pool, so refusing here
|
|
24
|
-
# fails closed for reads and writes alike.
|
|
25
|
-
def self.connection_pool
|
|
26
|
-
raise Railwatch::DatabaseNotConfigured,
|
|
27
|
-
"the `railwatch` database (issues, comments, saved views and deploys) is not configured for the " \
|
|
28
|
-
"#{Rails.env} environment; run `bin/rails generate railwatch:install` " \
|
|
29
|
-
"or add it to config/database.yml (docs/embedded.md)"
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class Comment < ApplicationRecord
|
|
5
|
-
self.table_name = "railwatch_comments"
|
|
6
|
-
SOURCES = %w[railwatch linear].freeze
|
|
7
|
-
|
|
8
|
-
belongs_to :issue
|
|
9
|
-
def user
|
|
10
|
-
viewer_id && User.find_by(id: viewer_id)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def user=(u)
|
|
14
|
-
self.viewer_id = u&.id&.to_s
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
validates :body, presence: true
|
|
18
|
-
validates :source, inclusion: { in: SOURCES }
|
|
19
|
-
validates :user, presence: true, if: -> { source == "railwatch" }
|
|
20
|
-
validates :author_name, presence: true, if: -> { source == "linear" }
|
|
21
|
-
|
|
22
|
-
after_create :record_activity
|
|
23
|
-
after_create_commit :enqueue_linear_sync
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def record_activity
|
|
28
|
-
issue.activities.create!(kind: "comment", user: user,
|
|
29
|
-
data: { comment_id: id, body: body, source: source, author_name: author_name })
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def enqueue_linear_sync
|
|
33
|
-
nil unless source == "railwatch"
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class Deploy < ApplicationRecord
|
|
5
|
-
self.table_name = "railwatch_deploys"
|
|
6
|
-
def environment = Environment.current
|
|
7
|
-
|
|
8
|
-
validates :deploy, presence: true, uniqueness: { scope: :environment_id }
|
|
9
|
-
validates :deployed_at, presence: true
|
|
10
|
-
# Rendered as the "Open" link on the deploy page. Same rule as
|
|
11
|
-
# Application#repository_url, checked by parsing: one whole http(s) URL
|
|
12
|
-
# with a host, so a hook (or whoever holds its token) cannot put a
|
|
13
|
-
# javascript:/data: link, a hostless "https://?x", or an arbitrary
|
|
14
|
-
# off-site destination behind a trusted button.
|
|
15
|
-
validate :url_is_a_web_address
|
|
16
|
-
|
|
17
|
-
scope :recent, -> { order(deployed_at: :desc) }
|
|
18
|
-
scope :between, ->(from, to) { where(deployed_at: from..to) }
|
|
19
|
-
|
|
20
|
-
before_validation :assign_previous_ref
|
|
21
|
-
before_validation :trim_commits
|
|
22
|
-
|
|
23
|
-
def short_ref
|
|
24
|
-
ref.to_s.first(7).presence || deploy.first(12)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def commits_count
|
|
28
|
-
commits.size
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# The deploy immediately before this one in the same environment.
|
|
32
|
-
def previous_deploy
|
|
33
|
-
environment.deploys.where(deployed_at: ...deployed_at).recent.first
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# The stretch of time this release was live: from its own deploy until the
|
|
37
|
-
# next one, or until now for the current release. Release health is summed
|
|
38
|
-
# over this, not over the page's window.
|
|
39
|
-
def window
|
|
40
|
-
[ deployed_at, environment.deploys.where("deployed_at > ?", deployed_at).order(:deployed_at).first&.deployed_at || Time.current ]
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def url_is_a_web_address
|
|
46
|
-
return if url.blank?
|
|
47
|
-
parsed = url.match?(/\A\S+\z/) ? URI.parse(url) : nil
|
|
48
|
-
return if parsed.is_a?(URI::HTTP) && parsed.host.present?
|
|
49
|
-
errors.add(:url, "must be a single http:// or https:// URL")
|
|
50
|
-
rescue URI::InvalidURIError
|
|
51
|
-
errors.add(:url, "must be a single http:// or https:// URL")
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def assign_previous_ref
|
|
55
|
-
return if previous_ref.present? || deployed_at.blank?
|
|
56
|
-
self.previous_ref = previous_deploy&.ref
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# The post-deploy hook sends its most recent commits (newest first) without
|
|
60
|
-
# knowing where the last deploy stopped, so cut the list at that commit.
|
|
61
|
-
def trim_commits
|
|
62
|
-
return if previous_ref.blank?
|
|
63
|
-
index = commits.index { |c| c["sha"].to_s.start_with?(previous_ref) }
|
|
64
|
-
self.commits = commits.first(index) if index
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# The one environment an embedded install monitors: the host application in
|
|
5
|
-
# its current Rails.env. Not a table. The hosted platform's Environment is an
|
|
6
|
-
# account-scoped Active Record row with a token, quota and pause state; the
|
|
7
|
-
# telemetry, ingest and dashboard code only ever asks it for an id, a slug,
|
|
8
|
-
# a name, with_telemetry, and a few display attributes, so this stands in
|
|
9
|
-
# with exactly that surface.
|
|
10
|
-
class Environment
|
|
11
|
-
ID = 1
|
|
12
|
-
|
|
13
|
-
attr_reader :id, :name, :slug, :application_name
|
|
14
|
-
|
|
15
|
-
def self.current = @current ||= new
|
|
16
|
-
|
|
17
|
-
def self.find(id)
|
|
18
|
-
raise ActiveRecord::RecordNotFound, "Environment #{id}" unless id.to_i == ID
|
|
19
|
-
current
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def initialize
|
|
23
|
-
@id = ID
|
|
24
|
-
@name = Rails.env.to_s
|
|
25
|
-
@application_name = Rails.application.class.module_parent_name
|
|
26
|
-
@slug = "#{@application_name.parameterize}-#{@name}"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def with_telemetry(&block) = TelemetryRecord.with_tenant(slug, &block)
|
|
30
|
-
def telemetry_exists? = true
|
|
31
|
-
def paused? = false
|
|
32
|
-
def token_prefix = "embedded"
|
|
33
|
-
def retention_days = 7
|
|
34
|
-
# Deliberately read from the telemetry database rather than held in this
|
|
35
|
-
# process. A deployed embedded install ingests in the writer process and
|
|
36
|
-
# renders the dashboard in a web one, so a value set during ingest is
|
|
37
|
-
# invisible to the page that needs it -- and a nil here is what the
|
|
38
|
-
# dashboard treats as "no events yet", so it showed its install steps no
|
|
39
|
-
# matter how much had been recorded. The newest batch row is the same
|
|
40
|
-
# fact, in the file both processes already share, one indexed lookup away.
|
|
41
|
-
# Ordered by received_at, not id: batches are written concurrently, so one
|
|
42
|
-
# that captured an earlier receipt time can land after a later one, and
|
|
43
|
-
# taking the newest row by insertion order would hand the dashboard a
|
|
44
|
-
# timestamp that goes backwards. `recent` is the scope that states this.
|
|
45
|
-
def last_seen_at
|
|
46
|
-
with_telemetry { Telemetry::IngestBatch.recent.pick(:received_at) }
|
|
47
|
-
end
|
|
48
|
-
def application = Application.current
|
|
49
|
-
def issues = Issue.where(environment_id: ID)
|
|
50
|
-
def deploys = Deploy.where(environment_id: ID)
|
|
51
|
-
def saved_views = SavedView.where(environment_id: ID)
|
|
52
|
-
def thresholds = Threshold.where(environment_id: ID)
|
|
53
|
-
def anomaly_rules = AnomalyRule.where(environment_id: ID)
|
|
54
|
-
def events_this_month = 0
|
|
55
|
-
# The platform lets an account list the servers it expects to report so the
|
|
56
|
-
# dashboard can flag silent ones. One embedded install is its own server.
|
|
57
|
-
def expected_servers = []
|
|
58
|
-
|
|
59
|
-
# Ingest::Batch touches this after writing a batch. Nothing to record: the
|
|
60
|
-
# batch row it just wrote is what last_seen_at reads.
|
|
61
|
-
def update_columns(last_seen_at:) = nil
|
|
62
|
-
|
|
63
|
-
# GlobalID for Active Job arguments (RollupJob.perform_later(environment, bucket)).
|
|
64
|
-
include GlobalID::Identification
|
|
65
|
-
def to_global_id(...) = GlobalID.create(self, app: "railwatch")
|
|
66
|
-
def self.find_by_id(id) = find(id)
|
|
67
|
-
end
|
|
68
|
-
end
|