railwatch 0.9.4 → 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 +50 -0
- data/README.md +51 -60
- data/config/routes.rb +0 -59
- data/docs/ai-and-mcp.md +9 -8
- data/docs/configuration.md +35 -66
- 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/configuration.rb +21 -151
- data/lib/railwatch/engine.rb +9 -118
- data/lib/railwatch/patches.rb +0 -11
- data/lib/railwatch/reporter.rb +3 -19
- data/lib/railwatch/subscribers/process_info.rb +1 -2
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch.rb +4 -62
- 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
|
@@ -183,20 +183,11 @@ module Railwatch
|
|
|
183
183
|
# where the command patch starts the execution, and sampling that
|
|
184
184
|
# execution builds the reporter -- before the task's `:environment`
|
|
185
185
|
# prerequisite boots Rails and runs config/initializers. A token in the
|
|
186
|
-
# environment is enough for Railwatch to be enabled that early
|
|
187
|
-
# embedded app gets a reporter aimed at HTTP, chosen from a config that
|
|
188
|
-
# had not yet been told the app is embedded. Left alone it stays aimed
|
|
189
|
-
# there: the memo outlives the configuration that produced it, and every
|
|
190
|
-
# record from that process goes to the receiver instead of the app's own
|
|
191
|
-
# database, skipping the export queue and the replay receipts it earns.
|
|
186
|
+
# environment is enough for Railwatch to be enabled that early.
|
|
192
187
|
#
|
|
193
188
|
# Nothing has been recorded at that point -- starting an execution
|
|
194
189
|
# decides sampling and pushes nothing -- so deciding again here is free.
|
|
195
190
|
def adopt_final_config!
|
|
196
|
-
# The swap forked_transport already makes, for the same reason: the
|
|
197
|
-
# situation this transport was chosen for is not the situation now.
|
|
198
|
-
reselected = Railwatch.local_transport if @config.local?
|
|
199
|
-
@transport = reselected if reselected && reselected.class != @transport.class
|
|
200
191
|
# Sized from the config as it stood, which was the default one. Safe
|
|
201
192
|
# only while the buffer is still empty: resizing means a new buffer,
|
|
202
193
|
# and anything already in the old one would go with it.
|
|
@@ -248,12 +239,6 @@ module Railwatch
|
|
|
248
239
|
end
|
|
249
240
|
|
|
250
241
|
def forked_transport
|
|
251
|
-
# A child may be a different kind of process from its parent: the
|
|
252
|
-
# writer forked from a Puma master must write SQLite itself, not hand
|
|
253
|
-
# batches back to the socket it is about to serve.
|
|
254
|
-
reselected = Railwatch.local_transport if @config.local?
|
|
255
|
-
return reselected if reselected && reselected.class != @transport.class
|
|
256
|
-
|
|
257
242
|
transport = @transport.dup
|
|
258
243
|
transport.reset_after_fork! if transport.respond_to?(:reset_after_fork!)
|
|
259
244
|
transport
|
|
@@ -447,9 +432,8 @@ module Railwatch
|
|
|
447
432
|
end
|
|
448
433
|
return unless gave_up
|
|
449
434
|
|
|
450
|
-
# Losing a batch is not a debug-level event: with an ingest
|
|
451
|
-
#
|
|
452
|
-
# 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
|
|
453
437
|
# rides on the NEXT successful delivery, which may never happen.
|
|
454
438
|
#
|
|
455
439
|
# Reported here, after the lock is released, never inside it. The
|
|
@@ -89,8 +89,7 @@ module Railwatch
|
|
|
89
89
|
# $PROGRAM_NAME after boot and would otherwise turn the supervisor,
|
|
90
90
|
# dispatcher, and scheduler into "web" on every health sample.
|
|
91
91
|
def role
|
|
92
|
-
if defined?(
|
|
93
|
-
elsif defined?(::SolidQueue) && ($PROGRAM_NAME.include?("jobs") || $PROGRAM_NAME.start_with?("solid-queue-") || ARGV.first.to_s.start_with?("solid_queue:")) then "worker"
|
|
92
|
+
if defined?(::SolidQueue) && ($PROGRAM_NAME.include?("jobs") || $PROGRAM_NAME.start_with?("solid-queue-") || ARGV.first.to_s.start_with?("solid_queue:")) then "worker"
|
|
94
93
|
elsif defined?(::Rails::Console) then "console"
|
|
95
94
|
elsif $PROGRAM_NAME.end_with?("rake") then "command"
|
|
96
95
|
elsif defined?(::Puma) then "web"
|
data/lib/railwatch/version.rb
CHANGED
data/lib/railwatch.rb
CHANGED
|
@@ -21,19 +21,9 @@ require "railwatch/current"
|
|
|
21
21
|
require "railwatch/record"
|
|
22
22
|
require "railwatch/buffer"
|
|
23
23
|
require "railwatch/transport/wire_encoder"
|
|
24
|
-
require "railwatch/export/policy"
|
|
25
|
-
require "railwatch/export/lease"
|
|
26
|
-
require "railwatch/export/outbox"
|
|
27
|
-
require "railwatch/export/client"
|
|
28
|
-
require "railwatch/export/sender"
|
|
29
24
|
require "railwatch/transport/http"
|
|
30
|
-
require "railwatch/transport/local"
|
|
31
|
-
require "railwatch/transport/socket"
|
|
32
|
-
require "railwatch/writer"
|
|
33
25
|
require "railwatch/authentication"
|
|
34
26
|
require "railwatch/json_compat"
|
|
35
|
-
require "railwatch/embedded"
|
|
36
|
-
require "railwatch/ingest_request_body_limit"
|
|
37
27
|
require "railwatch/reporter"
|
|
38
28
|
require "railwatch/sampler"
|
|
39
29
|
require "railwatch/redactor"
|
|
@@ -69,7 +59,7 @@ module Railwatch
|
|
|
69
59
|
end
|
|
70
60
|
|
|
71
61
|
def reporter
|
|
72
|
-
@reporter ||= Reporter.new(config
|
|
62
|
+
@reporter ||= Reporter.new(config)
|
|
73
63
|
end
|
|
74
64
|
|
|
75
65
|
# Called once config/initializers has run, so a reporter built before
|
|
@@ -87,16 +77,6 @@ module Railwatch
|
|
|
87
77
|
@reporter&.adopt_final_config!
|
|
88
78
|
end
|
|
89
79
|
|
|
90
|
-
# Embedded mode: a Puma worker hands its batches to the writer process
|
|
91
|
-
# over the socket; the writer itself, and any process when no socket is
|
|
92
|
-
# configured, writes them straight into SQLite. nil means HTTP.
|
|
93
|
-
def local_transport
|
|
94
|
-
return nil unless config.local?
|
|
95
|
-
return Transport::Local.new(config) if Writer.running? || config.writer_socket_path.nil?
|
|
96
|
-
|
|
97
|
-
Transport::Socket.new(config)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
80
|
def redactor
|
|
101
81
|
@redactor ||= Redactor.new(config)
|
|
102
82
|
end
|
|
@@ -124,9 +104,7 @@ module Railwatch
|
|
|
124
104
|
Subscribers::Users.restart_after_fork!
|
|
125
105
|
Subscribers::ProcessInfo.restart_after_fork!
|
|
126
106
|
Health.restart_after_fork!
|
|
127
|
-
Export::Sender.restart_after_fork!
|
|
128
107
|
Sessions.restart_after_fork!
|
|
129
|
-
Maintenance.restart_after_fork!
|
|
130
108
|
end
|
|
131
109
|
|
|
132
110
|
# --- execution lifecycle -------------------------------------------------
|
|
@@ -619,45 +597,16 @@ module Railwatch
|
|
|
619
597
|
end
|
|
620
598
|
|
|
621
599
|
require "railwatch/subscribers"
|
|
622
|
-
require "railwatch/dashboard_assets"
|
|
623
600
|
|
|
624
601
|
module Railwatch
|
|
625
|
-
#
|
|
626
|
-
#
|
|
627
|
-
|
|
628
|
-
# every gem update, from the gem's own history, the way Active Storage
|
|
629
|
-
# migrates its tables. Nothing is copied into the app.
|
|
630
|
-
def self.migrations_path(database)
|
|
631
|
-
File.expand_path("../db/#{database}_migrate", __dir__)
|
|
632
|
-
end
|
|
633
|
-
|
|
634
|
-
# Where links the models build (a trace from an execution page, a saved
|
|
635
|
-
# view's page) resolve. The engine's own routes serve the embedded
|
|
636
|
-
# dashboard; a host that mounts these pages on its own routes, as
|
|
637
|
-
# Railwatch Cloud does, points this at them.
|
|
638
|
-
def self.url_helpers
|
|
639
|
-
@url_helpers || Engine.routes.url_helpers
|
|
640
|
-
end
|
|
641
|
-
|
|
642
|
-
def self.url_helpers=(helpers)
|
|
643
|
-
@url_helpers = helpers
|
|
644
|
-
end
|
|
645
|
-
|
|
646
|
-
# The one browser client, relative to the gem root. The dashboard bundle
|
|
647
|
-
# is built from it and the install generator copies it into a host.
|
|
648
|
-
BROWSER_CLIENT = "app/frontend/lib/railwatch.ts"
|
|
602
|
+
# The Inertia browser client, relative to the gem root. The install
|
|
603
|
+
# generator copies it into a host's app/frontend/lib.
|
|
604
|
+
BROWSER_CLIENT = "lib/generators/railwatch/install/templates/railwatch.ts"
|
|
649
605
|
|
|
650
606
|
def self.browser_client_path
|
|
651
607
|
File.expand_path("../#{BROWSER_CLIENT}", __dir__)
|
|
652
608
|
end
|
|
653
609
|
|
|
654
|
-
# The dashboard's frontend source. Railwatch Cloud builds its bundle from
|
|
655
|
-
# this directory plus its own hosting pages, so there is one copy of every
|
|
656
|
-
# dashboard page and component.
|
|
657
|
-
def self.frontend_path
|
|
658
|
-
File.expand_path("../app/frontend", __dir__)
|
|
659
|
-
end
|
|
660
|
-
|
|
661
610
|
# One realistic wire record per record type, exactly as the gem's own
|
|
662
611
|
# subscribers emit it, generated by spec/railwatch/wire_fixtures_spec.rb.
|
|
663
612
|
# A receiver tests its mapper against these instead of hand-writing what
|
|
@@ -669,12 +618,5 @@ module Railwatch
|
|
|
669
618
|
def self.wire_fixtures
|
|
670
619
|
@wire_fixtures ||= JSON.parse(File.read(wire_fixtures_path)).freeze
|
|
671
620
|
end
|
|
672
|
-
|
|
673
|
-
# Raised when the engine's models are used in an app whose database.yml
|
|
674
|
-
# has no entry for them. They must never fall back to the host's primary
|
|
675
|
-
# connection: the telemetry tables are unprefixed (`sessions`, `visits`,
|
|
676
|
-
# `people`, `notifications`, `logs`), so a query there would read, and a
|
|
677
|
-
# write would corrupt, the application's own tables.
|
|
678
|
-
class DatabaseNotConfigured < StandardError; end
|
|
679
621
|
end
|
|
680
622
|
require "railwatch/engine" if defined?(Rails::Engine)
|