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
|
@@ -60,23 +60,18 @@ module Railwatch
|
|
|
60
60
|
Rack::QueryParser::ParameterTypeError
|
|
61
61
|
].freeze
|
|
62
62
|
|
|
63
|
-
# What
|
|
64
|
-
#
|
|
65
|
-
# "they left the default".
|
|
63
|
+
# What railwatch-server's dashboard proxy inherits from unless the host
|
|
64
|
+
# names its own.
|
|
66
65
|
DEFAULT_BASE_CONTROLLER = "ActionController::Base"
|
|
67
66
|
|
|
68
|
-
attr_accessor :
|
|
69
|
-
:export_max_deliveries, :export_max_age
|
|
70
|
-
attr_accessor :enabled, :token, :ingest_url, :allow_http, :server, :environment, :transport,
|
|
71
|
-
:issue_prefix, :repository_url, :retention_days, :dashboard_user, :writer_socket,
|
|
67
|
+
attr_accessor :enabled, :token, :ingest_url, :allow_http, :server, :environment,
|
|
72
68
|
:http_basic_auth_enabled, :http_basic_auth_user, :http_basic_auth_password, :base_controller_class,
|
|
73
|
-
:dashboard_open,
|
|
74
69
|
:sample, :log_level, :capture_request_payload,
|
|
75
70
|
:capture_exception_source, :capture_exception_locals, :redact_headers, :redact_params,
|
|
76
71
|
:buffer_size, :buffer_bytes, :execution_buffer_bytes, :batch_bytes,
|
|
77
72
|
:backpressure,
|
|
78
73
|
:flush_interval, :flush_threshold,
|
|
79
|
-
:connect_timeout, :timeout, :shutdown_timeout,
|
|
74
|
+
:connect_timeout, :timeout, :shutdown_timeout,
|
|
80
75
|
:slow_query_threshold_ms, :n_plus_one_threshold,
|
|
81
76
|
:max_view_renders_per_execution, :ignored_cache_key_prefixes,
|
|
82
77
|
:beacon_enabled, :beacon_rate_limit, :beacon_global_rate_limit, :beacon_allowed_origins,
|
|
@@ -99,49 +94,24 @@ module Railwatch
|
|
|
99
94
|
def initialize
|
|
100
95
|
@enabled = env_bool("RAILWATCH_ENABLED", true)
|
|
101
96
|
@token = ENV["RAILWATCH_TOKEN"]
|
|
102
|
-
|
|
103
|
-
#
|
|
104
|
-
@issue_prefix = ENV["RAILWATCH_ISSUE_PREFIX"]
|
|
105
|
-
@repository_url = ENV["RAILWATCH_REPOSITORY_URL"]
|
|
106
|
-
@retention_days = env_int("RAILWATCH_RETENTION_DAYS", 7)
|
|
107
|
-
@dashboard_user = nil
|
|
108
|
-
# Mirroring an embedded install's telemetry to a remote receiver. Off
|
|
109
|
-
# unless asked for: an embedded install's promise is that nothing leaves
|
|
110
|
-
# the machine, and a token being present is not consent.
|
|
111
|
-
@export_enabled = env_bool("RAILWATCH_EXPORT_ENABLED", false)
|
|
112
|
-
@export_policy = ENV.fetch("RAILWATCH_EXPORT_POLICY", "everything").to_sym
|
|
113
|
-
@export_url = ENV["RAILWATCH_EXPORT_URL"]
|
|
114
|
-
@export_token = ENV["RAILWATCH_EXPORT_TOKEN"]
|
|
115
|
-
@export_max_bytes = env_int("RAILWATCH_EXPORT_MAX_BYTES", 256 * 1024 * 1024)
|
|
116
|
-
@export_max_deliveries = env_int("RAILWATCH_EXPORT_MAX_DELIVERIES", 100_000)
|
|
117
|
-
@export_max_age = env_int("RAILWATCH_EXPORT_MAX_AGE_SECONDS", 86_400)
|
|
118
|
-
# Dashboard access, the way Mission Control Jobs does it: HTTP Basic
|
|
97
|
+
# The gate in front of the dashboard railwatch-server serves at
|
|
98
|
+
# /railwatch, the way Mission Control Jobs does it: HTTP Basic
|
|
119
99
|
# authentication is on and CLOSED by default. With no user and password
|
|
120
100
|
# configured every dashboard request is 401, so an install that forgot
|
|
121
101
|
# to set anything up is never a public page. Credentials come from
|
|
122
102
|
# Rails credentials (railwatch.http_basic_auth_user/_password, written
|
|
123
103
|
# by `bin/rails railwatch:authentication:configure`), from these env
|
|
124
104
|
# vars, or by assignment here. A host with its own admin auth turns
|
|
125
|
-
# Basic off and names a base controller
|
|
126
|
-
#
|
|
105
|
+
# Basic off and names a base controller. Nothing here reads them unless
|
|
106
|
+
# railwatch-server is installed.
|
|
127
107
|
@http_basic_auth_enabled = env_bool("RAILWATCH_HTTP_BASIC_AUTH_ENABLED", true)
|
|
128
108
|
@http_basic_auth_user = ENV["RAILWATCH_HTTP_BASIC_AUTH_USER"]
|
|
129
109
|
@http_basic_auth_password = ENV["RAILWATCH_HTTP_BASIC_AUTH_PASSWORD"]
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
@dashboard_open = env_bool("RAILWATCH_DASHBOARD_OPEN", false)
|
|
135
|
-
# The dashboard controllers inherit from this class, so a host's own
|
|
136
|
-
# before_actions (require an admin, redirect to sign-in) run in front of
|
|
137
|
-
# every page. Default is the engine's own base, which authenticates
|
|
138
|
-
# nothing itself beyond HTTP Basic above.
|
|
110
|
+
# railwatch-server's dashboard proxy inherits from this class, so a
|
|
111
|
+
# host's own before_actions (require an admin, redirect to sign-in) run
|
|
112
|
+
# in front of every page. The default authenticates nothing itself
|
|
113
|
+
# beyond HTTP Basic above.
|
|
139
114
|
@base_controller_class = ENV.fetch("RAILWATCH_BASE_CONTROLLER_CLASS", DEFAULT_BASE_CONTROLLER)
|
|
140
|
-
# Embedded mode's writer process (lib/railwatch/writer.rb): the Unix
|
|
141
|
-
# socket the Puma workers hand their batches to. Relative paths are
|
|
142
|
-
# under Rails.root. nil disables the writer and every process writes
|
|
143
|
-
# its own batches, as before.
|
|
144
|
-
@writer_socket = ENV.fetch("RAILWATCH_WRITER_SOCKET", "tmp/sockets/railwatch-writer.sock")
|
|
145
115
|
@ingest_url = ENV.fetch("RAILWATCH_INGEST_URL", "https://railwatch.rebulk.com")
|
|
146
116
|
@allow_http = env_bool("RAILWATCH_ALLOW_HTTP", false)
|
|
147
117
|
@project_root = defined?(Rails) ? Rails.root : Dir.pwd
|
|
@@ -187,13 +157,6 @@ module Railwatch
|
|
|
187
157
|
@connect_timeout = env_float("RAILWATCH_CONNECT_TIMEOUT", 1.0)
|
|
188
158
|
@timeout = env_float("RAILWATCH_TIMEOUT", 3.0)
|
|
189
159
|
@shutdown_timeout = env_float("RAILWATCH_SHUTDOWN_TIMEOUT", 2.0)
|
|
190
|
-
# Seconds a migration of the railwatch databases waits for SQLite's
|
|
191
|
-
# write lock (Patches::MigrationBusyTimeout). A deploy migrates while
|
|
192
|
-
# the old release's writer is still committing to the same file; the
|
|
193
|
-
# database's own `timeout` (5 s) is too short to get between its
|
|
194
|
-
# transactions when the disk is busy. Kept under a typical health-check
|
|
195
|
-
# window (Kamal's deploy_timeout defaults to 30 s and is often 90 s).
|
|
196
|
-
@migration_busy_timeout = env_float("RAILWATCH_MIGRATION_BUSY_TIMEOUT", 60.0)
|
|
197
160
|
@slow_query_threshold_ms = env_float("RAILWATCH_SLOW_QUERY_MS", 5.0)
|
|
198
161
|
@n_plus_one_threshold = env_int("RAILWATCH_N_PLUS_ONE_THRESHOLD", 5)
|
|
199
162
|
@max_view_renders_per_execution = 20
|
|
@@ -318,67 +281,13 @@ module Railwatch
|
|
|
318
281
|
end
|
|
319
282
|
|
|
320
283
|
def enabled?
|
|
321
|
-
@enabled &&
|
|
284
|
+
@enabled && token.present?
|
|
322
285
|
end
|
|
323
286
|
|
|
324
|
-
# :
|
|
325
|
-
#
|
|
326
|
-
def local? =
|
|
327
|
-
|
|
328
|
-
# The receiver admits an unseen delivery for seven days; queueing one for
|
|
329
|
-
# longer cannot help.
|
|
330
|
-
MAX_EXPORT_AGE = 7 * 24 * 60 * 60
|
|
331
|
-
|
|
332
|
-
# Mirroring is a thing an embedded install opts into; it is meaningless
|
|
333
|
-
# for an install that is already sending everything over HTTP.
|
|
334
|
-
def export? = export_enabled && local? && export_problem.nil?
|
|
335
|
-
|
|
336
|
-
# Why export is configured but unusable, or nil when it is fine. The
|
|
337
|
-
# doctor reports this; nothing silently half-enables.
|
|
338
|
-
def export_problem
|
|
339
|
-
return nil unless export_enabled
|
|
340
|
-
return "export needs transport :local; an :http install already sends everything" unless local?
|
|
341
|
-
return "RAILWATCH_EXPORT_POLICY #{export_policy} is not implemented" unless export_policy.to_s == "everything"
|
|
342
|
-
return "no export token: set RAILWATCH_EXPORT_TOKEN or RAILWATCH_TOKEN" if resolved_export_token.to_s.empty?
|
|
343
|
-
return "no export url: set RAILWATCH_EXPORT_URL or RAILWATCH_INGEST_URL" if resolved_export_url.to_s.empty?
|
|
344
|
-
return "export url must be HTTPS (or set RAILWATCH_ALLOW_HTTP=true)" unless url_allowed?(resolved_export_url)
|
|
345
|
-
# Past this a receiver stops recognising a delivery's id, so holding one
|
|
346
|
-
# any longer just means discovering later that it can never be sent.
|
|
347
|
-
if export_max_age > MAX_EXPORT_AGE
|
|
348
|
-
return "RAILWATCH_EXPORT_MAX_AGE_SECONDS cannot exceed #{MAX_EXPORT_AGE} (the receiver stops recognising a delivery past that)"
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
nil
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
# The receiver, and the credential we are bound to it with. Both fall back
|
|
355
|
-
# to the ordinary ingest settings so switching an install from embedded to
|
|
356
|
-
# cloud needs no second set of values.
|
|
357
|
-
def resolved_export_url
|
|
358
|
-
url = export_url.presence || (ingest_url.presence && URI.join(ingest_url, "/ingest").to_s)
|
|
359
|
-
url&.sub(%r{/\z}, "")
|
|
360
|
-
rescue URI::Error
|
|
361
|
-
# BadURIError (a relative ingest_url) is not an InvalidURIError, and
|
|
362
|
-
# letting it out of here takes the doctor down with it.
|
|
363
|
-
nil
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def resolved_export_token = export_token.presence || token
|
|
367
|
-
|
|
368
|
-
# Absolute path of the writer socket, or nil when the writer is off.
|
|
369
|
-
def writer_socket_path
|
|
370
|
-
path = writer_socket.to_s
|
|
371
|
-
return nil if path.empty?
|
|
372
|
-
|
|
373
|
-
root = defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? Rails.root.to_s : Dir.pwd
|
|
374
|
-
File.expand_path(path, root)
|
|
375
|
-
end
|
|
287
|
+
# Always false: there is no embedded transport any more. Only
|
|
288
|
+
# railwatch-server 0.1.0 calls it.
|
|
289
|
+
def local? = false
|
|
376
290
|
|
|
377
|
-
# Who the embedded dashboard shows as the signed-in operator. A host
|
|
378
|
-
# passes a lambda taking the request (cookies, warden, whatever it uses)
|
|
379
|
-
# and returning a User, {id:, name:, email:} or nil. Authentication and
|
|
380
|
-
# authorisation stay the host's job: put the mount behind its own
|
|
381
|
-
# constraint. This only names the person for comments and saved views.
|
|
382
291
|
# Both halves present. Read late (not at boot) so credentials set from an
|
|
383
292
|
# initializer, an env var or the configure task all count.
|
|
384
293
|
def http_basic_auth_configured?
|
|
@@ -386,17 +295,17 @@ module Railwatch
|
|
|
386
295
|
end
|
|
387
296
|
|
|
388
297
|
# Development with Basic on and nothing configured is open, so a first
|
|
389
|
-
# run is
|
|
390
|
-
#
|
|
391
|
-
#
|
|
298
|
+
# run is a page, not a password step first. Rails already shows full
|
|
299
|
+
# error pages there. Every other environment stays closed until
|
|
300
|
+
# credentials exist.
|
|
392
301
|
def http_basic_auth_waived?
|
|
393
302
|
http_basic_auth_enabled && !http_basic_auth_configured? && defined?(Rails) && Rails.env.development?
|
|
394
303
|
end
|
|
395
304
|
|
|
396
305
|
# Whether the request carries the configured HTTP Basic credentials.
|
|
397
306
|
# False when Basic is on and nothing is configured (closed), true when
|
|
398
|
-
# Basic is off (the host's base controller
|
|
399
|
-
#
|
|
307
|
+
# Basic is off (the host's base controller is the gate then).
|
|
308
|
+
# railwatch-server's dashboard proxy calls it.
|
|
400
309
|
def http_basic_auth_ok?(request)
|
|
401
310
|
return true unless http_basic_auth_enabled
|
|
402
311
|
return true if http_basic_auth_waived?
|
|
@@ -408,45 +317,6 @@ module Railwatch
|
|
|
408
317
|
end == true
|
|
409
318
|
end
|
|
410
319
|
|
|
411
|
-
# Which gate is in force, for the doctor, the boot warning and the live
|
|
412
|
-
# channel. :basic when HTTP Basic is on (closed until credentials exist),
|
|
413
|
-
# :controller when the host named its own base controller, :resolver when
|
|
414
|
-
# it gave a dashboard_user that can refuse, :open when it said the
|
|
415
|
-
# dashboard is deliberately public, and :undeclared when Basic is off and
|
|
416
|
-
# none of those is true -- which usually means a routes constraint the
|
|
417
|
-
# gem cannot see, and might mean nothing at all.
|
|
418
|
-
def dashboard_gate
|
|
419
|
-
return :basic if http_basic_auth_enabled
|
|
420
|
-
return :controller unless base_controller_class == DEFAULT_BASE_CONTROLLER
|
|
421
|
-
return :resolver if dashboard_user
|
|
422
|
-
return :open if dashboard_open
|
|
423
|
-
|
|
424
|
-
:undeclared
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
# Whether a live-update subscription is allowed. Action Cable runs on the
|
|
428
|
-
# host's own /cable endpoint, which a routes constraint around the
|
|
429
|
-
# engine's mount does not cover and a base controller cannot reach, so an
|
|
430
|
-
# undeclared gate refuses rather than assuming.
|
|
431
|
-
def dashboard_channel_allowed?(request)
|
|
432
|
-
return http_basic_auth_ok?(request) if http_basic_auth_enabled
|
|
433
|
-
if dashboard_user
|
|
434
|
-
return !!(dashboard_user.respond_to?(:call) ? dashboard_user.call(request) : dashboard_user)
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
# A controller name is not evidence of authorization on /cable.
|
|
438
|
-
!!dashboard_open
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
def resolve_dashboard_user(request)
|
|
442
|
-
resolved = dashboard_user.respond_to?(:call) ? dashboard_user.call(request) : dashboard_user
|
|
443
|
-
case resolved
|
|
444
|
-
when User then resolved
|
|
445
|
-
when Hash then User.new(id: resolved[:id] || User::ID, name: resolved[:name].to_s.presence || "Operator", email: resolved[:email])
|
|
446
|
-
else User.default
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
|
|
450
320
|
def ingest_url_allowed?
|
|
451
321
|
url_allowed?(ingest_url)
|
|
452
322
|
end
|
data/lib/railwatch/engine.rb
CHANGED
|
@@ -1,88 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# The dashboard controllers are Inertia controllers; a host that does not use
|
|
4
|
-
# Inertia itself never requires the gem, so the engine does.
|
|
5
|
-
require "inertia_rails"
|
|
6
|
-
# The engine's jobs (grouping, rollups, scans) are Active Job classes even
|
|
7
|
-
# though embedded mode calls their perform directly, so a host built with
|
|
8
|
-
# `rails new --minimal` (no Active Job) still needs the framework loaded.
|
|
9
|
-
# Part of the rails gem this one depends on, so nothing new is pulled in.
|
|
10
|
-
require "active_job/railtie"
|
|
11
|
-
|
|
12
3
|
module Railwatch
|
|
13
|
-
#
|
|
14
|
-
# engine's own records (issues, comments, deploys: Railwatch::ApplicationRecord)
|
|
15
|
-
# carry it; the telemetry tables are the hosted platform's schema and stay
|
|
16
|
-
# unprefixed. Defined before isolate_namespace runs, which only adds a
|
|
17
|
-
# prefix when none is set.
|
|
18
|
-
def self.table_name_prefix = ""
|
|
19
|
-
|
|
4
|
+
# Mounted for the browser beacon (POST <mount>/beacon), its only route.
|
|
20
5
|
class Engine < ::Rails::Engine
|
|
21
6
|
isolate_namespace Railwatch
|
|
22
7
|
|
|
23
|
-
config.railwatch = ActiveSupport::OrderedOptions.new
|
|
24
|
-
|
|
25
|
-
# The dashboard bundle is built into the gem at release time; serve it
|
|
26
|
-
# from here so a host app needs no asset pipeline integration at all.
|
|
27
|
-
# Where it goes in the stack is DashboardAssets.install!'s decision.
|
|
28
|
-
initializer "railwatch.dashboard_assets" do |app|
|
|
29
|
-
Railwatch::DashboardAssets.install!(app, root: root.join("public/railwatch").to_s)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Action Cable is optional: live dashboard updates need it, nothing else
|
|
33
|
-
# does. A host without it (`rails new --minimal`, --skip-action-cable)
|
|
34
|
-
# must not eager-load the engine's channel, which inherits from a class
|
|
35
|
-
# that does not exist there.
|
|
36
|
-
initializer "railwatch.channels", before: :setup_main_autoloader do
|
|
37
|
-
Rails.autoloaders.main.ignore(root.join("app/channels")) unless defined?(::ActionCable)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# The dashboard bundle subscribes to "EnvironmentChannel" by name; Action
|
|
41
|
-
# Cable constantizes that, so the engine's channel needs the bare name.
|
|
42
|
-
# Only defined when the host has not got one of its own.
|
|
43
|
-
initializer "railwatch.live_channel" do
|
|
44
|
-
next unless defined?(::ActionCable)
|
|
45
|
-
|
|
46
|
-
# Action Cable constantizes the subscription's channel name at
|
|
47
|
-
# subscribe time; the alias only has to exist by then, and it must
|
|
48
|
-
# not clobber a host channel of the same name.
|
|
49
|
-
config.to_prepare do
|
|
50
|
-
Object.const_set(:EnvironmentChannel, Railwatch::EnvironmentChannel) unless Object.const_defined?(:EnvironmentChannel)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
8
|
# The request middleware goes first so wall time includes every other
|
|
55
9
|
# middleware, exactly like Nightwatch's GlobalMiddleware.
|
|
56
10
|
initializer "railwatch.middleware", before: :load_config_initializers do |app|
|
|
57
11
|
app.middleware.insert_before 0, Railwatch::Middleware::Request
|
|
58
12
|
end
|
|
59
13
|
|
|
60
|
-
# HTTP installs have no embedded databases or job backend. Loading their
|
|
61
|
-
# models still registers them with Active Record, whose schema-cache boot
|
|
62
|
-
# hook asks every descendant for its connection_pool (e.g. with
|
|
63
|
-
# activerecord-tenanted). Decide after the host's initializer selects the
|
|
64
|
-
# transport. Keep autoloading available for the local installer and tools;
|
|
65
|
-
# only local installs should eagerly load the embedded models and jobs.
|
|
66
|
-
# Dashboard controllers also reference model constants in their class
|
|
67
|
-
# bodies. The beacon is the only controller a cloud install needs.
|
|
68
|
-
initializer "railwatch.embedded_eager_loading", after: :load_config_initializers, before: :setup_main_autoloader do
|
|
69
|
-
next if Railwatch.config.local?
|
|
70
|
-
|
|
71
|
-
Rails.autoloaders.main.do_not_eager_load(root.join("app/models"))
|
|
72
|
-
Rails.autoloaders.main.do_not_eager_load(root.join("app/jobs"))
|
|
73
|
-
root.glob("app/controllers/railwatch/*.rb").each do |path|
|
|
74
|
-
Rails.autoloaders.main.do_not_eager_load(path) unless path.basename.to_s == "beacon_controller.rb"
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
14
|
# Before anything subscribes or starts a thread, and after the app's own
|
|
79
15
|
# initializer has had its say about config: a console captures nothing.
|
|
80
16
|
initializer "railwatch.console", after: :load_config_initializers, before: "railwatch.subscribe" do
|
|
81
17
|
Railwatch::Console.silence!
|
|
82
18
|
end
|
|
83
19
|
|
|
84
|
-
#
|
|
85
|
-
#
|
|
20
|
+
# The dashboard gate railwatch-server's proxy applies. Same source
|
|
21
|
+
# Mission Control Jobs reads: railwatch.http_basic_auth_user and
|
|
22
|
+
# _password in Rails credentials, which `bin/rails
|
|
86
23
|
# railwatch:authentication:configure` writes. An initializer or env var
|
|
87
24
|
# that already set them wins.
|
|
88
25
|
initializer "railwatch.http_basic_auth", after: :load_config_initializers do |app|
|
|
@@ -91,49 +28,26 @@ module Railwatch
|
|
|
91
28
|
config.http_basic_auth_password ||= app.credentials.dig(:railwatch, :http_basic_auth_password)
|
|
92
29
|
end
|
|
93
30
|
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# cannot decode, an embedded dashboard with nothing declared in front of
|
|
97
|
-
# it, and one that is closed to everyone because Basic has no
|
|
98
|
-
# credentials outside development.
|
|
31
|
+
# Worth one line in the log at boot, because it is invisible until
|
|
32
|
+
# something is already wrong: a Rails/json pair that cannot decode.
|
|
99
33
|
initializer "railwatch.warnings", after: :load_config_initializers do
|
|
100
34
|
config.after_initialize do
|
|
101
35
|
next unless Railwatch.enabled?
|
|
102
36
|
|
|
103
37
|
Rails.logger.warn("[railwatch] #{Railwatch::JsonCompat.advice}") if Railwatch::JsonCompat.broken?
|
|
104
|
-
|
|
105
|
-
if Railwatch.config.local? && Railwatch.config.dashboard_gate == :basic &&
|
|
106
|
-
!Railwatch.config.http_basic_auth_configured? && !Rails.env.local?
|
|
107
|
-
Rails.logger.warn(
|
|
108
|
-
"[railwatch] the dashboard is closed: HTTP Basic is on and no credentials are configured for " \
|
|
109
|
-
"#{Rails.env}, so every request to it is 401. Run `RAILS_ENV=#{Rails.env} bin/rails " \
|
|
110
|
-
"railwatch:authentication:configure`, or gate it with your own auth and set " \
|
|
111
|
-
"`c.http_basic_auth_enabled = false` (docs/embedded.md)."
|
|
112
|
-
)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
if Railwatch.config.local? && Railwatch.config.dashboard_gate == :undeclared && !Rails.env.local?
|
|
116
|
-
Rails.logger.warn(
|
|
117
|
-
"[railwatch] the dashboard at the engine's mount has no gate this gem can see: HTTP Basic is off and " \
|
|
118
|
-
"no base_controller_class, dashboard_user or dashboard_open is set. If a routes constraint or your " \
|
|
119
|
-
"network already gates it, authorize /cable separately with `c.dashboard_user`, or set `c.dashboard_open = true` " \
|
|
120
|
-
"only when the network protects both endpoints. Otherwise anyone who can reach the dashboard can read every query, log line and exception this app records."
|
|
121
|
-
)
|
|
122
|
-
end
|
|
123
38
|
end
|
|
124
39
|
end
|
|
125
40
|
|
|
126
41
|
# A rake process starts its command execution before this point -- the
|
|
127
42
|
# top-level task is invoked before its `:environment` prerequisite boots
|
|
128
|
-
# Rails -- so a reporter can already exist, built from a config that
|
|
129
|
-
# not
|
|
130
|
-
# app's own configuration is final.
|
|
43
|
+
# Rails -- so a reporter can already exist, built from a config that was
|
|
44
|
+
# not final. This is the first moment the app's own configuration is.
|
|
131
45
|
initializer "railwatch.adopt_final_config", after: :load_config_initializers do
|
|
132
46
|
Railwatch.adopt_final_config!
|
|
133
47
|
end
|
|
134
48
|
|
|
135
49
|
initializer "railwatch.transport_security", after: :load_config_initializers do
|
|
136
|
-
next if Railwatch.config.
|
|
50
|
+
next if Railwatch.config.ingest_url_allowed?
|
|
137
51
|
|
|
138
52
|
Rails.logger.warn(
|
|
139
53
|
"Railwatch will not send telemetry to #{Railwatch.config.ingest_url}: plain HTTP is allowed only for loopback " \
|
|
@@ -243,28 +157,6 @@ module Railwatch
|
|
|
243
157
|
at_exit { Railwatch::Sessions.stop! }
|
|
244
158
|
end
|
|
245
159
|
|
|
246
|
-
# The embedded install's maintenance clock (release health, scans,
|
|
247
|
-
# pruning). Same shape and ordering rationale as "railwatch.health":
|
|
248
|
-
# stopped before the reporter's final flush. Maintenance.start! is a
|
|
249
|
-
# no-op unless the transport is local.
|
|
250
|
-
initializer "railwatch.maintenance" do
|
|
251
|
-
next unless Railwatch.enabled?
|
|
252
|
-
|
|
253
|
-
Railwatch::Maintenance.start!
|
|
254
|
-
at_exit { Railwatch::Maintenance.stop! }
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Draining the export queue. Same shape and ordering as the others:
|
|
258
|
-
# stopped before the reporter's final flush, so its last claim is
|
|
259
|
-
# released rather than left to expire. Sender.start! is a no-op unless
|
|
260
|
-
# export is configured and usable.
|
|
261
|
-
initializer "railwatch.export" do
|
|
262
|
-
next unless Railwatch.enabled?
|
|
263
|
-
|
|
264
|
-
Railwatch::Export::Sender.start!
|
|
265
|
-
at_exit { Railwatch::Export::Sender.stop! }
|
|
266
|
-
end
|
|
267
|
-
|
|
268
160
|
# lib/tasks/railwatch_tasks.rake is picked up by Rails::Engine's default
|
|
269
161
|
# lib/tasks convention; the rake_tasks block above only installs the
|
|
270
162
|
# Rake::Task patch.
|
|
@@ -274,7 +166,6 @@ end
|
|
|
274
166
|
require "railwatch/console"
|
|
275
167
|
require "railwatch/health"
|
|
276
168
|
require "railwatch/sessions"
|
|
277
|
-
require "railwatch/maintenance"
|
|
278
169
|
require "railwatch/middleware/request"
|
|
279
170
|
require "railwatch/job_tracing"
|
|
280
171
|
require "railwatch/controller_helpers"
|
data/lib/railwatch/patches.rb
CHANGED
|
@@ -4,7 +4,6 @@ require "railwatch/patches/net_http"
|
|
|
4
4
|
require "railwatch/patches/rake_task"
|
|
5
5
|
require "railwatch/patches/runner_command"
|
|
6
6
|
require "railwatch/patches/inertia"
|
|
7
|
-
require "railwatch/patches/migration_busy_timeout"
|
|
8
7
|
|
|
9
8
|
module Railwatch
|
|
10
9
|
module Patches
|
|
@@ -23,16 +22,6 @@ module Railwatch
|
|
|
23
22
|
# From Rails::Engine#load_tasks, which has already required rake.
|
|
24
23
|
def install_rake_task!
|
|
25
24
|
::Rake::Task.prepend(RakeTask) unless ::Rake::Task.ancestors.include?(RakeTask)
|
|
26
|
-
install_migration_busy_timeout!
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# db:prepare and db:migrate run as rake tasks, so this goes in with the
|
|
30
|
-
# rake patch. Unlike it, this one is not gated on Railwatch.enabled?: a
|
|
31
|
-
# container entrypoint migrates with Railwatch switched off, and that is
|
|
32
|
-
# exactly the migration that has to wait out the old release's writer.
|
|
33
|
-
def install_migration_busy_timeout!
|
|
34
|
-
tasks = ::ActiveRecord::Tasks::DatabaseTasks.singleton_class
|
|
35
|
-
tasks.prepend(MigrationBusyTimeout) unless tasks.ancestors.include?(MigrationBusyTimeout)
|
|
36
25
|
end
|
|
37
26
|
|
|
38
27
|
# From Rails::Application#load_runner, which RunnerCommand#perform calls
|
data/lib/railwatch/reporter.rb
CHANGED
|
@@ -13,7 +13,8 @@ module Railwatch
|
|
|
13
13
|
# counted) so the buffer's newest records win again. Without the cap a
|
|
14
14
|
# batch that keeps failing would be pinned forever while everything
|
|
15
15
|
# newer was discarded around it. Eight attempts on the backoff ladder is
|
|
16
|
-
# roughly four minutes of outage
|
|
16
|
+
# roughly four minutes of outage; one to two when every attempt is
|
|
17
|
+
# refused with a Retry-After (MAX_REFUSED_RETRY_DELAY).
|
|
17
18
|
MAX_RETRY_ATTEMPTS = 8
|
|
18
19
|
# An unhandled exception asks for an immediate flush so it reaches the
|
|
19
20
|
# platform without waiting out flush_interval. "Immediate" is this many
|
|
@@ -26,7 +27,23 @@ module Railwatch
|
|
|
26
27
|
# is enough to turn a saturated stream into breathing room without the
|
|
27
28
|
# long recovery and sparse telemetry a 16x cap would impose.
|
|
28
29
|
MAX_BACKPRESSURE_FACTOR = 8.0
|
|
30
|
+
# A receiver that answers with a Retry-After (503 "disk almost full" or
|
|
31
|
+
# "database busy", 429 "rate limit exceeded") is refusing, not failing.
|
|
32
|
+
# The next attempt waits for its Retry-After instead of the ladder, up
|
|
33
|
+
# to this long. Railwatch's receivers send 15 s for a busy database,
|
|
34
|
+
# honoured as given, and 60 s for the disk floor and the rate-limit
|
|
35
|
+
# window, which are not forecasts: the floor is re-checked every 10 s
|
|
36
|
+
# and lifts the moment space is back, and a window can end at any time.
|
|
37
|
+
# The ladder alone was worse than either: a minute into a refusal its
|
|
38
|
+
# rungs were 16-60 s apart, and a recovered app stayed sampled down
|
|
39
|
+
# until the next one. A longer wait is cut to between half and all of
|
|
40
|
+
# this, jittered so processes refused together do not return together:
|
|
41
|
+
# one POST per process every 7.5-15 s.
|
|
42
|
+
MAX_REFUSED_RETRY_DELAY = 15.0
|
|
29
43
|
DeliveryBatch = Data.define(:id, :records, :bytes, :dropped, :dropped_bytes, :prepared)
|
|
44
|
+
# From the first refusal to the next accepted delivery: when it began,
|
|
45
|
+
# what the receiver said, and the loss counters as they stood then.
|
|
46
|
+
Refusal = Struct.new(:started_at, :reason, :overflowed, :sampled_out, :gave_up)
|
|
30
47
|
|
|
31
48
|
class DeliveryError < StandardError
|
|
32
49
|
attr_reader :status, :records, :bytes, :dropped, :dropped_bytes
|
|
@@ -66,6 +83,8 @@ module Railwatch
|
|
|
66
83
|
@in_flight_bytes = 0
|
|
67
84
|
@in_flight_dropped_bytes = 0
|
|
68
85
|
@shutdown_notified = false
|
|
86
|
+
@refusal = nil
|
|
87
|
+
@sampled_out = 0
|
|
69
88
|
end
|
|
70
89
|
|
|
71
90
|
def buffer
|
|
@@ -75,6 +94,13 @@ module Railwatch
|
|
|
75
94
|
|
|
76
95
|
attr_reader :backpressure_factor
|
|
77
96
|
|
|
97
|
+
# The sampler calls this for an execution the configured rate kept and
|
|
98
|
+
# backpressure did not. Only reached while the factor is above one.
|
|
99
|
+
def note_sampled_out
|
|
100
|
+
ensure_process!
|
|
101
|
+
@mutex.synchronize { @sampled_out += 1 }
|
|
102
|
+
end
|
|
103
|
+
|
|
78
104
|
def write(record, bytes = nil)
|
|
79
105
|
ensure_process!
|
|
80
106
|
size = @buffer.push(record, bytes)
|
|
@@ -146,6 +172,8 @@ module Railwatch
|
|
|
146
172
|
@in_flight_bytes = 0
|
|
147
173
|
@in_flight_dropped_bytes = 0
|
|
148
174
|
@shutdown_notified = false
|
|
175
|
+
@refusal = nil
|
|
176
|
+
@sampled_out = 0
|
|
149
177
|
remove_instance_variable(:@shutdown_deadline) if defined?(@shutdown_deadline)
|
|
150
178
|
self
|
|
151
179
|
end
|
|
@@ -155,20 +183,11 @@ module Railwatch
|
|
|
155
183
|
# where the command patch starts the execution, and sampling that
|
|
156
184
|
# execution builds the reporter -- before the task's `:environment`
|
|
157
185
|
# prerequisite boots Rails and runs config/initializers. A token in the
|
|
158
|
-
# environment is enough for Railwatch to be enabled that early
|
|
159
|
-
# embedded app gets a reporter aimed at HTTP, chosen from a config that
|
|
160
|
-
# had not yet been told the app is embedded. Left alone it stays aimed
|
|
161
|
-
# there: the memo outlives the configuration that produced it, and every
|
|
162
|
-
# record from that process goes to the receiver instead of the app's own
|
|
163
|
-
# database, skipping the export queue and the replay receipts it earns.
|
|
186
|
+
# environment is enough for Railwatch to be enabled that early.
|
|
164
187
|
#
|
|
165
188
|
# Nothing has been recorded at that point -- starting an execution
|
|
166
189
|
# decides sampling and pushes nothing -- so deciding again here is free.
|
|
167
190
|
def adopt_final_config!
|
|
168
|
-
# The swap forked_transport already makes, for the same reason: the
|
|
169
|
-
# situation this transport was chosen for is not the situation now.
|
|
170
|
-
reselected = Railwatch.local_transport if @config.local?
|
|
171
|
-
@transport = reselected if reselected && reselected.class != @transport.class
|
|
172
191
|
# Sized from the config as it stood, which was the default one. Safe
|
|
173
192
|
# only while the buffer is still empty: resizing means a new buffer,
|
|
174
193
|
# and anything already in the old one would go with it.
|
|
@@ -220,12 +239,6 @@ module Railwatch
|
|
|
220
239
|
end
|
|
221
240
|
|
|
222
241
|
def forked_transport
|
|
223
|
-
# A child may be a different kind of process from its parent: the
|
|
224
|
-
# writer forked from a Puma master must write SQLite itself, not hand
|
|
225
|
-
# batches back to the socket it is about to serve.
|
|
226
|
-
reselected = Railwatch.local_transport if @config.local?
|
|
227
|
-
return reselected if reselected && reselected.class != @transport.class
|
|
228
|
-
|
|
229
242
|
transport = @transport.dup
|
|
230
243
|
transport.reset_after_fork! if transport.respond_to?(:reset_after_fork!)
|
|
231
244
|
transport
|
|
@@ -348,6 +361,7 @@ module Railwatch
|
|
|
348
361
|
# raise here and be retained as if it had failed.
|
|
349
362
|
Railwatch.debug { "ingest rejected #{result.rejected} of #{batch.records.size} records" } if result.rejected.to_i.positive?
|
|
350
363
|
delivery_succeeded
|
|
364
|
+
refusal_ended if @refusal
|
|
351
365
|
elsif retryable?(result)
|
|
352
366
|
retain(batch, result)
|
|
353
367
|
else
|
|
@@ -370,22 +384,33 @@ module Railwatch
|
|
|
370
384
|
end
|
|
371
385
|
|
|
372
386
|
def retain(batch, result)
|
|
387
|
+
refused_until = result.retry_after_at if result.respond_to?(:retry_after_at)
|
|
388
|
+
refusal = nil
|
|
373
389
|
gave_up = @mutex.synchronize do
|
|
374
390
|
@in_flight_records = 0
|
|
375
391
|
@in_flight_dropped = 0
|
|
376
392
|
@in_flight_bytes = 0
|
|
377
393
|
@in_flight_dropped_bytes = 0
|
|
394
|
+
if refused_until && !@refusal
|
|
395
|
+
refusal = @refusal = Refusal.new(started_at: Clock.monotonic, reason: failure_detail(result),
|
|
396
|
+
overflowed: @buffer.overflowed, sampled_out: @sampled_out, gave_up: 0)
|
|
397
|
+
# Nothing leaves this buffer until the receiver accepts again, and
|
|
398
|
+
# with retries now seconds apart, so are the flush ticks that would
|
|
399
|
+
# otherwise double the factor up to here. They hold it there.
|
|
400
|
+
@backpressure_factor = MAX_BACKPRESSURE_FACTOR if @config.backpressure
|
|
401
|
+
end
|
|
378
402
|
@retry_attempt += 1
|
|
379
403
|
if @retry_attempt > MAX_RETRY_ATTEMPTS
|
|
380
404
|
@buffer.account_dropped(batch.records.size + batch.dropped,
|
|
381
405
|
bytes: batch.bytes + batch.dropped_bytes)
|
|
406
|
+
@refusal.gave_up += batch.records.size if @refusal
|
|
382
407
|
@retry_attempt = 0
|
|
383
408
|
@retry_at = nil
|
|
384
409
|
Railwatch.debug { "gave up on a batch of #{batch.records.size} records after #{MAX_RETRY_ATTEMPTS} retries (#{result.error || result.status}); dropped and counted" }
|
|
385
410
|
next true
|
|
386
411
|
end
|
|
387
412
|
@retry_batch = batch
|
|
388
|
-
delay = retry_delay(@retry_attempt)
|
|
413
|
+
delay = refused_until ? refused_delay(refused_until) : retry_delay(@retry_attempt)
|
|
389
414
|
@retry_at = Clock.monotonic + delay
|
|
390
415
|
@wakeup.signal
|
|
391
416
|
Railwatch.debug do
|
|
@@ -394,11 +419,21 @@ module Railwatch
|
|
|
394
419
|
end
|
|
395
420
|
false
|
|
396
421
|
end
|
|
422
|
+
# From here backpressure samples out executions the app asked to keep,
|
|
423
|
+
# and the receiver refusing them has no way to count them. Said once
|
|
424
|
+
# per refusal, and refusal_ended says what it cost.
|
|
425
|
+
if refusal
|
|
426
|
+
defer_notification(
|
|
427
|
+
DeliveryError.new("Railwatch ingest is refusing deliveries (#{refusal.reason}); holding " \
|
|
428
|
+
"#{batch.records.size} #{batch.records.size == 1 ? "record" : "records"}" \
|
|
429
|
+
"#{" and sampling down" if @config.backpressure} until it accepts again",
|
|
430
|
+
status: result.status)
|
|
431
|
+
)
|
|
432
|
+
end
|
|
397
433
|
return unless gave_up
|
|
398
434
|
|
|
399
|
-
# Losing a batch is not a debug-level event: with an ingest
|
|
400
|
-
#
|
|
401
|
-
# loss is ever reported, and the dropped counter it leaves behind
|
|
435
|
+
# Losing a batch is not a debug-level event: with an ingest that never
|
|
436
|
+
# comes back this is the only place the loss is ever reported, and the dropped counter it leaves behind
|
|
402
437
|
# rides on the NEXT successful delivery, which may never happen.
|
|
403
438
|
#
|
|
404
439
|
# Reported here, after the lock is released, never inside it. The
|
|
@@ -420,6 +455,42 @@ module Railwatch
|
|
|
420
455
|
)
|
|
421
456
|
end
|
|
422
457
|
|
|
458
|
+
# The first delivery ingest accepted after refusing. The refusal was the
|
|
459
|
+
# pressure, so the divisor goes straight back to one, as it went up,
|
|
460
|
+
# rather than halving over three more flush ticks. A buffer still over
|
|
461
|
+
# its high-water mark raises it again on the next tick, as it would have
|
|
462
|
+
# anyway.
|
|
463
|
+
def refusal_ended
|
|
464
|
+
refusal, sampled_out, overflowed = @mutex.synchronize do
|
|
465
|
+
ended = @refusal
|
|
466
|
+
@refusal = nil
|
|
467
|
+
[ ended, @sampled_out - ended.sampled_out, @buffer.overflowed - ended.overflowed ]
|
|
468
|
+
end
|
|
469
|
+
@backpressure_factor = 1.0
|
|
470
|
+
dropped = overflowed + refusal.gave_up
|
|
471
|
+
seconds = (Clock.monotonic - refusal.started_at).round
|
|
472
|
+
defer_notification(
|
|
473
|
+
DeliveryError.new("Railwatch ingest is accepting deliveries again after refusing for #{seconds}s; " \
|
|
474
|
+
"meanwhile backpressure sampled out #{sampled_out} " \
|
|
475
|
+
"#{sampled_out == 1 ? "execution" : "executions"} and #{dropped} " \
|
|
476
|
+
"#{dropped == 1 ? "record was" : "records were"} dropped",
|
|
477
|
+
records: dropped)
|
|
478
|
+
)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Never sooner than the ladder's rung either, so a receiver answering
|
|
482
|
+
# "Retry-After: 0" gets backoff, not a loop.
|
|
483
|
+
def refused_delay(retry_after_at)
|
|
484
|
+
wanted = retry_after_at - Time.now
|
|
485
|
+
return MAX_REFUSED_RETRY_DELAY * (0.5 + @random.rand * 0.5) if wanted > MAX_REFUSED_RETRY_DELAY
|
|
486
|
+
|
|
487
|
+
[ wanted, retry_delay(@retry_attempt) ].max.clamp(0.0, MAX_REFUSED_RETRY_DELAY)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def failure_detail(result)
|
|
491
|
+
[ result.status && "HTTP #{result.status}", result.error ].compact.reject { |part| part.to_s.empty? }.join(": ")
|
|
492
|
+
end
|
|
493
|
+
|
|
423
494
|
def discard_unauthorized
|
|
424
495
|
batch = drain_into_flight
|
|
425
496
|
delivery_succeeded
|