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,194 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# TASKS is built from duration literals at load time; the engine has these
|
|
4
|
-
# loaded already, but a writer process required ahead of the app must not
|
|
5
|
-
# depend on that.
|
|
6
|
-
require "active_support/core_ext/numeric/time"
|
|
7
|
-
|
|
8
|
-
module Railwatch
|
|
9
|
-
# The embedded install's own clock. One background thread per web/worker
|
|
10
|
-
# process wakes every TICK seconds and runs whichever maintenance task is
|
|
11
|
-
# due: release-health rollups, threshold and anomaly scans, missed
|
|
12
|
-
# scheduled tasks, auto-resolve, pruning. Every process runs the clock;
|
|
13
|
-
# one process at a time runs a given task, claimed through a lease row in
|
|
14
|
-
# the railwatch database (MaintenanceTask), so a Puma cluster and a Solid
|
|
15
|
-
# Queue worker on the same host do not all prune at once.
|
|
16
|
-
#
|
|
17
|
-
# Same thread shape as Railwatch::Health: parked on a ConditionVariable,
|
|
18
|
-
# started from the engine, re-armed in every forked child. Nothing here
|
|
19
|
-
# goes through Active Job -- the bodies call the job classes' perform
|
|
20
|
-
# directly -- so an embedded install needs no worker, no recurring.yml,
|
|
21
|
-
# and never writes the host's queue adapter.
|
|
22
|
-
#
|
|
23
|
-
# A task must never be visible to the app it is maintaining: each body runs
|
|
24
|
-
# inside Railwatch.ignore and rescues everything.
|
|
25
|
-
module Maintenance
|
|
26
|
-
ROLES = %w[web worker writer].freeze
|
|
27
|
-
TICK = 30
|
|
28
|
-
FOLLOWUP_BATCHES_PER_TICK = 200
|
|
29
|
-
|
|
30
|
-
# name => [interval, lease, body]. The lease is how long a claim is held
|
|
31
|
-
# by a process that never releases it (crashed mid-task); it is a ceiling
|
|
32
|
-
# on the work, not an estimate of it.
|
|
33
|
-
TASKS = {
|
|
34
|
-
# A batch's exceptions are grouped into issues right after it commits;
|
|
35
|
-
# a process that dies in between leaves the work recorded on the
|
|
36
|
-
# batch's ledger row. Bounded per tick so a long outage drains over a
|
|
37
|
-
# few ticks rather than one long one.
|
|
38
|
-
"drain_followups" => [ 1.minute, 5.minutes, lambda { |env|
|
|
39
|
-
env.with_telemetry do
|
|
40
|
-
Telemetry::IngestBatch.with_pending_followups.limit(FOLLOWUP_BATCHES_PER_TICK).each do |batch|
|
|
41
|
-
batch.drain_followups!(env)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
} ],
|
|
45
|
-
"release_health" => [ 1.minute, 5.minutes, lambda { |env|
|
|
46
|
-
now = Time.current
|
|
47
|
-
[ now.beginning_of_hour, (now - 1.hour).beginning_of_hour ].each do |bucket|
|
|
48
|
-
ReleaseHealthRollupJob.new.perform(env, bucket)
|
|
49
|
-
end
|
|
50
|
-
} ],
|
|
51
|
-
# Embedded batches fold themselves into the current hour as they land
|
|
52
|
-
# (Ingest::RollupAbsorber), so the reconciler only ever needs to catch
|
|
53
|
-
# rows that arrived after their hour closed.
|
|
54
|
-
"rollup_reconcile" => [ 1.hour, 10.minutes, lambda { |env|
|
|
55
|
-
RollupJob.new.perform(env, (Time.current - 1.hour).beginning_of_hour)
|
|
56
|
-
} ],
|
|
57
|
-
"performance_scan" => [ 5.minutes, 10.minutes, lambda { |env|
|
|
58
|
-
DetectPerformanceIssuesJob.new.perform(env)
|
|
59
|
-
} ],
|
|
60
|
-
"anomaly_scan" => [ 5.minutes, 10.minutes, lambda { |env|
|
|
61
|
-
DetectAnomaliesJob.new.perform(env) if AnomalyRule.where(enabled: true).exists?
|
|
62
|
-
} ],
|
|
63
|
-
"scheduled_tasks" => [ 10.minutes, 10.minutes, lambda { |env|
|
|
64
|
-
CheckScheduledTasksJob.new.perform(env)
|
|
65
|
-
} ],
|
|
66
|
-
"auto_resolve" => [ 24.hours, 10.minutes, lambda { |_env|
|
|
67
|
-
AutoResolveIssuesJob.new.perform
|
|
68
|
-
} ],
|
|
69
|
-
# Deliveries the export queue has stopped waiting for, and the history
|
|
70
|
-
# of ones it finished. Without this the age limit is advice rather than
|
|
71
|
-
# a limit: a delivery whose receiver never recovers keeps its bytes,
|
|
72
|
-
# and the queue eventually fills and sheds live telemetry instead.
|
|
73
|
-
"export_expiry" => [ 5.minutes, 10.minutes, lambda { |env|
|
|
74
|
-
next unless Railwatch.config.export?
|
|
75
|
-
|
|
76
|
-
outbox = Export::Outbox.new(Railwatch.config, env)
|
|
77
|
-
outbox.expire!
|
|
78
|
-
outbox.prune!
|
|
79
|
-
} ],
|
|
80
|
-
# PASSIVE, not TRUNCATE: this runs inside a Puma worker, and a
|
|
81
|
-
# truncating checkpoint blocks every reader and writer on the file.
|
|
82
|
-
"prune" => [ 24.hours, 60.minutes, lambda { |env|
|
|
83
|
-
PruneTelemetryJob.new.perform(env, checkpoint: "PASSIVE")
|
|
84
|
-
OptimizeTelemetryJob.new.perform(env)
|
|
85
|
-
FollowupReceipt.prune!
|
|
86
|
-
} ]
|
|
87
|
-
}.freeze
|
|
88
|
-
|
|
89
|
-
@mutex = Mutex.new
|
|
90
|
-
@wakeup = ConditionVariable.new
|
|
91
|
-
@thread = nil
|
|
92
|
-
@pid = nil
|
|
93
|
-
@stopping = false
|
|
94
|
-
|
|
95
|
-
module_function
|
|
96
|
-
|
|
97
|
-
# Idempotent; Railwatch.restart_after_fork! calls it again in every forked
|
|
98
|
-
# child.
|
|
99
|
-
def start!
|
|
100
|
-
return unless Railwatch.enabled? && Railwatch.config.local?
|
|
101
|
-
return if defined?(Rails) && Rails.env.test?
|
|
102
|
-
return unless ROLES.include?(Subscribers::ProcessInfo.role)
|
|
103
|
-
# With a writer process listening, it is the one clock. The lease table
|
|
104
|
-
# would keep two clocks honest, but there is no reason to run a second.
|
|
105
|
-
return if !Writer.running? && Writer.listening?
|
|
106
|
-
return if @thread&.alive? && @pid == Process.pid
|
|
107
|
-
|
|
108
|
-
@mutex.synchronize do
|
|
109
|
-
return if @thread&.alive? && @pid == Process.pid
|
|
110
|
-
|
|
111
|
-
@pid = Process.pid
|
|
112
|
-
@stopping = false
|
|
113
|
-
@thread = Thread.new { run }
|
|
114
|
-
@thread.name = "railwatch-maintenance"
|
|
115
|
-
@thread.abort_on_exception = false
|
|
116
|
-
@thread.report_on_exception = false
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# A forked child inherits a dead thread and may inherit a mutex held by a
|
|
121
|
-
# vanished parent thread; every primitive is replaced before start!.
|
|
122
|
-
def restart_after_fork!
|
|
123
|
-
@mutex = Mutex.new
|
|
124
|
-
@wakeup = ConditionVariable.new
|
|
125
|
-
@thread = nil
|
|
126
|
-
@pid = nil
|
|
127
|
-
@stopping = false
|
|
128
|
-
start!
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def stop!
|
|
132
|
-
return unless @thread
|
|
133
|
-
|
|
134
|
-
@stopping = true
|
|
135
|
-
@mutex.synchronize { @wakeup.signal }
|
|
136
|
-
@thread.join(1)
|
|
137
|
-
@thread = nil
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def run
|
|
141
|
-
until @stopping
|
|
142
|
-
@mutex.synchronize { @wakeup.wait(@mutex, TICK) unless @stopping }
|
|
143
|
-
tick unless @stopping
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Runs every task that is due and unclaimed. Returns the names it ran.
|
|
148
|
-
# Public so a spec, or an operator in a console, can drive the clock by
|
|
149
|
-
# hand. One failing task is reported and does not stop the others.
|
|
150
|
-
#
|
|
151
|
-
# A web worker's clock starts at boot, before the Puma plugin has forked
|
|
152
|
-
# the writer, so the start-time check in start! cannot see it. Checked
|
|
153
|
-
# again on every tick: once a writer is listening this process's clock
|
|
154
|
-
# stands down and stays down (the lease table would keep both honest,
|
|
155
|
-
# but there is no reason to run a second one).
|
|
156
|
-
def tick(now: Time.current)
|
|
157
|
-
return [] if !Writer.running? && Writer.listening?
|
|
158
|
-
|
|
159
|
-
ran = []
|
|
160
|
-
Rails.application.executor.wrap do
|
|
161
|
-
env = Environment.current
|
|
162
|
-
TASKS.each do |name, (every, lease, body)|
|
|
163
|
-
token = MaintenanceTask.claim(name, every: every, lease: lease, owner: owner, now: now) or next
|
|
164
|
-
|
|
165
|
-
succeeded = false
|
|
166
|
-
begin
|
|
167
|
-
Railwatch.ignore { body.call(env) }
|
|
168
|
-
succeeded = true
|
|
169
|
-
ran << name
|
|
170
|
-
rescue StandardError => e
|
|
171
|
-
# Not Rails.error: this thread has no execution for Railwatch.ignore
|
|
172
|
-
# to pause, so a report there would be captured by Railwatch's own
|
|
173
|
-
# subscriber and opened as an application issue about Railwatch.
|
|
174
|
-
Railwatch.debug { "maintenance #{name} failed: #{e.class}: #{e.message}" }
|
|
175
|
-
Railwatch.notify_unrecoverable(TaskError.new("maintenance task #{name} failed: #{e.class}: #{e.message}"))
|
|
176
|
-
ensure
|
|
177
|
-
MaintenanceTask.release(name, token: token, ran_at: now, succeeded: succeeded)
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
ran
|
|
182
|
-
rescue StandardError => e
|
|
183
|
-
# The lease table not being migrated yet is the usual way to get here.
|
|
184
|
-
Railwatch.debug { "maintenance tick failed: #{e.class}: #{e.message}" }
|
|
185
|
-
ran
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def owner
|
|
189
|
-
"#{Railwatch.config.server}:#{Process.pid}"
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
class TaskError < StandardError; end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
module Patches
|
|
5
|
-
# Gives the migration connection to a Railwatch database a long SQLite
|
|
6
|
-
# busy timeout, for the migration only.
|
|
7
|
-
#
|
|
8
|
-
# A deploy migrates while the previous release is still serving, and on
|
|
9
|
-
# an embedded install that release's writer process is committing
|
|
10
|
-
# telemetry to the same file the whole time. Each of its transactions
|
|
11
|
-
# takes SQLite's single write lock: about a second on a warm 15 GB file
|
|
12
|
-
# (the export sender's claim scans the destination's whole delivery
|
|
13
|
-
# history), several seconds while a deploy's image build has the disk.
|
|
14
|
-
# The migration connection waited only the database's configured
|
|
15
|
-
# `timeout` -- 5 s in the generated database.yml -- and then raised
|
|
16
|
-
# SQLite3::BusyException, which db:prepare reports as a failed migration
|
|
17
|
-
# and the container entrypoint as a failed boot. That is how a deploy of
|
|
18
|
-
# rebulk-system crash-looped eight times without one migration running.
|
|
19
|
-
#
|
|
20
|
-
# The configured timeout is the right one for the app's own requests,
|
|
21
|
-
# which should fail fast rather than queue behind ingest. A migration is
|
|
22
|
-
# the opposite: it runs once, before the server binds, and waiting is the
|
|
23
|
-
# only way through. So only the connection Active Record migrates with is
|
|
24
|
-
# changed, only while it migrates, and only for a database whose
|
|
25
|
-
# migrations_paths include this gem's. The host's own databases keep
|
|
26
|
-
# their timeouts.
|
|
27
|
-
module MigrationBusyTimeout
|
|
28
|
-
def migrate(*, **)
|
|
29
|
-
pool = migration_connection_pool
|
|
30
|
-
raw = Railwatch::Patches::MigrationBusyTimeout.lengthen(pool)
|
|
31
|
-
super
|
|
32
|
-
ensure
|
|
33
|
-
Railwatch::Patches::MigrationBusyTimeout.restore(pool, raw) if raw
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
class << self
|
|
37
|
-
# The raw SQLite connection whose timeout was lengthened, or nil when
|
|
38
|
-
# this is not a Railwatch database (or not SQLite).
|
|
39
|
-
def lengthen(pool)
|
|
40
|
-
return unless railwatch_database?(pool.db_config)
|
|
41
|
-
|
|
42
|
-
raw = pool.lease_connection.raw_connection
|
|
43
|
-
return unless raw.respond_to?(:busy_handler_timeout=)
|
|
44
|
-
|
|
45
|
-
raw.busy_handler_timeout = (Railwatch.config.migration_busy_timeout.to_f * 1000).to_i
|
|
46
|
-
raw
|
|
47
|
-
rescue StandardError => e
|
|
48
|
-
Railwatch.debug { "migration busy timeout not applied: #{e.class}: #{e.message}" }
|
|
49
|
-
nil
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Back to what database.yml says, so a connection that outlives the
|
|
53
|
-
# migration (db:migrate in a long-lived process, a spec) behaves as
|
|
54
|
-
# configured afterwards.
|
|
55
|
-
def restore(pool, raw)
|
|
56
|
-
timeout = pool.db_config.configuration_hash[:timeout]
|
|
57
|
-
if timeout
|
|
58
|
-
raw.busy_handler_timeout = Integer(timeout)
|
|
59
|
-
else
|
|
60
|
-
raw.busy_handler(nil)
|
|
61
|
-
end
|
|
62
|
-
rescue StandardError => e
|
|
63
|
-
Railwatch.debug { "migration busy timeout not restored: #{e.class}: #{e.message}" }
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def railwatch_database?(db_config)
|
|
67
|
-
ours = %i[railwatch railwatch_telemetry].map { |name| File.expand_path(Railwatch.migrations_path(name)) }
|
|
68
|
-
Array(db_config.migrations_paths).any? { |path| ours.include?(File.expand_path(path.to_s, Rails.root.to_s)) }
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
module Transport
|
|
5
|
-
# Writes each batch straight into the engine's telemetry database instead
|
|
6
|
-
# of POSTing it. Same Reporter, same buffer, same backpressure; the only
|
|
7
|
-
# difference from Transport::Http is that deliver ends in Ingest::Batch
|
|
8
|
-
# rather than Net::HTTP. Runs on the reporter thread, never on a request.
|
|
9
|
-
class Local
|
|
10
|
-
Result = Struct.new(:ok, :status, :accepted, :rejected, :rejections, :error, :retryable_error, keyword_init: true) do
|
|
11
|
-
def retryable? = retryable_error ? true : false
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def initialize(config)
|
|
15
|
-
@config = config
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def deliver(records, dropped: 0, dropped_bytes: 0, backpressure_factor: 1.0, batch_id: nil)
|
|
19
|
-
wire = stringify(records)
|
|
20
|
-
# The reporter thread is not a request or a job: nothing has set up
|
|
21
|
-
# an ExecutionContext for it. Query log tags, Rails.error.report and
|
|
22
|
-
# connection checkin all assume one, so run the write inside the
|
|
23
|
-
# executor the same way a job would.
|
|
24
|
-
#
|
|
25
|
-
# The rescue is INSIDE the wrap. The executor reports any exception
|
|
26
|
-
# that escapes its block to Rails.error before re-raising it, and
|
|
27
|
-
# Railwatch subscribes to Rails.error: a write that failed (the
|
|
28
|
-
# telemetry database not migrated yet, a locked file) would otherwise
|
|
29
|
-
# be captured as one of the application's own exceptions and opened
|
|
30
|
-
# as an issue about Railwatch, by Railwatch.
|
|
31
|
-
Rails.application.executor.wrap do
|
|
32
|
-
write(wire, records.size, dropped: dropped, backpressure_factor: backpressure_factor, batch_id: batch_id)
|
|
33
|
-
rescue StandardError => e
|
|
34
|
-
Railwatch.debug { "local ingest failed: #{e.class}: #{e.message}" }
|
|
35
|
-
# With a batch id the reporter can safely retry: the ledger says
|
|
36
|
-
# whether the write committed, and the unique execution_id index
|
|
37
|
-
# makes a replay of a half-visible batch a no-op. Without one (an
|
|
38
|
-
# older caller) the batch is consumed, since a retry could
|
|
39
|
-
# double-insert. A malformed record never gets here: the mapper
|
|
40
|
-
# counts it as rejected and the rest of the batch is written.
|
|
41
|
-
Result.new(ok: false, error: "#{e.class}: #{e.message}", retryable_error: !batch_id.nil?)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def ping = true
|
|
46
|
-
def unauthorized? = false
|
|
47
|
-
def reset_after_fork! = self
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def write(wire, count, dropped:, backpressure_factor:, batch_id:)
|
|
52
|
-
environment = Environment.current
|
|
53
|
-
# The same contract the HTTP path gets from X-Railwatch-Batch-Id: a
|
|
54
|
-
# batch the reporter retries after a failure is written once. The
|
|
55
|
-
# ledger row is created inside the batch transaction, so its presence
|
|
56
|
-
# is the commit.
|
|
57
|
-
if (ledger = environment.with_telemetry { Ingest::Batch.committed(batch_id) })
|
|
58
|
-
return Result.new(ok: true, status: 200, accepted: ledger.accepted, rejected: ledger.rejected, rejections: [])
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
result = Ingest::Batch.new(environment, wire, dropped_by_client: dropped, backpressure_factor: backpressure_factor,
|
|
62
|
-
gem_version: Railwatch::VERSION, embedded: true, batch_id: batch_id).write!
|
|
63
|
-
Result.new(ok: true, status: 200, accepted: result.accepted, rejected: result.rejected,
|
|
64
|
-
rejections: result.rejections.first(10))
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# The gem builds symbol-keyed records, nested hashes included (stages,
|
|
68
|
-
# counters, headers); the mapper reads string keys throughout and
|
|
69
|
-
# rejects a symbol-keyed structure. A JSON round trip is the deep
|
|
70
|
-
# stringify. Measured against the alternatives on a 500-record batch:
|
|
71
|
-
# 1.9 ms and 11k allocations here versus 4.8 ms and 25k for
|
|
72
|
-
# deep_transform_keys, which also leaves Symbol values as Symbols.
|
|
73
|
-
def stringify(records)
|
|
74
|
-
JSON.parse(JSON.generate(records))
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "zlib"
|
|
5
|
-
require "json"
|
|
6
|
-
|
|
7
|
-
module Railwatch
|
|
8
|
-
module Transport
|
|
9
|
-
# Hands each batch to the embedded writer process (Railwatch::Writer) over
|
|
10
|
-
# a Unix socket instead of writing it into SQLite on this thread. The
|
|
11
|
-
# workers' Ruby threads then never map a record, hold SQLite's write lock,
|
|
12
|
-
# or run a t-digest; all of that happens in one process whose GVL no
|
|
13
|
-
# request shares. Same Reporter, same buffer, same batch id, same ledger:
|
|
14
|
-
# a batch the writer already committed replays as a no-op there.
|
|
15
|
-
#
|
|
16
|
-
# One request per connection, length-prefixed, gzip JSON both ways:
|
|
17
|
-
# > [u32 length][gzip(JSON {batch_id, records, dropped, dropped_bytes, backpressure_factor})]
|
|
18
|
-
# < [u32 length][JSON {ok, status, accepted, rejected, rejections, error, retryable_error}]
|
|
19
|
-
#
|
|
20
|
-
# Whether a writer is EXPECTED decides what a missing one means. Under
|
|
21
|
-
# the Puma plugin (which sets Writer.expected! in the master before it
|
|
22
|
-
# forks the workers) a socket that is absent or refusing is a writer
|
|
23
|
-
# that is starting or restarting: the batch is retained and retried on
|
|
24
|
-
# the reporter's backoff ladder, which is bounded -- after
|
|
25
|
-
# Reporter::MAX_RETRY_ATTEMPTS the batch is counted as dropped and
|
|
26
|
-
# reported through on_unrecoverable, rather than held for ever. Anywhere else
|
|
27
|
-
# (a plain `rails server`, a runner, a Solid Queue worker, the suite) no
|
|
28
|
-
# writer will ever appear, so the first miss switches this transport to
|
|
29
|
-
# an in-process Transport::Local for the rest of the process's life and
|
|
30
|
-
# embedded mode works exactly as it did before the writer existed. A
|
|
31
|
-
# refusing socket in that case is a stale inode from a dead writer and
|
|
32
|
-
# falls back the same way rather than dropping batches forever.
|
|
33
|
-
class Socket
|
|
34
|
-
Result = Local::Result
|
|
35
|
-
MAX_REPLY_BYTES = 1 << 20
|
|
36
|
-
# How long an in-process fallback lasts before the socket is tried
|
|
37
|
-
# again. The fallback is provisional on purpose: a process that missed
|
|
38
|
-
# the writer once (a bare `puma` whose plugin could not mark one
|
|
39
|
-
# expected, a worker forked before the writer bound) would otherwise
|
|
40
|
-
# write its own batches for the rest of its life.
|
|
41
|
-
FALLBACK_RECHECK = 30
|
|
42
|
-
# How long a process that EXPECTS a writer keeps retaining batches for
|
|
43
|
-
# one that never answers before it writes them itself. A writer that is
|
|
44
|
-
# restarting is back in seconds; one that cannot bind at all (an
|
|
45
|
-
# unwritable socket directory, a fork that keeps failing) would
|
|
46
|
-
# otherwise retain until the reporter's retry cap and then drop the
|
|
47
|
-
# batch. Writing it here instead keeps the records.
|
|
48
|
-
WRITER_GRACE = 60
|
|
49
|
-
|
|
50
|
-
def initialize(config, path: nil, expected: nil)
|
|
51
|
-
@config = config
|
|
52
|
-
@path = path || config.writer_socket_path
|
|
53
|
-
@expected = expected
|
|
54
|
-
# A path the kernel cannot bind (over 108 bytes on Linux) can never
|
|
55
|
-
# have a writer behind it; do not spend a batch finding out, and do
|
|
56
|
-
# not keep re-checking it either.
|
|
57
|
-
@unusable_path = !Writer.usable_path?(@path)
|
|
58
|
-
@fallback = @unusable_path ? Local.new(config) : nil
|
|
59
|
-
@fallback_at = Clock.monotonic if @fallback
|
|
60
|
-
Railwatch.debug { "writer socket path #{@path.inspect} is too long; writing batches in-process" } if @fallback
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
attr_reader :path
|
|
64
|
-
|
|
65
|
-
def fallback? = !@fallback.nil?
|
|
66
|
-
|
|
67
|
-
# Read when a batch misses, not when the transport is built: under
|
|
68
|
-
# `rails server` the app boots (and the reporter picks this transport)
|
|
69
|
-
# before Puma evaluates config/puma.rb, so the plugin's Writer.expected!
|
|
70
|
-
# lands after construction, and the workers inherit a copy of this
|
|
71
|
-
# object at fork. A flag captured here at construction was always false
|
|
72
|
-
# in every worker, and the first flush before the writer was listening
|
|
73
|
-
# fell back to in-process writes for the life of the worker.
|
|
74
|
-
def expected? = @expected.nil? ? Writer.expected? : @expected
|
|
75
|
-
|
|
76
|
-
def deliver(records, dropped: 0, dropped_bytes: 0, backpressure_factor: 1.0, batch_id: nil)
|
|
77
|
-
return @fallback.deliver(records, dropped: dropped, dropped_bytes: dropped_bytes,
|
|
78
|
-
backpressure_factor: backpressure_factor, batch_id: batch_id) if falling_back?
|
|
79
|
-
|
|
80
|
-
payload = Zlib.gzip(JSON.generate(batch_id: batch_id, records: records, dropped: dropped,
|
|
81
|
-
dropped_bytes: dropped_bytes, backpressure_factor: backpressure_factor))
|
|
82
|
-
reply = exchange(payload)
|
|
83
|
-
Result.new(**reply.slice("ok", "status", "accepted", "rejected", "rejections", "error", "retryable_error").transform_keys(&:to_sym))
|
|
84
|
-
rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTSOCK => e
|
|
85
|
-
@missing_since ||= Clock.monotonic
|
|
86
|
-
if expected? && Clock.monotonic - @missing_since < WRITER_GRACE
|
|
87
|
-
Railwatch.debug { "writer not answering at #{@path} (#{e.class}); retaining the batch" }
|
|
88
|
-
return Result.new(ok: false, error: "writer unavailable: #{e.message}", retryable_error: true)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
Railwatch.debug do
|
|
92
|
-
if expected?
|
|
93
|
-
"no writer at #{@path} after #{WRITER_GRACE}s (#{e.class}); writing batches in-process, re-checking every #{FALLBACK_RECHECK}s"
|
|
94
|
-
else
|
|
95
|
-
"no writer at #{@path} (#{e.class}) and none expected; writing batches in-process, re-checking every #{FALLBACK_RECHECK}s"
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
@fallback = Local.new(@config)
|
|
99
|
-
@fallback_at = Clock.monotonic
|
|
100
|
-
deliver(records, dropped: dropped, dropped_bytes: dropped_bytes, backpressure_factor: backpressure_factor, batch_id: batch_id)
|
|
101
|
-
rescue SystemCallError, IOError, Zlib::Error, JSON::ParserError, Timeout::Error => e
|
|
102
|
-
Railwatch.debug { "writer delivery failed: #{e.class}: #{e.message}" }
|
|
103
|
-
Result.new(ok: false, error: "#{e.class}: #{e.message}", retryable_error: true)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Whether this batch goes in-process. A fallback for an unbindable path
|
|
107
|
-
# is permanent; any other one is re-examined every FALLBACK_RECHECK
|
|
108
|
-
# seconds, so a writer that turns up later takes the work back.
|
|
109
|
-
def falling_back?
|
|
110
|
-
return false if @fallback.nil?
|
|
111
|
-
return true if @unusable_path || Clock.monotonic - @fallback_at < FALLBACK_RECHECK
|
|
112
|
-
|
|
113
|
-
if Writer.listening?(@path)
|
|
114
|
-
Railwatch.debug { "writer is answering at #{@path} again; handing batches back to it" }
|
|
115
|
-
@fallback = nil
|
|
116
|
-
@missing_since = nil
|
|
117
|
-
false
|
|
118
|
-
else
|
|
119
|
-
@fallback_at = Clock.monotonic
|
|
120
|
-
true
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def ping
|
|
125
|
-
return true if @fallback
|
|
126
|
-
|
|
127
|
-
UNIXSocket.new(@path).close
|
|
128
|
-
true
|
|
129
|
-
rescue SystemCallError, ArgumentError
|
|
130
|
-
false
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def unauthorized? = false
|
|
134
|
-
def reset_after_fork! = self
|
|
135
|
-
|
|
136
|
-
private
|
|
137
|
-
|
|
138
|
-
# Every read and write is under one deadline of config.timeout, the
|
|
139
|
-
# same budget the HTTP transport gives a POST; a writer that accepts but
|
|
140
|
-
# stops reading cannot hold the reporter thread past it.
|
|
141
|
-
def exchange(payload)
|
|
142
|
-
UNIXSocket.open(@path) do |sock|
|
|
143
|
-
deadline = Clock.monotonic + @config.timeout
|
|
144
|
-
write_exactly(sock, [ payload.bytesize ].pack("N") + payload, deadline)
|
|
145
|
-
header = read_exactly(sock, 4, deadline)
|
|
146
|
-
length = header.unpack1("N")
|
|
147
|
-
raise IOError, "writer reply too large (#{length} bytes)" if length > MAX_REPLY_BYTES
|
|
148
|
-
|
|
149
|
-
JSON.parse(read_exactly(sock, length, deadline))
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def write_exactly(sock, data, deadline)
|
|
154
|
-
offset = 0
|
|
155
|
-
while offset < data.bytesize
|
|
156
|
-
remaining = deadline - Clock.monotonic
|
|
157
|
-
raise Timeout::Error, "writer did not accept the batch within #{@config.timeout}s" if remaining <= 0
|
|
158
|
-
raise IOError, "writer closed the connection" unless sock.wait_writable(remaining)
|
|
159
|
-
|
|
160
|
-
written = sock.write_nonblock(data.byteslice(offset..), exception: false)
|
|
161
|
-
offset += written if written.is_a?(Integer)
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def read_exactly(sock, count, deadline)
|
|
166
|
-
buffer = +""
|
|
167
|
-
while buffer.bytesize < count
|
|
168
|
-
remaining = deadline - Clock.monotonic
|
|
169
|
-
raise Timeout::Error, "writer did not answer within #{@config.timeout}s" if remaining <= 0
|
|
170
|
-
raise IOError, "writer closed the connection" unless sock.wait_readable(remaining)
|
|
171
|
-
|
|
172
|
-
chunk = sock.read_nonblock(count - buffer.bytesize, exception: false)
|
|
173
|
-
case chunk
|
|
174
|
-
when :wait_readable then next
|
|
175
|
-
when nil then raise IOError, "writer closed the connection"
|
|
176
|
-
else buffer << chunk
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
buffer
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|