rails_error_dashboard 0.14.2 → 0.14.3
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/README.md +4 -7
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +27 -23
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +14 -16
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +13 -7
- data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +41 -21
- data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +68 -64
- data/lib/rails_error_dashboard/commands/drop_all_tables.rb +77 -0
- data/lib/rails_error_dashboard/configuration.rb +33 -2
- data/lib/rails_error_dashboard/logger.rb +11 -3
- data/lib/rails_error_dashboard/queries/uninstall_plan.rb +63 -0
- data/lib/rails_error_dashboard/services/solid_queue_config_check.rb +214 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +3 -0
- data/lib/tasks/error_dashboard.rake +34 -3
- data/lib/tasks/rails_error_dashboard_tasks.rake +37 -43
- metadata +5 -3
- data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +0 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f96e195659bca5562fcf7c4ed02c1f8f69c058daec06f6902523958f5746a8a4
|
|
4
|
+
data.tar.gz: 2ea940fe412652bef9e0f47eb818d4da93a6d7b7cb1e32cd5820e96cf032fafe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7b8a36c5ffde9bcd71a0649a0d2df111345625717ec67a997e4bd4929215700408efd95844b41506e3a48aa47fc7179cd1df140aa1274451a1a8760a5f32db7
|
|
7
|
+
data.tar.gz: d6b3722433ce12451ef44242874dfe98620441832626368d0e8ce9d852396d8e4b00b5cc5773f04a348d48f64254fc763cd4fae4a1580d5b399cb4aab21f3de3
|
data/README.md
CHANGED
|
@@ -635,9 +635,9 @@ The installer guides you through optional feature selection — notifications, p
|
|
|
635
635
|
http://localhost:3000/red
|
|
636
636
|
```
|
|
637
637
|
|
|
638
|
-
Default credentials: `gandalf` / `youshallnotpass
|
|
638
|
+
Default credentials: `gandalf` / `youshallnotpass`, for development and test only.
|
|
639
639
|
|
|
640
|
-
**
|
|
640
|
+
**Before you deploy anywhere else**, set `ERROR_DASHBOARD_USER` and `ERROR_DASHBOARD_PASSWORD` in that environment. Outside development and test, the app refuses to boot on the defaults. See [Dashboard Credentials](docs/guides/CONFIGURATION.md#dashboard-credentials).
|
|
641
641
|
|
|
642
642
|
### 4. Test it out
|
|
643
643
|
|
|
@@ -654,11 +654,8 @@ raise "Test error from Rails Error Dashboard!"
|
|
|
654
654
|
|
|
655
655
|
```ruby
|
|
656
656
|
RailsErrorDashboard.configure do |config|
|
|
657
|
-
#
|
|
658
|
-
|
|
659
|
-
config.dashboard_password = ENV.fetch('ERROR_DASHBOARD_PASSWORD', 'youshallnotpass')
|
|
660
|
-
|
|
661
|
-
# Or use your existing auth (Devise, Warden, etc.):
|
|
657
|
+
# Dashboard login: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD outside
|
|
658
|
+
# development and test (the gem reads them itself). Or use your existing auth:
|
|
662
659
|
# config.authenticate_with = -> { warden.authenticated? }
|
|
663
660
|
|
|
664
661
|
# Optional features — enable as needed
|
|
@@ -33,6 +33,32 @@ module RailsErrorDashboard
|
|
|
33
33
|
)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# Deletes error logs and everything that references them: dependents first,
|
|
37
|
+
# all in batches. Occurrences, comments and cascade patterns hold foreign
|
|
38
|
+
# keys to the log, so deleting the logs first fails, and one unbounded
|
|
39
|
+
# DELETE would lock the table. Public because error_dashboard:cleanup_resolved
|
|
40
|
+
# deletes the same way.
|
|
41
|
+
#
|
|
42
|
+
# @param scope [ActiveRecord::Relation] the ErrorLog rows to delete
|
|
43
|
+
# @return [Integer] number of error logs deleted
|
|
44
|
+
def self.delete_with_dependents(scope)
|
|
45
|
+
ids = scope.select(:id)
|
|
46
|
+
|
|
47
|
+
ErrorOccurrence.where(error_log_id: ids).in_batches(of: 1000).delete_all
|
|
48
|
+
# Hour buckets for storm-shed events. Listed explicitly because delete_all
|
|
49
|
+
# does not fire the has_many :dependent callback on ErrorLog -- without
|
|
50
|
+
# this line the buckets outlive the group they describe, forever.
|
|
51
|
+
EventCount.where(error_log_id: ids).in_batches(of: 1000).delete_all if EventCount.table_exists?
|
|
52
|
+
ErrorComment.where(error_log_id: ids).in_batches(of: 1000).delete_all
|
|
53
|
+
CascadePattern.where(parent_error_id: ids)
|
|
54
|
+
.or(CascadePattern.where(child_error_id: ids))
|
|
55
|
+
.in_batches(of: 1000).delete_all
|
|
56
|
+
|
|
57
|
+
deleted = 0
|
|
58
|
+
scope.in_batches(of: 1000) { |batch| deleted += batch.delete_all }
|
|
59
|
+
deleted
|
|
60
|
+
end
|
|
61
|
+
|
|
36
62
|
# @return [Integer] number of errors deleted
|
|
37
63
|
def perform
|
|
38
64
|
retention_days = RailsErrorDashboard.configuration.retention_days
|
|
@@ -52,29 +78,7 @@ module RailsErrorDashboard
|
|
|
52
78
|
expired_scope = self.class.expired_scope(cutoff)
|
|
53
79
|
return 0 if expired_scope.none?
|
|
54
80
|
|
|
55
|
-
deleted_count =
|
|
56
|
-
|
|
57
|
-
# Delete dependents first, then errors — all in batches to prevent table locks
|
|
58
|
-
expired_ids_scope = expired_scope.select(:id)
|
|
59
|
-
|
|
60
|
-
# Batch delete dependent records (occurrences, comments, cascade patterns)
|
|
61
|
-
ErrorOccurrence.where(error_log_id: expired_ids_scope).in_batches(of: 1000).delete_all
|
|
62
|
-
# Hour buckets for storm-shed events. Listed explicitly because this job
|
|
63
|
-
# deletes with delete_all, which does not fire the has_many :dependent
|
|
64
|
-
# callback on ErrorLog -- without this line the buckets outlive the group
|
|
65
|
-
# they describe, forever. The migration's own comment promised this
|
|
66
|
-
# pruning before the code existed.
|
|
67
|
-
EventCount.where(error_log_id: expired_ids_scope).in_batches(of: 1000).delete_all if EventCount.table_exists?
|
|
68
|
-
ErrorComment.where(error_log_id: expired_ids_scope).in_batches(of: 1000).delete_all
|
|
69
|
-
CascadePattern.where(parent_error_id: expired_ids_scope)
|
|
70
|
-
.or(CascadePattern.where(child_error_id: expired_ids_scope))
|
|
71
|
-
.in_batches(of: 1000).delete_all
|
|
72
|
-
|
|
73
|
-
# Now batch delete the error logs themselves
|
|
74
|
-
expired_scope.in_batches(of: 1000) do |batch|
|
|
75
|
-
batch_size = batch.delete_all
|
|
76
|
-
deleted_count += batch_size
|
|
77
|
-
end
|
|
81
|
+
deleted_count = self.class.delete_with_dependents(expired_scope)
|
|
78
82
|
|
|
79
83
|
# delete_all skips callbacks, and the stat cards are cached.
|
|
80
84
|
Services::AnalyticsCacheManager.clear if deleted_count > 0
|
|
@@ -15,7 +15,7 @@ module RailsErrorDashboard
|
|
|
15
15
|
class_option :pagerduty, type: :boolean, default: false, desc: "Enable PagerDuty notifications"
|
|
16
16
|
class_option :webhooks, type: :boolean, default: false, desc: "Enable webhook notifications"
|
|
17
17
|
# Performance options
|
|
18
|
-
class_option :async_logging, type: :boolean, default: true, desc: "Enable async error logging (default: true
|
|
18
|
+
class_option :async_logging, type: :boolean, default: true, desc: "Enable async error logging (default: true; jobs run on your app's Active Job adapter, so production needs a worker)"
|
|
19
19
|
class_option :error_sampling, type: :boolean, default: false, desc: "Enable error sampling (reduce volume)"
|
|
20
20
|
class_option :separate_database, type: :boolean, default: false, desc: "Use separate database for errors"
|
|
21
21
|
class_option :database, type: :string, default: nil, desc: "Database name to use for errors (e.g., 'error_dashboard')"
|
|
@@ -383,7 +383,7 @@ module RailsErrorDashboard
|
|
|
383
383
|
return
|
|
384
384
|
end
|
|
385
385
|
|
|
386
|
-
route "mount RailsErrorDashboard::Engine => '/red' # RED (Rails Error Dashboard)
|
|
386
|
+
route "mount RailsErrorDashboard::Engine => '/red' # RED (Rails Error Dashboard)"
|
|
387
387
|
end
|
|
388
388
|
|
|
389
389
|
def show_feature_summary
|
|
@@ -400,7 +400,7 @@ module RailsErrorDashboard
|
|
|
400
400
|
say " ✓ Dashboard UI", :green
|
|
401
401
|
say " ✓ Real-time Updates", :green
|
|
402
402
|
say " ✓ Analytics", :green
|
|
403
|
-
say " ✓ Async Logging (
|
|
403
|
+
say " ✓ Async Logging (runs on your app's Active Job adapter)", :green
|
|
404
404
|
|
|
405
405
|
# Count optional features enabled
|
|
406
406
|
enabled_count = 0
|
|
@@ -477,14 +477,11 @@ module RailsErrorDashboard
|
|
|
477
477
|
say " → Set PAGERDUTY_INTEGRATION_KEY in .env", :yellow if @enable_pagerduty
|
|
478
478
|
say " → Set WEBHOOK_URLS in .env", :yellow if @enable_webhooks
|
|
479
479
|
if @enable_async_logging
|
|
480
|
-
#
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
else
|
|
486
|
-
say " ✓ Async logging uses Rails :async adapter — no extra process needed", :green
|
|
487
|
-
end
|
|
480
|
+
# RED's jobs run on the app's own Active Job adapter; async_adapter only
|
|
481
|
+
# feeds validation and the Settings page. Rails 8 production uses Solid
|
|
482
|
+
# Queue, which needs a worker, so don't promise that none is needed.
|
|
483
|
+
say " → Async logging enqueues on your app's config.active_job.queue_adapter.", :yellow
|
|
484
|
+
say " Run a worker for the default and error_notifications queues, or set config.async_logging = false.", :yellow
|
|
488
485
|
end
|
|
489
486
|
|
|
490
487
|
# Database-specific instructions
|
|
@@ -503,21 +500,22 @@ module RailsErrorDashboard
|
|
|
503
500
|
else
|
|
504
501
|
say " 3. Run: rails db:migrate:error_dashboard"
|
|
505
502
|
end
|
|
506
|
-
say " 4.
|
|
503
|
+
say " 4. Before deploying: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD there"
|
|
507
504
|
say " 5. Restart your Rails server"
|
|
508
505
|
say " 6. Visit http://localhost:3000/red"
|
|
509
506
|
say " 7. Verify: rails error_dashboard:verify"
|
|
510
507
|
else
|
|
511
508
|
say " 1. Run: rails db:migrate"
|
|
512
|
-
say " 2.
|
|
509
|
+
say " 2. Before deploying: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD there"
|
|
513
510
|
say " 3. Restart your Rails server"
|
|
514
511
|
say " 4. Visit http://localhost:3000/red"
|
|
515
512
|
end
|
|
516
513
|
say "Authentication:", :cyan
|
|
517
|
-
say " Default: HTTP Basic Auth
|
|
514
|
+
say " Default: HTTP Basic Auth, gandalf/youshallnotpass in development and test only", :white
|
|
515
|
+
say " Elsewhere: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD, or the app won't boot", :white
|
|
518
516
|
say " Devise/Warden: config.authenticate_with = -> { warden.authenticated? }", :white
|
|
519
517
|
say " Session-based: config.authenticate_with = -> { session[:admin] == true }", :white
|
|
520
|
-
say " See: https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md#
|
|
518
|
+
say " See: https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md#dashboard-credentials", :white
|
|
521
519
|
say "\n"
|
|
522
520
|
say "Issue Tracking (optional):", :cyan
|
|
523
521
|
say " Create a dedicated RED (Rails Error Dashboard) bot account on your platform:", :white
|
|
@@ -549,7 +547,7 @@ module RailsErrorDashboard
|
|
|
549
547
|
|
|
550
548
|
def build_quick_defaults
|
|
551
549
|
{
|
|
552
|
-
# Async ON (
|
|
550
|
+
# Async ON (runs on the app's Active Job adapter)
|
|
553
551
|
async_logging: true,
|
|
554
552
|
# All analytics ON — run at query/background time, zero request-path overhead
|
|
555
553
|
baseline_alerts: true,
|
|
@@ -5,9 +5,12 @@ RailsErrorDashboard.configure do |config|
|
|
|
5
5
|
# AUTHENTICATION (Always Required - Cannot Be Disabled)
|
|
6
6
|
# ============================================================================
|
|
7
7
|
|
|
8
|
-
# Dashboard
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# Dashboard login (HTTP Basic Auth). Always enforced, in every environment.
|
|
9
|
+
# gandalf / youshallnotpass work in development and test only. Everywhere
|
|
10
|
+
# else, set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD in the
|
|
11
|
+
# environment, or the app refuses to boot. Don't replace the fallback values
|
|
12
|
+
# below with a real password: it would end up in source control.
|
|
13
|
+
# https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md#dashboard-credentials
|
|
11
14
|
config.dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "gandalf")
|
|
12
15
|
config.dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "youshallnotpass")
|
|
13
16
|
|
|
@@ -160,9 +163,11 @@ RailsErrorDashboard.configure do |config|
|
|
|
160
163
|
|
|
161
164
|
<% if @enable_async_logging -%>
|
|
162
165
|
# Async Error Logging - ENABLED
|
|
163
|
-
# Errors are logged in background jobs
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
+
# Errors are logged in background jobs, off the request path. The jobs run on
|
|
167
|
+
# your app's Active Job adapter (config.active_job.queue_adapter), so in
|
|
168
|
+
# production a worker must process the default and error_notifications queues
|
|
169
|
+
# (Rails 8: bin/jobs for Solid Queue). async_adapter below is only validated
|
|
170
|
+
# and shown on the Settings page; it does not choose the backend.
|
|
166
171
|
config.async_logging = true
|
|
167
172
|
config.async_adapter = :async # Options: :async (built-in), :sidekiq, :solid_queue
|
|
168
173
|
# To disable: Set config.async_logging = false
|
|
@@ -446,7 +451,8 @@ RailsErrorDashboard.configure do |config|
|
|
|
446
451
|
config.enable_internal_logging = false
|
|
447
452
|
|
|
448
453
|
# Log level (default: :silent)
|
|
449
|
-
# Options: :debug, :info, :warn, :error, :silent
|
|
454
|
+
# Options: :debug, :info, :warn, :error, :fatal, :silent
|
|
455
|
+
# (RED has no fatal-level messages, so :fatal logs nothing, like :silent)
|
|
450
456
|
config.log_level = :silent
|
|
451
457
|
|
|
452
458
|
# Example: Enable verbose logging for debugging
|
|
@@ -2,34 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsErrorDashboard
|
|
4
4
|
module Generators
|
|
5
|
-
#
|
|
5
|
+
# Deprecated: checks the app's Solid Queue config for RED. Writes nothing.
|
|
6
6
|
# Usage: rails generate rails_error_dashboard:solid_queue
|
|
7
|
+
#
|
|
8
|
+
# It used to write a config/queue.yml with workers and no dispatchers.
|
|
9
|
+
# Solid Queue only falls back to its default dispatcher when a section has
|
|
10
|
+
# neither, so that file ran no dispatcher: no delayed job ran, the host
|
|
11
|
+
# app's own included. A Solid Queue config with a "*" worker and a
|
|
12
|
+
# dispatcher already processes RED's queues, so this generator now only
|
|
13
|
+
# checks the file the app has. It never writes Solid Queue's files or
|
|
14
|
+
# points at Solid Queue's installer (decision 0001). To be removed in a
|
|
15
|
+
# later minor release.
|
|
7
16
|
class SolidQueueGenerator < Rails::Generators::Base
|
|
8
|
-
|
|
17
|
+
desc "Deprecated: checks config/queue.yml for problems that stop RED's jobs. Writes nothing."
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
def check_queue_config
|
|
20
|
+
say "\nrails_error_dashboard:solid_queue is deprecated and no longer writes config/queue.yml.", :yellow
|
|
21
|
+
say "A Solid Queue config with a \"*\" worker and a dispatcher already processes RED's queues.\n\n"
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
23
|
+
check = RailsErrorDashboard::Services::SolidQueueConfigCheck
|
|
24
|
+
path = check.config_path(destination_root)
|
|
25
|
+
relative = path.relative_path_from(Pathname(destination_root)).to_s
|
|
26
|
+
|
|
27
|
+
unless path.exist?
|
|
28
|
+
say "No #{relative}: Solid Queue runs its defaults (a \"*\" worker and a dispatcher), which process RED's queues.", :green
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
environments = check.app_environments(destination_root)
|
|
33
|
+
environments = [ Rails.env.to_s ] if environments.empty?
|
|
34
|
+
result = check.call(path, environments: environments)
|
|
35
|
+
|
|
36
|
+
if result[:skipped]
|
|
37
|
+
say "Could not check #{relative}: #{result[:skipped]}", :yellow
|
|
38
|
+
elsif result[:problems].any?
|
|
39
|
+
say "#{relative} has problems that stop jobs from running:", :red
|
|
40
|
+
result[:problems].each { |problem| say " - #{problem}", :red }
|
|
41
|
+
say ""
|
|
42
|
+
check::FIX.each { |line| say line, :yellow }
|
|
43
|
+
else
|
|
44
|
+
say "#{relative} processes RED's queues (#{check.red_queue_names.join(', ')}) and runs a dispatcher.", :green
|
|
45
|
+
end
|
|
14
46
|
end
|
|
15
47
|
|
|
16
48
|
def show_instructions
|
|
17
|
-
say "\
|
|
18
|
-
say "
|
|
19
|
-
say "=
|
|
20
|
-
say "
|
|
21
|
-
say " 1. Install Solid Queue gem (if not already):", :cyan
|
|
22
|
-
say " bundle add solid_queue", :white
|
|
23
|
-
say "\n 2. Run Solid Queue migrations:", :cyan
|
|
24
|
-
say " bin/rails solid_queue:install", :white
|
|
25
|
-
say "\n 3. Set ActiveJob adapter in config/application.rb:", :cyan
|
|
26
|
-
say " config.active_job.queue_adapter = :solid_queue", :white
|
|
27
|
-
say "\n 4. Start Solid Queue worker:", :cyan
|
|
28
|
-
say " bin/jobs", :white
|
|
29
|
-
say "\n 5. Enable async logging in config/initializers/rails_error_dashboard.rb:", :cyan
|
|
30
|
-
say " config.async_logging = true", :white
|
|
31
|
-
say " config.async_adapter = :solid_queue", :white
|
|
32
|
-
say "\n" + "=" * 80, :green
|
|
49
|
+
say "\nTo run RED's jobs with Solid Queue:", :cyan
|
|
50
|
+
say " config.active_job.queue_adapter = :solid_queue (config/environments/production.rb)", :white
|
|
51
|
+
say " Run a worker: bin/jobs, or SOLID_QUEUE_IN_PUMA=1 with Puma's solid_queue plugin", :white
|
|
52
|
+
say " Check anytime: bin/rails error_dashboard:verify\n", :white
|
|
33
53
|
end
|
|
34
54
|
end
|
|
35
55
|
end
|
|
@@ -5,6 +5,11 @@ module RailsErrorDashboard
|
|
|
5
5
|
class UninstallGenerator < Rails::Generators::Base
|
|
6
6
|
desc "Uninstalls Rails Error Dashboard and removes all associated files and data"
|
|
7
7
|
|
|
8
|
+
MIGRATION_GLOBS = [
|
|
9
|
+
"db/migrate/*rails_error_dashboard*.rb",
|
|
10
|
+
"db/error_dashboard_migrate/*rails_error_dashboard*.rb"
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
8
13
|
class_option :keep_data, type: :boolean, default: false, desc: "Keep error data in database (don't drop tables)"
|
|
9
14
|
class_option :skip_confirmation, type: :boolean, default: false, desc: "Skip confirmation prompts"
|
|
10
15
|
class_option :manual_only, type: :boolean, default: false, desc: "Show manual instructions only, don't perform automated uninstall"
|
|
@@ -35,6 +40,15 @@ module RailsErrorDashboard
|
|
|
35
40
|
say " #{status_icon(@components[:migrations])} Migrations (#{migration_count} files)"
|
|
36
41
|
say " #{status_icon(@components[:tables])} Database tables (#{table_count} tables)"
|
|
37
42
|
say "\n"
|
|
43
|
+
|
|
44
|
+
# Couldn't see the error database (no database.yml entry for it, or the
|
|
45
|
+
# server is down). Carrying on would remove the initializer and leave
|
|
46
|
+
# every table behind, with nothing left that knows where they are.
|
|
47
|
+
if @database_error && !options[:keep_data] && !options[:manual_only]
|
|
48
|
+
raise Thor::Error, "Can't reach the error database, so nothing was removed: #{@database_error}\n" \
|
|
49
|
+
"Fix config/database.yml (or start the database) and run this again, " \
|
|
50
|
+
"or pass --keep-data to remove only the files."
|
|
51
|
+
end
|
|
38
52
|
end
|
|
39
53
|
|
|
40
54
|
def show_manual_instructions
|
|
@@ -42,13 +56,16 @@ module RailsErrorDashboard
|
|
|
42
56
|
say " 📖 Manual Uninstall Instructions", :cyan
|
|
43
57
|
say "=" * 80
|
|
44
58
|
say "\n"
|
|
45
|
-
|
|
46
|
-
say "Step 1: Remove from Gemfile", :yellow
|
|
47
|
-
say " Open: Gemfile"
|
|
48
|
-
say " Remove: gem 'rails_error_dashboard'"
|
|
49
|
-
say " Run: bundle install"
|
|
59
|
+
say "Do these in order: the table drop needs the gem, so the Gemfile comes last.", :yellow
|
|
50
60
|
say "\n"
|
|
51
61
|
|
|
62
|
+
if @components[:tables]
|
|
63
|
+
say "Step 1: Drop database tables (⚠️ DESTRUCTIVE - will delete all error data)", :yellow
|
|
64
|
+
say " Run: bin/rails rails_error_dashboard:db:drop"
|
|
65
|
+
say " It drops every RED table in foreign-key order, on the error database if you use one."
|
|
66
|
+
say "\n"
|
|
67
|
+
end
|
|
68
|
+
|
|
52
69
|
if @components[:initializer]
|
|
53
70
|
say "Step 2: Remove initializer", :yellow
|
|
54
71
|
say " Delete: config/initializers/rails_error_dashboard.rb"
|
|
@@ -58,33 +75,31 @@ module RailsErrorDashboard
|
|
|
58
75
|
if @components[:route]
|
|
59
76
|
say "Step 3: Remove route", :yellow
|
|
60
77
|
say " Open: config/routes.rb"
|
|
61
|
-
say " Remove: mount RailsErrorDashboard::Engine =>
|
|
78
|
+
say " Remove the line: mount RailsErrorDashboard::Engine => ..."
|
|
62
79
|
say "\n"
|
|
63
80
|
end
|
|
64
81
|
|
|
65
82
|
if @components[:migrations]
|
|
66
83
|
say "Step 4: Remove migrations", :yellow
|
|
67
|
-
say " Delete migration files from db/migrate/:"
|
|
68
84
|
migration_files.each do |file|
|
|
69
|
-
say " - #{
|
|
85
|
+
say " - #{file}", :white
|
|
70
86
|
end
|
|
71
87
|
say "\n"
|
|
72
88
|
end
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_cascade_patterns')", :white
|
|
81
|
-
say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_baselines')", :white
|
|
82
|
-
say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_comments')", :white
|
|
83
|
-
say " ActiveRecord::Migration.drop_table(:rails_error_dashboard_error_logs) rescue nil", :white
|
|
84
|
-
say "\n"
|
|
85
|
-
end
|
|
90
|
+
say "Step 5: Separate database only: remove its entry from config/database.yml", :yellow
|
|
91
|
+
say "\n"
|
|
92
|
+
|
|
93
|
+
say "Step 6: Regenerate the schema file", :yellow
|
|
94
|
+
say " Run: bin/rails db:schema:dump (otherwise db:schema:load recreates the tables)"
|
|
95
|
+
say "\n"
|
|
86
96
|
|
|
87
|
-
say "Step
|
|
97
|
+
say "Step 7: Remove from Gemfile", :yellow
|
|
98
|
+
say " Remove: gem 'rails_error_dashboard'"
|
|
99
|
+
say " Run: bundle install"
|
|
100
|
+
say "\n"
|
|
101
|
+
|
|
102
|
+
say "Step 8: Clean up environment variables (optional)", :yellow
|
|
88
103
|
say " Remove from .env or environment:"
|
|
89
104
|
say " - ERROR_DASHBOARD_USER"
|
|
90
105
|
say " - ERROR_DASHBOARD_PASSWORD"
|
|
@@ -96,8 +111,7 @@ module RailsErrorDashboard
|
|
|
96
111
|
say " - DASHBOARD_BASE_URL"
|
|
97
112
|
say "\n"
|
|
98
113
|
|
|
99
|
-
say "Step
|
|
100
|
-
say " Run: rails restart (or restart your server)"
|
|
114
|
+
say "Step 9: Restart your application", :yellow
|
|
101
115
|
say "\n"
|
|
102
116
|
|
|
103
117
|
say "=" * 80
|
|
@@ -165,6 +179,20 @@ module RailsErrorDashboard
|
|
|
165
179
|
say "\n"
|
|
166
180
|
end
|
|
167
181
|
|
|
182
|
+
# Before any file is removed: if the drop fails, the app still boots with
|
|
183
|
+
# its initializer and can reach the error database to try again.
|
|
184
|
+
def drop_database_tables
|
|
185
|
+
return if options[:manual_only]
|
|
186
|
+
return if options[:keep_data]
|
|
187
|
+
return unless @components[:tables]
|
|
188
|
+
|
|
189
|
+
say " Dropping database tables...", :yellow
|
|
190
|
+
dropped = RailsErrorDashboard::Commands::DropAllTables.call(connection: drop_connection)
|
|
191
|
+
say " ✓ Dropped #{dropped.size} database table(s)", :green
|
|
192
|
+
rescue => e
|
|
193
|
+
raise Thor::Error, "Could not drop RED's tables, so no files were removed: #{e.message}"
|
|
194
|
+
end
|
|
195
|
+
|
|
168
196
|
def remove_initializer
|
|
169
197
|
return if options[:manual_only]
|
|
170
198
|
return unless @components[:initializer]
|
|
@@ -182,7 +210,7 @@ module RailsErrorDashboard
|
|
|
182
210
|
say " ✓ Removed route", :green
|
|
183
211
|
rescue => e
|
|
184
212
|
say " ⚠️ Could not automatically remove route: #{e.message}", :yellow
|
|
185
|
-
say " Please
|
|
213
|
+
say " Please remove the mount RailsErrorDashboard::Engine line from config/routes.rb", :yellow
|
|
186
214
|
end
|
|
187
215
|
end
|
|
188
216
|
|
|
@@ -196,35 +224,6 @@ module RailsErrorDashboard
|
|
|
196
224
|
say " ✓ Removed #{migration_count} migration file(s)", :green
|
|
197
225
|
end
|
|
198
226
|
|
|
199
|
-
def drop_database_tables
|
|
200
|
-
return if options[:manual_only]
|
|
201
|
-
return if options[:keep_data]
|
|
202
|
-
return unless @components[:tables]
|
|
203
|
-
|
|
204
|
-
say " Dropping database tables...", :yellow
|
|
205
|
-
|
|
206
|
-
# Drop tables in reverse order (to respect foreign keys)
|
|
207
|
-
tables_to_drop = [
|
|
208
|
-
"rails_error_dashboard_error_comments",
|
|
209
|
-
"rails_error_dashboard_error_occurrences",
|
|
210
|
-
"rails_error_dashboard_cascade_patterns",
|
|
211
|
-
"rails_error_dashboard_error_baselines",
|
|
212
|
-
"rails_error_dashboard_error_logs"
|
|
213
|
-
]
|
|
214
|
-
|
|
215
|
-
dropped_count = 0
|
|
216
|
-
tables_to_drop.each do |table|
|
|
217
|
-
if ActiveRecord::Base.connection.table_exists?(table)
|
|
218
|
-
ActiveRecord::Base.connection.drop_table(table, if_exists: true)
|
|
219
|
-
dropped_count += 1
|
|
220
|
-
end
|
|
221
|
-
rescue => e
|
|
222
|
-
say " ⚠️ Could not drop table #{table}: #{e.message}", :yellow
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
say " ✓ Dropped #{dropped_count} database table(s)", :green
|
|
226
|
-
end
|
|
227
|
-
|
|
228
227
|
def show_completion_message
|
|
229
228
|
return if options[:manual_only]
|
|
230
229
|
|
|
@@ -282,7 +281,7 @@ module RailsErrorDashboard
|
|
|
282
281
|
end
|
|
283
282
|
|
|
284
283
|
def migration_files
|
|
285
|
-
Dir.glob(
|
|
284
|
+
Dir.glob(MIGRATION_GLOBS)
|
|
286
285
|
end
|
|
287
286
|
|
|
288
287
|
def migration_count
|
|
@@ -290,22 +289,27 @@ module RailsErrorDashboard
|
|
|
290
289
|
end
|
|
291
290
|
|
|
292
291
|
def tables_exist?
|
|
293
|
-
|
|
294
|
-
table_names.any? { |table| ActiveRecord::Base.connection.table_exists?(table) rescue false }
|
|
292
|
+
table_names.any?
|
|
295
293
|
end
|
|
296
294
|
|
|
295
|
+
# The tables Queries::UninstallPlan finds on the error database, in drop
|
|
296
|
+
# order. Empty (with the reason shown) when that database can't be reached.
|
|
297
297
|
def table_names
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
"
|
|
303
|
-
|
|
304
|
-
|
|
298
|
+
@table_names ||= begin
|
|
299
|
+
RailsErrorDashboard::Queries::UninstallPlan.call(drop_connection).map { |entry| entry[:table] }
|
|
300
|
+
rescue => e
|
|
301
|
+
@database_error = e.message
|
|
302
|
+
say " ⚠️ Could not inspect the error database: #{e.message}", :yellow
|
|
303
|
+
[]
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def drop_connection
|
|
308
|
+
RailsErrorDashboard::Commands::DropAllTables.connection
|
|
305
309
|
end
|
|
306
310
|
|
|
307
311
|
def table_count
|
|
308
|
-
table_names.count
|
|
312
|
+
table_names.count
|
|
309
313
|
end
|
|
310
314
|
|
|
311
315
|
def gemfile_includes_gem?
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Drops every RED table, for uninstalling. DESTRUCTIVE: all error data goes.
|
|
6
|
+
#
|
|
7
|
+
# Used by the rails_error_dashboard:db:drop task and the uninstall
|
|
8
|
+
# generator. Drops on the connection RED's models use, so a separate or
|
|
9
|
+
# shared error database is covered, in the order Queries::UninstallPlan
|
|
10
|
+
# derives from the foreign keys. Where the adapter runs DDL in a transaction
|
|
11
|
+
# (PostgreSQL, SQLite) it is all-or-nothing. Errors are raised, never
|
|
12
|
+
# swallowed: a half-finished uninstall must not report success.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# RailsErrorDashboard::Commands::DropAllTables.call
|
|
16
|
+
# # => ["rails_error_dashboard_error_comments", ...]
|
|
17
|
+
class DropAllTables
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
20
|
+
# The connection RED's models use.
|
|
21
|
+
#
|
|
22
|
+
# @raise [Error] when use_separate_database is on but the engine fell back
|
|
23
|
+
# to the primary database because database.yml has no entry for it:
|
|
24
|
+
# dropping there would leave the real error database untouched.
|
|
25
|
+
# @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
|
26
|
+
def self.connection
|
|
27
|
+
config = RailsErrorDashboard.configuration
|
|
28
|
+
record = RailsErrorDashboard::ErrorLogsRecord
|
|
29
|
+
|
|
30
|
+
if config.use_separate_database
|
|
31
|
+
expected = config.database.to_s
|
|
32
|
+
actual = record.connection_db_config&.name.to_s
|
|
33
|
+
if actual != expected
|
|
34
|
+
raise Error, "use_separate_database is on, but RED is connected to the '#{actual}' database, " \
|
|
35
|
+
"not '#{expected}': database.yml has no '#{expected}' entry for #{Rails.env}. " \
|
|
36
|
+
"Add it (see docs/guides/DATABASE_OPTIONS.md) and run this again, or RED's tables " \
|
|
37
|
+
"in the error database would be left behind."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
record.connection
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
|
45
|
+
# @return [Array<String>] the dropped tables, in drop order
|
|
46
|
+
# @raise [Error] when a host table has a foreign key into a RED table
|
|
47
|
+
def self.call(connection: self.connection)
|
|
48
|
+
tables = Queries::UninstallPlan.call(connection).map { |entry| entry[:table] }
|
|
49
|
+
return [] if tables.empty?
|
|
50
|
+
|
|
51
|
+
blocking = host_foreign_keys(connection, tables)
|
|
52
|
+
if blocking.any?
|
|
53
|
+
raise Error, "Not dropping anything: these tables have foreign keys into RED's tables. " \
|
|
54
|
+
"Remove the foreign keys first: #{blocking.join('; ')}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
drop = -> { tables.each { |table| connection.drop_table(table, if_exists: true) } }
|
|
58
|
+
if connection.supports_ddl_transactions?
|
|
59
|
+
connection.transaction(requires_new: true) { drop.call }
|
|
60
|
+
else
|
|
61
|
+
drop.call
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
tables
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.host_foreign_keys(connection, tables)
|
|
68
|
+
(connection.tables - tables).flat_map do |table|
|
|
69
|
+
connection.foreign_keys(table)
|
|
70
|
+
.select { |fk| tables.include?(fk.to_table) }
|
|
71
|
+
.map { |fk| "#{table}.#{fk.column} -> #{fk.to_table} (#{fk.name})" }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
private_class_method :host_foreign_keys
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -495,7 +495,7 @@ module RailsErrorDashboard
|
|
|
495
495
|
|
|
496
496
|
# Internal logging defaults - SILENT by default
|
|
497
497
|
@enable_internal_logging = false # Opt-in for debugging
|
|
498
|
-
@log_level = :silent # Silent by default
|
|
498
|
+
@log_level = :silent # Silent by default; see Logger::LOG_LEVELS for the levels
|
|
499
499
|
|
|
500
500
|
# Dashboard UI
|
|
501
501
|
@accent_color = :crimson # :crimson, :ruby, :ember, :violet
|
|
@@ -554,6 +554,14 @@ module RailsErrorDashboard
|
|
|
554
554
|
errors << "Default or blank credentials cannot be used in #{Rails.env}: #{reason}. Only development and test may run on the built-in credentials. Set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD environment variables and make sure the initializer does not overwrite them, or use authenticate_with for custom auth."
|
|
555
555
|
end
|
|
556
556
|
|
|
557
|
+
# In development the app boots on a blank credential, and the login then
|
|
558
|
+
# denies everyone: say so, or the developer is locked out with no clue
|
|
559
|
+
# why. Development only, because a test run boots on every CI job.
|
|
560
|
+
if default_credentials? && credentials_problem == :blank &&
|
|
561
|
+
defined?(Rails) && Rails.respond_to?(:env) && Rails.env.development?
|
|
562
|
+
warnings.concat(blank_credential_warnings)
|
|
563
|
+
end
|
|
564
|
+
|
|
557
565
|
# Validate sampling_rate (must be between 0.0 and 1.0)
|
|
558
566
|
if sampling_rate && (sampling_rate < 0.0 || sampling_rate > 1.0)
|
|
559
567
|
errors << "sampling_rate must be between 0.0 and 1.0 (got: #{sampling_rate})"
|
|
@@ -786,7 +794,9 @@ module RailsErrorDashboard
|
|
|
786
794
|
|
|
787
795
|
# Validate log level (must be valid symbol)
|
|
788
796
|
if log_level
|
|
789
|
-
|
|
797
|
+
# Referenced here, not as a class-body constant: this file loads before
|
|
798
|
+
# logger.rb, and a bare Logger in the class body would be ::Logger.
|
|
799
|
+
valid_log_levels = RailsErrorDashboard::Logger::LOG_LEVELS.keys
|
|
790
800
|
unless valid_log_levels.include?(log_level)
|
|
791
801
|
errors << "log_level must be one of #{valid_log_levels.inspect} (got: #{log_level.inspect})"
|
|
792
802
|
end
|
|
@@ -996,6 +1006,27 @@ module RailsErrorDashboard
|
|
|
996
1006
|
!Rails.env.development? && !Rails.env.test?
|
|
997
1007
|
end
|
|
998
1008
|
|
|
1009
|
+
# One warning per blank credential, naming where the blank came from: an
|
|
1010
|
+
# environment variable that is set but empty, or the initializer.
|
|
1011
|
+
#
|
|
1012
|
+
# @return [Array<String>]
|
|
1013
|
+
private def blank_credential_warnings
|
|
1014
|
+
{
|
|
1015
|
+
"ERROR_DASHBOARD_USER" => [ :dashboard_username, dashboard_username ],
|
|
1016
|
+
"ERROR_DASHBOARD_PASSWORD" => [ :dashboard_password, dashboard_password ]
|
|
1017
|
+
}.filter_map do |variable, (setting, value)|
|
|
1018
|
+
next unless self.class.blank_credential?(value)
|
|
1019
|
+
|
|
1020
|
+
if ENV.key?(variable) && ENV[variable].to_s == value.to_s
|
|
1021
|
+
"#{variable} is set but empty, so every dashboard login will be denied. " \
|
|
1022
|
+
"Unset it to use the development default, or give it a value."
|
|
1023
|
+
else
|
|
1024
|
+
"config.#{setting} is blank, so every dashboard login will be denied. " \
|
|
1025
|
+
"Remove that line from the initializer to use the development default, or give it a value."
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
end
|
|
1029
|
+
|
|
999
1030
|
# Resolve the effective issue tracker provider (auto-detect from git_repository_url).
|
|
1000
1031
|
# Linear is never auto-detected (it is not a git forge) — set issue_tracker_provider
|
|
1001
1032
|
# explicitly.
|
|
@@ -18,12 +18,17 @@ module RailsErrorDashboard
|
|
|
18
18
|
# config.log_level = :error
|
|
19
19
|
# end
|
|
20
20
|
module Logger
|
|
21
|
+
# The levels Configuration#validate! accepts: it reads this hash, so the two
|
|
22
|
+
# can't drift apart again (validate! once accepted :fatal while this hash
|
|
23
|
+
# lacked it, and every log call raised). RED emits nothing at :fatal, so it
|
|
24
|
+
# logs nothing, as with Ruby's Logger.
|
|
21
25
|
LOG_LEVELS = {
|
|
22
26
|
debug: 0,
|
|
23
27
|
info: 1,
|
|
24
28
|
warn: 2,
|
|
25
29
|
error: 3,
|
|
26
|
-
|
|
30
|
+
fatal: 4,
|
|
31
|
+
silent: 5
|
|
27
32
|
}.freeze
|
|
28
33
|
|
|
29
34
|
class << self
|
|
@@ -89,8 +94,11 @@ module RailsErrorDashboard
|
|
|
89
94
|
# @param level [Symbol] The log level to check (:debug, :info, :warn, :error)
|
|
90
95
|
# @return [Boolean]
|
|
91
96
|
def log_level_enabled?(level)
|
|
92
|
-
config_level = RailsErrorDashboard.configuration.log_level
|
|
93
|
-
LOG_LEVELS
|
|
97
|
+
config_level = RailsErrorDashboard.configuration.log_level.to_s.to_sym
|
|
98
|
+
LOG_LEVELS.fetch(level) >= LOG_LEVELS.fetch(config_level, LOG_LEVELS[:silent])
|
|
99
|
+
rescue StandardError
|
|
100
|
+
# The logger runs inside rescue blocks on the capture path; it must never raise.
|
|
101
|
+
false
|
|
94
102
|
end
|
|
95
103
|
|
|
96
104
|
# Format message with gem prefix
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Queries
|
|
5
|
+
# Every RED table on a connection, in an order that can be dropped.
|
|
6
|
+
#
|
|
7
|
+
# The tables are discovered rather than listed by hand: a hand-kept list
|
|
8
|
+
# knew 5 of 13 tables, so uninstall left the rest behind (rack_attack_events
|
|
9
|
+
# among them, which holds IP addresses). The order comes from the real
|
|
10
|
+
# foreign keys: a table that references another is dropped first.
|
|
11
|
+
#
|
|
12
|
+
# Uses the raw connection only, never the models, so no model's schema
|
|
13
|
+
# cache ever records a table as dropped.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# UninstallPlan.call(RailsErrorDashboard::ErrorLogsRecord.connection)
|
|
17
|
+
# # => [{ table: "rails_error_dashboard_error_comments", rows: 12 }, ...]
|
|
18
|
+
class UninstallPlan
|
|
19
|
+
PREFIX = "rails_error_dashboard_"
|
|
20
|
+
|
|
21
|
+
class CycleError < StandardError; end
|
|
22
|
+
|
|
23
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
|
24
|
+
# @return [Array<Hash>] { table: String, rows: Integer or nil } in drop order
|
|
25
|
+
def self.call(connection)
|
|
26
|
+
tables = connection.tables.select { |table| table.start_with?(PREFIX) }
|
|
27
|
+
references = tables.to_h do |table|
|
|
28
|
+
[ table, connection.foreign_keys(table).map(&:to_table) & tables ]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
order(references).map { |table| { table: table, rows: row_count(connection, table) } }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param references [Hash{String => Array<String>}] each table and the tables it references
|
|
35
|
+
# @return [Array<String>] every table, each before the tables it references
|
|
36
|
+
# @raise [CycleError] when the foreign keys form a cycle
|
|
37
|
+
def self.order(references)
|
|
38
|
+
remaining = references.keys.sort
|
|
39
|
+
ordered = []
|
|
40
|
+
|
|
41
|
+
until remaining.empty?
|
|
42
|
+
# Droppable once no other remaining table references it.
|
|
43
|
+
droppable = remaining.reject do |table|
|
|
44
|
+
remaining.any? { |other| other != table && references.fetch(other, []).include?(table) }
|
|
45
|
+
end
|
|
46
|
+
raise CycleError, "foreign-key cycle among: #{remaining.join(', ')}" if droppable.empty?
|
|
47
|
+
|
|
48
|
+
ordered.concat(droppable)
|
|
49
|
+
remaining -= droppable
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
ordered
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.row_count(connection, table)
|
|
56
|
+
connection.select_value("SELECT COUNT(*) FROM #{connection.quote_table_name(table)}").to_i
|
|
57
|
+
rescue ActiveRecord::ActiveRecordError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
private_class_method :row_count
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module RailsErrorDashboard
|
|
6
|
+
module Services
|
|
7
|
+
# Pure algorithm: what in a Solid Queue config would stop jobs from running
|
|
8
|
+
#
|
|
9
|
+
# Mirrors Solid Queue 1.7.0's own rules, so it reports exactly what Solid
|
|
10
|
+
# Queue would do with the file:
|
|
11
|
+
# - SolidQueue::Configuration reads the section for the environment, or the
|
|
12
|
+
# whole file when there is none, keeps workers/dispatchers/scheduler, and
|
|
13
|
+
# only falls back to its defaults (a "*" worker and a dispatcher) when
|
|
14
|
+
# none of the three is there. A section with workers and no dispatchers
|
|
15
|
+
# therefore runs NO dispatcher: nothing scheduled ever becomes ready, so
|
|
16
|
+
# no `retry_on wait:` or `perform_later(wait:)` runs, the host app's own
|
|
17
|
+
# jobs included. That is what the retired rails_error_dashboard:solid_queue
|
|
18
|
+
# generator wrote.
|
|
19
|
+
# - SolidQueue::QueueSelector treats a worker without queues as "*", "*" as
|
|
20
|
+
# every queue, and a trailing "*" as a prefix.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# path = SolidQueueConfigCheck.config_path(Rails.root)
|
|
24
|
+
# SolidQueueConfigCheck.call(path, environments: %w[development production])
|
|
25
|
+
# # => { problems: ["production: workers but no dispatcher ..."], skipped: nil }
|
|
26
|
+
class SolidQueueConfigCheck
|
|
27
|
+
DEFAULT_CONFIG_FILE = "config/queue.yml"
|
|
28
|
+
PROCESS_KEYS = %i[workers dispatchers scheduler].freeze
|
|
29
|
+
# Before 1.4.0, scheduler: was not a process key: a section with only a
|
|
30
|
+
# scheduler fell back to the defaults, a "*" worker and a dispatcher.
|
|
31
|
+
SCHEDULER_KEY_SINCE = Gem::Version.new("1.4.0")
|
|
32
|
+
GUIDE_URL = "https://anjanj.github.io/rails_error_dashboard/docs/guides/solid-queue-setup/#what-red-needs-from-solid-queue"
|
|
33
|
+
|
|
34
|
+
# The fix printed with a reported problem. It is never Solid Queue's
|
|
35
|
+
# installer: whoever sees this already uses Solid Queue, and its 1.7.0
|
|
36
|
+
# installer also rewrites production.rb to use a separate queue database.
|
|
37
|
+
FIX = [
|
|
38
|
+
"Fix: edit the file. Give each environment a dispatchers: block and a worker",
|
|
39
|
+
"for RED's queues (a \"*\" worker covers them). Guide: #{GUIDE_URL}"
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
# The config file Solid Queue reads, honouring SOLID_QUEUE_CONFIG as it does.
|
|
43
|
+
#
|
|
44
|
+
# @param root [Pathname, String] the application root
|
|
45
|
+
# @return [Pathname]
|
|
46
|
+
def self.config_path(root)
|
|
47
|
+
Pathname(root.to_s).join(ENV["SOLID_QUEUE_CONFIG"].presence || DEFAULT_CONFIG_FILE)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The environments an app defines, from config/environments/*.rb.
|
|
51
|
+
#
|
|
52
|
+
# @param root [Pathname, String] the application root
|
|
53
|
+
# @return [Array<String>]
|
|
54
|
+
def self.app_environments(root)
|
|
55
|
+
Dir[Pathname(root.to_s).join("config", "environments", "*.rb").to_s]
|
|
56
|
+
.map { |file| File.basename(file, ".rb") }
|
|
57
|
+
.sort
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The queues RED's jobs are enqueued on, as this app names them
|
|
61
|
+
# (queue_name_prefix included).
|
|
62
|
+
#
|
|
63
|
+
# @return [Array<String>]
|
|
64
|
+
def self.red_queue_names
|
|
65
|
+
[ RailsErrorDashboard::AsyncErrorLoggingJob, RailsErrorDashboard::SlackErrorNotificationJob ]
|
|
66
|
+
.map { |job| job.queue_name.to_s }
|
|
67
|
+
.uniq
|
|
68
|
+
rescue => e
|
|
69
|
+
RailsErrorDashboard::Logger.debug("SolidQueueConfigCheck could not read queue names: #{e.class}: #{e.message}")
|
|
70
|
+
%w[default error_notifications]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Read the file the way Solid Queue does (ERB, then YAML with aliases) and
|
|
74
|
+
# check each environment. Never raises: an unreadable file is reported as
|
|
75
|
+
# skipped.
|
|
76
|
+
#
|
|
77
|
+
# @param path [Pathname, String]
|
|
78
|
+
# @param environments [Array<String>]
|
|
79
|
+
# @param queue_names [Array<String>]
|
|
80
|
+
# @return [Hash] { problems: Array<String>, skipped: String or nil }
|
|
81
|
+
def self.call(path, environments:, queue_names: red_queue_names)
|
|
82
|
+
{ problems: check(parse(path), environments: environments, queue_names: queue_names), skipped: nil }
|
|
83
|
+
rescue StandardError, ScriptError => e # ScriptError: ERB in the file that doesn't compile
|
|
84
|
+
{ problems: [], skipped: "could not read #{path}: #{e.class}: #{e.message.lines.first.to_s.strip}" }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param path [Pathname, String]
|
|
88
|
+
# @return [Hash] the parsed file, symbolized keys, as Solid Queue reads it
|
|
89
|
+
def self.parse(path)
|
|
90
|
+
ActiveSupport::ConfigurationFile.parse(Pathname(path.to_s)).deep_symbolize_keys
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param config [Hash] the parsed file, symbolized keys
|
|
94
|
+
# @param environments [Array<String>]
|
|
95
|
+
# @param queue_names [Array<String>]
|
|
96
|
+
# @return [Array<String>] one line per problem, prefixed with the environment
|
|
97
|
+
def self.check(config, environments:, queue_names:)
|
|
98
|
+
environments.flat_map do |env|
|
|
99
|
+
facts = processes_for(config, environment: env, queue_names: queue_names)
|
|
100
|
+
problems_in(facts, queue_names).map { |problem| "#{env}: #{problem}" }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# What Solid Queue would start for one environment. The contract spec
|
|
105
|
+
# compares this with the real SolidQueue::Configuration and QueueSelector.
|
|
106
|
+
#
|
|
107
|
+
# @param config [Hash] the parsed file, symbolized keys
|
|
108
|
+
# @param environment [String]
|
|
109
|
+
# @param queue_names [Array<String>]
|
|
110
|
+
# @param solid_queue_version [String, nil] the rules to follow; the loaded
|
|
111
|
+
# Solid Queue's by default, the newest when none is loaded
|
|
112
|
+
# @return [Hash] { dispatchers: Integer, workers: Integer,
|
|
113
|
+
# processed: Array<String> (the queue_names a worker picks up), error: String or nil }
|
|
114
|
+
def self.processes_for(config, environment:, queue_names:, solid_queue_version: loaded_solid_queue_version)
|
|
115
|
+
section = section_for(config, environment)
|
|
116
|
+
return cannot_start("the #{environment} section is not a set of settings") unless section.is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
section = section.slice(*process_keys(solid_queue_version))
|
|
119
|
+
# Empty: Solid Queue runs its defaults, a "*" worker and a dispatcher.
|
|
120
|
+
return { dispatchers: 1, workers: 1, processed: queue_names.dup, error: nil } if section.empty?
|
|
121
|
+
|
|
122
|
+
error = shape_error(section)
|
|
123
|
+
return cannot_start(error) if error
|
|
124
|
+
|
|
125
|
+
# bin/jobs runs in fork mode: each worker entry starts `processes` of them.
|
|
126
|
+
workers = section.fetch(:workers, []).select { |worker| worker.fetch(:processes, 1).positive? }
|
|
127
|
+
{
|
|
128
|
+
dispatchers: section.fetch(:dispatchers, []).size,
|
|
129
|
+
workers: workers.sum { |worker| worker.fetch(:processes, 1) },
|
|
130
|
+
processed: queue_names.select { |name| workers.any? { |worker| processes?(worker, name) } },
|
|
131
|
+
error: nil
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.loaded_solid_queue_version
|
|
136
|
+
::SolidQueue::VERSION if defined?(::SolidQueue::VERSION)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.process_keys(solid_queue_version)
|
|
140
|
+
return PROCESS_KEYS if solid_queue_version.nil? || Gem::Version.new(solid_queue_version) >= SCHEDULER_KEY_SINCE
|
|
141
|
+
|
|
142
|
+
PROCESS_KEYS - [ :scheduler ]
|
|
143
|
+
rescue ArgumentError # a version string Gem::Version can't read
|
|
144
|
+
PROCESS_KEYS
|
|
145
|
+
end
|
|
146
|
+
private_class_method :process_keys
|
|
147
|
+
|
|
148
|
+
# SolidQueue::Configuration#config_from: the environment's section when
|
|
149
|
+
# present, else the whole file.
|
|
150
|
+
def self.section_for(config, env)
|
|
151
|
+
config[env.to_sym] ? config[env.to_sym] : config
|
|
152
|
+
end
|
|
153
|
+
private_class_method :section_for
|
|
154
|
+
|
|
155
|
+
# Settings Solid Queue raises on while it builds its processes, so
|
|
156
|
+
# bin/jobs can't start: a key present with no value, a map where a list
|
|
157
|
+
# belongs, a list entry that isn't a set of settings, or processes that
|
|
158
|
+
# isn't a whole number.
|
|
159
|
+
def self.shape_error(section)
|
|
160
|
+
%i[workers dispatchers].each do |key|
|
|
161
|
+
next unless section.key?(key)
|
|
162
|
+
|
|
163
|
+
list = section[key]
|
|
164
|
+
return "#{key}: must be a list, each entry starting with \"- \"" unless list.is_a?(Array)
|
|
165
|
+
return "every entry under #{key}: must be a set of settings" unless list.all?(Hash)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if section.fetch(:workers, []).any? { |worker| worker.key?(:processes) && !worker[:processes].is_a?(Integer) }
|
|
169
|
+
return "a worker's processes: must be a whole number"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
private_class_method :shape_error
|
|
175
|
+
|
|
176
|
+
def self.cannot_start(reason)
|
|
177
|
+
{ dispatchers: 0, workers: 0, processed: [], error: reason }
|
|
178
|
+
end
|
|
179
|
+
private_class_method :cannot_start
|
|
180
|
+
|
|
181
|
+
def self.problems_in(facts, queue_names)
|
|
182
|
+
return [ "Solid Queue can't start with this config: #{facts[:error]}" ] if facts[:error]
|
|
183
|
+
|
|
184
|
+
dispatchers = facts[:dispatchers]
|
|
185
|
+
workers = facts[:workers]
|
|
186
|
+
return [ "no worker and no dispatcher, so no job ever runs" ] if workers.zero? && dispatchers.zero?
|
|
187
|
+
return [ "no worker process starts (no workers, or processes: 0), so no job is ever performed" ] if workers.zero?
|
|
188
|
+
|
|
189
|
+
problems = []
|
|
190
|
+
if dispatchers.zero?
|
|
191
|
+
problems << "workers but no dispatcher, so scheduled jobs and retries " \
|
|
192
|
+
"(retry_on wait:, perform_later(wait:)) never run, the app's own jobs included"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
uncovered = queue_names - facts[:processed]
|
|
196
|
+
problems << "no worker processes #{uncovered.join(' or ')}" if uncovered.any?
|
|
197
|
+
problems
|
|
198
|
+
end
|
|
199
|
+
private_class_method :problems_in
|
|
200
|
+
|
|
201
|
+
# SolidQueue::QueueSelector: no queues means "*"; "*" is every queue; a
|
|
202
|
+
# trailing "*" is a prefix.
|
|
203
|
+
def self.processes?(worker, queue_name)
|
|
204
|
+
queues = worker.is_a?(Hash) ? Array(worker[:queues]).map { |queue| queue.to_s.strip } : []
|
|
205
|
+
queues = [ "*" ] if queues.empty?
|
|
206
|
+
|
|
207
|
+
queues.any? do |queue|
|
|
208
|
+
queue == "*" || queue == queue_name || (queue.end_with?("*") && queue_name.start_with?(queue.delete_suffix("*")))
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
private_class_method :processes?
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -57,6 +57,7 @@ require "rails_error_dashboard/services/statistical_classifier"
|
|
|
57
57
|
require "rails_error_dashboard/services/source_code_reader"
|
|
58
58
|
require "rails_error_dashboard/services/git_blame_reader"
|
|
59
59
|
require "rails_error_dashboard/services/git_head_reader"
|
|
60
|
+
require "rails_error_dashboard/services/solid_queue_config_check"
|
|
60
61
|
require "rails_error_dashboard/services/github_link_generator"
|
|
61
62
|
require "rails_error_dashboard/services/cause_chain_extractor"
|
|
62
63
|
require "rails_error_dashboard/services/environment_snapshot"
|
|
@@ -137,6 +138,8 @@ require "rails_error_dashboard/commands/flush_swallowed_exceptions"
|
|
|
137
138
|
require "rails_error_dashboard/commands/flush_rack_attack_events"
|
|
138
139
|
require "rails_error_dashboard/commands/scrub_invalid_encoding"
|
|
139
140
|
require "rails_error_dashboard/commands/backfill_resolved_at"
|
|
141
|
+
require "rails_error_dashboard/queries/uninstall_plan"
|
|
142
|
+
require "rails_error_dashboard/commands/drop_all_tables"
|
|
140
143
|
require "rails_error_dashboard/queries/errors_list"
|
|
141
144
|
# Before the queries that use it: window volume is shared by dashboard and
|
|
142
145
|
# analytics, and both would otherwise rescue a NameError into a silent zero.
|
|
@@ -177,8 +177,12 @@ namespace :error_dashboard do
|
|
|
177
177
|
if config.refuse_default_credentials?
|
|
178
178
|
puts "WARNING - #{label} outside development and test!"
|
|
179
179
|
checks_failed += 1
|
|
180
|
+
elsif problem == :blank
|
|
181
|
+
# The login denies everyone on a blank credential, in every environment.
|
|
182
|
+
puts "WARNING - #{label}: every login is denied"
|
|
183
|
+
warnings += 1
|
|
180
184
|
else
|
|
181
|
-
puts "OK (#{label} -
|
|
185
|
+
puts "OK (#{label} - set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD before deploying)"
|
|
182
186
|
warnings += 1
|
|
183
187
|
end
|
|
184
188
|
elsif config.dashboard_password == RailsErrorDashboard::Configuration::DEFAULT_DASHBOARD_PASSWORD
|
|
@@ -189,6 +193,31 @@ namespace :error_dashboard do
|
|
|
189
193
|
checks_passed += 1
|
|
190
194
|
end
|
|
191
195
|
|
|
196
|
+
# 9. Solid Queue config. A section with workers and no dispatchers runs no
|
|
197
|
+
# dispatcher, so no delayed job ever runs, the app's own included; the
|
|
198
|
+
# retired rails_error_dashboard:solid_queue generator wrote exactly that.
|
|
199
|
+
# Checked whenever Solid Queue is loaded, not only when it is the current
|
|
200
|
+
# adapter: Rails 8 sets it in production only, and verify usually runs locally.
|
|
201
|
+
queue_check = RailsErrorDashboard::Services::SolidQueueConfigCheck
|
|
202
|
+
queue_config = queue_check.config_path(Rails.root)
|
|
203
|
+
if defined?(SolidQueue) && queue_config.exist?
|
|
204
|
+
print " Solid Queue config... "
|
|
205
|
+
environments = queue_check.app_environments(Rails.root).presence || [ Rails.env.to_s ]
|
|
206
|
+
result = queue_check.call(queue_config, environments: environments)
|
|
207
|
+
if result[:skipped]
|
|
208
|
+
puts "SKIPPED (#{result[:skipped]})"
|
|
209
|
+
warnings += 1
|
|
210
|
+
elsif result[:problems].any?
|
|
211
|
+
puts "FAILED"
|
|
212
|
+
result[:problems].each { |problem| puts " - #{problem}" }
|
|
213
|
+
queue_check::FIX.each { |line| puts " #{line}" }
|
|
214
|
+
checks_failed += 1
|
|
215
|
+
else
|
|
216
|
+
puts "OK"
|
|
217
|
+
checks_passed += 1
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
192
221
|
# Summary
|
|
193
222
|
puts "\n" + "-" * 70
|
|
194
223
|
puts " Results: #{checks_passed} passed, #{checks_failed} failed, #{warnings} warnings"
|
|
@@ -453,7 +482,7 @@ namespace :error_dashboard do
|
|
|
453
482
|
|
|
454
483
|
# Confirm before proceeding
|
|
455
484
|
print "\nProceed with deletion? (y/N): "
|
|
456
|
-
confirmation = $stdin.gets.chomp.downcase
|
|
485
|
+
confirmation = $stdin.gets.to_s.chomp.downcase
|
|
457
486
|
|
|
458
487
|
unless confirmation == "y" || confirmation == "yes"
|
|
459
488
|
puts "\n✗ Cleanup cancelled"
|
|
@@ -463,7 +492,9 @@ namespace :error_dashboard do
|
|
|
463
492
|
|
|
464
493
|
puts "\nDeleting errors..."
|
|
465
494
|
start_time = Time.current
|
|
466
|
-
|
|
495
|
+
# Dependents first, in batches: occurrences, comments and cascade patterns
|
|
496
|
+
# hold foreign keys to the log, so a plain delete_all failed on them.
|
|
497
|
+
deleted = RailsErrorDashboard::RetentionCleanupJob.delete_with_dependents(scope)
|
|
467
498
|
# delete_all skips callbacks, and the stat cards are cached.
|
|
468
499
|
RailsErrorDashboard::Services::AnalyticsCacheManager.clear
|
|
469
500
|
elapsed = (Time.current - start_time).round(2)
|
|
@@ -16,75 +16,69 @@ namespace :rails_error_dashboard do
|
|
|
16
16
|
puts "=" * 80
|
|
17
17
|
puts "\n"
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"rails_error_dashboard_error_baselines",
|
|
25
|
-
"rails_error_dashboard_error_logs"
|
|
26
|
-
]
|
|
19
|
+
# The connection RED's models use: the separate or shared error database
|
|
20
|
+
# when there is one. Raises if the engine fell back to the primary one.
|
|
21
|
+
connection = RailsErrorDashboard::Commands::DropAllTables.connection
|
|
22
|
+
database = connection.pool.db_config.name
|
|
23
|
+
plan = RailsErrorDashboard::Queries::UninstallPlan.call(connection)
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
if existing_tables.empty?
|
|
33
|
-
puts "No Rails Error Dashboard tables found in the database."
|
|
25
|
+
if plan.empty?
|
|
26
|
+
puts "No Rails Error Dashboard tables found in the '#{database}' database."
|
|
34
27
|
puts "\n"
|
|
35
|
-
|
|
28
|
+
next
|
|
36
29
|
end
|
|
37
30
|
|
|
38
|
-
puts "The following tables will be PERMANENTLY DELETED:"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
puts " • #{table} (#{record_count} records)"
|
|
31
|
+
puts "The following tables in the '#{database}' database will be PERMANENTLY DELETED:"
|
|
32
|
+
plan.each do |entry|
|
|
33
|
+
puts " • #{entry[:table]} (#{entry[:rows] || '?'} records)"
|
|
42
34
|
end
|
|
43
35
|
puts "\n"
|
|
36
|
+
|
|
37
|
+
if plan.any? { |entry| entry[:table] == "rails_error_dashboard_applications" }
|
|
38
|
+
names = connection.select_values("SELECT name FROM #{connection.quote_table_name('rails_error_dashboard_applications')} ORDER BY name")
|
|
39
|
+
if names.size > 1
|
|
40
|
+
puts "⚠️ This database holds errors for #{names.size} applications, and ALL of them will be deleted:"
|
|
41
|
+
names.each { |name| puts " • #{name}" }
|
|
42
|
+
puts "\n"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
44
46
|
puts "⚠️ This action CANNOT be undone!"
|
|
45
47
|
puts "\n"
|
|
46
48
|
|
|
47
|
-
# Ask for confirmation
|
|
48
49
|
print "Type 'DELETE ALL DATA' to confirm: "
|
|
49
|
-
confirmation = $stdin.gets.chomp
|
|
50
|
+
confirmation = $stdin.gets.to_s.chomp
|
|
50
51
|
|
|
51
52
|
if confirmation != "DELETE ALL DATA"
|
|
52
53
|
puts "\n"
|
|
53
54
|
puts "Cancelled. No tables were dropped."
|
|
54
55
|
puts "\n"
|
|
55
|
-
|
|
56
|
+
next
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
puts "\n"
|
|
59
60
|
puts "Dropping tables..."
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
dropped_count = 0
|
|
63
|
-
existing_tables.reverse.each do |table|
|
|
64
|
-
begin
|
|
65
|
-
ActiveRecord::Base.connection.drop_table(table, if_exists: true)
|
|
66
|
-
puts " ✓ Dropped #{table}"
|
|
67
|
-
dropped_count += 1
|
|
68
|
-
rescue => e
|
|
69
|
-
puts " ✗ Failed to drop #{table}: #{e.message}"
|
|
70
|
-
end
|
|
71
|
-
end
|
|
61
|
+
dropped = RailsErrorDashboard::Commands::DropAllTables.call(connection: connection)
|
|
62
|
+
dropped.each { |table| puts " ✓ Dropped #{table}" }
|
|
72
63
|
|
|
73
64
|
puts "\n"
|
|
74
65
|
puts "=" * 80
|
|
75
|
-
puts " ✅ Successfully dropped #{
|
|
66
|
+
puts " ✅ Successfully dropped #{dropped.size} table(s)"
|
|
76
67
|
puts "=" * 80
|
|
77
68
|
puts "\n"
|
|
78
69
|
puts "Next steps:"
|
|
79
|
-
puts " 1. Remove
|
|
80
|
-
puts " 2.
|
|
81
|
-
puts " 3.
|
|
82
|
-
puts "
|
|
83
|
-
puts "
|
|
84
|
-
puts "
|
|
70
|
+
puts " 1. Remove the initializer: config/initializers/rails_error_dashboard.rb"
|
|
71
|
+
puts " 2. Remove the route from config/routes.rb (mount RailsErrorDashboard::Engine)"
|
|
72
|
+
puts " 3. Delete RED's migrations: db/migrate/*rails_error_dashboard*.rb"
|
|
73
|
+
puts " (and db/error_dashboard_migrate/ with a separate database)"
|
|
74
|
+
puts " 4. Separate database: remove its entry from config/database.yml"
|
|
75
|
+
puts " 5. Regenerate the schema file: bin/rails db:schema:dump"
|
|
76
|
+
puts " (otherwise db:schema:load recreates these tables)"
|
|
77
|
+
puts " 6. Remove gem 'rails_error_dashboard' from the Gemfile and run: bundle install"
|
|
78
|
+
puts " 7. Restart your Rails server"
|
|
85
79
|
puts "\n"
|
|
86
|
-
puts "Or use the automated uninstaller:"
|
|
87
|
-
puts " rails generate rails_error_dashboard:uninstall"
|
|
80
|
+
puts "Or use the automated uninstaller, before removing the gem:"
|
|
81
|
+
puts " bin/rails generate rails_error_dashboard:uninstall"
|
|
88
82
|
puts "\n"
|
|
89
83
|
end
|
|
90
84
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_error_dashboard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.14.
|
|
4
|
+
version: 0.14.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -429,7 +429,6 @@ files:
|
|
|
429
429
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
430
430
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
431
431
|
- lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb
|
|
432
|
-
- lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml
|
|
433
432
|
- lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb
|
|
434
433
|
- lib/rails_error_dashboard.rb
|
|
435
434
|
- lib/rails_error_dashboard/commands/add_error_comment.rb
|
|
@@ -442,6 +441,7 @@ files:
|
|
|
442
441
|
- lib/rails_error_dashboard/commands/batch_unmute_errors.rb
|
|
443
442
|
- lib/rails_error_dashboard/commands/calculate_cascade_probability.rb
|
|
444
443
|
- lib/rails_error_dashboard/commands/create_issue.rb
|
|
444
|
+
- lib/rails_error_dashboard/commands/drop_all_tables.rb
|
|
445
445
|
- lib/rails_error_dashboard/commands/find_or_create_application.rb
|
|
446
446
|
- lib/rails_error_dashboard/commands/find_or_increment_error.rb
|
|
447
447
|
- lib/rails_error_dashboard/commands/flush_rack_attack_events.rb
|
|
@@ -508,6 +508,7 @@ files:
|
|
|
508
508
|
- lib/rails_error_dashboard/queries/similar_errors.rb
|
|
509
509
|
- lib/rails_error_dashboard/queries/storm_history.rb
|
|
510
510
|
- lib/rails_error_dashboard/queries/swallowed_exception_summary.rb
|
|
511
|
+
- lib/rails_error_dashboard/queries/uninstall_plan.rb
|
|
511
512
|
- lib/rails_error_dashboard/queries/user_impact_summary.rb
|
|
512
513
|
- lib/rails_error_dashboard/services/ai_agent_classifier.rb
|
|
513
514
|
- lib/rails_error_dashboard/services/analytics_cache_manager.rb
|
|
@@ -563,6 +564,7 @@ files:
|
|
|
563
564
|
- lib/rails_error_dashboard/services/severity_classifier.rb
|
|
564
565
|
- lib/rails_error_dashboard/services/similarity_calculator.rb
|
|
565
566
|
- lib/rails_error_dashboard/services/slack_payload_builder.rb
|
|
567
|
+
- lib/rails_error_dashboard/services/solid_queue_config_check.rb
|
|
566
568
|
- lib/rails_error_dashboard/services/source_code_reader.rb
|
|
567
569
|
- lib/rails_error_dashboard/services/statistical_classifier.rb
|
|
568
570
|
- lib/rails_error_dashboard/services/storm_protection/circuit_breaker.rb
|
|
@@ -599,7 +601,7 @@ metadata:
|
|
|
599
601
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
600
602
|
post_install_message: |
|
|
601
603
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
602
|
-
RED (Rails Error Dashboard) v0.14.
|
|
604
|
+
RED (Rails Error Dashboard) v0.14.3
|
|
603
605
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
604
606
|
|
|
605
607
|
First install:
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Solid Queue configuration for RailsErrorDashboard
|
|
2
|
-
#
|
|
3
|
-
# This file configures workers for error logging and notifications.
|
|
4
|
-
# Adjust threads and processes based on your application's needs.
|
|
5
|
-
|
|
6
|
-
# Development environment - optimized for low resource usage
|
|
7
|
-
development:
|
|
8
|
-
workers:
|
|
9
|
-
# Error notifications (Slack, Email, Discord, PagerDuty, Webhooks)
|
|
10
|
-
- queues: error_notifications
|
|
11
|
-
threads: 2
|
|
12
|
-
processes: 1
|
|
13
|
-
polling_interval: 1
|
|
14
|
-
|
|
15
|
-
# Default queue (includes async error logging)
|
|
16
|
-
- queues: default
|
|
17
|
-
threads: 3
|
|
18
|
-
processes: 1
|
|
19
|
-
polling_interval: 1
|
|
20
|
-
|
|
21
|
-
# Test environment - minimal configuration
|
|
22
|
-
test:
|
|
23
|
-
workers:
|
|
24
|
-
- queues: "*"
|
|
25
|
-
threads: 1
|
|
26
|
-
processes: 1
|
|
27
|
-
polling_interval: 0.1
|
|
28
|
-
|
|
29
|
-
# Production environment - optimized for throughput
|
|
30
|
-
production:
|
|
31
|
-
workers:
|
|
32
|
-
# Error notifications - lower concurrency (external API calls)
|
|
33
|
-
- queues: error_notifications
|
|
34
|
-
threads: 3
|
|
35
|
-
processes: 1
|
|
36
|
-
polling_interval: 0.5
|
|
37
|
-
|
|
38
|
-
# Default queue - higher concurrency (database operations)
|
|
39
|
-
- queues: default
|
|
40
|
-
threads: 5
|
|
41
|
-
processes: 2
|
|
42
|
-
polling_interval: 0.5
|
|
43
|
-
|
|
44
|
-
# Staging environment - similar to production but scaled down
|
|
45
|
-
staging:
|
|
46
|
-
workers:
|
|
47
|
-
- queues: error_notifications
|
|
48
|
-
threads: 2
|
|
49
|
-
processes: 1
|
|
50
|
-
polling_interval: 0.5
|
|
51
|
-
|
|
52
|
-
- queues: default
|
|
53
|
-
threads: 3
|
|
54
|
-
processes: 1
|
|
55
|
-
polling_interval: 0.5
|