rails_error_dashboard 0.14.2 → 0.14.4
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 +36 -18
- data/app/helpers/rails_error_dashboard/application_helper.rb +8 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +27 -23
- data/app/views/layouts/rails_error_dashboard.html.erb +13 -0
- data/config/locales/de.yml +4 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/es.yml +4 -0
- data/config/locales/fr.yml +4 -0
- data/config/locales/it.yml +4 -0
- data/config/locales/ja.yml +4 -0
- data/config/locales/pl.yml +4 -0
- data/config/locales/pt-BR.yml +4 -0
- data/config/locales/ru.yml +4 -0
- data/config/locales/uk.yml +4 -0
- data/config/locales/zh-CN.yml +4 -0
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +9 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +45 -50
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +17 -10
- data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +41 -21
- data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +79 -66
- data/lib/rails_error_dashboard/commands/drop_all_tables.rb +77 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +5 -2
- data/lib/rails_error_dashboard/configuration.rb +67 -4
- data/lib/rails_error_dashboard/engine.rb +10 -4
- data/lib/rails_error_dashboard/logger.rb +11 -3
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +15 -43
- data/lib/rails_error_dashboard/plugin.rb +30 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +3 -1
- data/lib/rails_error_dashboard/queries/uninstall_plan.rb +63 -0
- data/lib/rails_error_dashboard/services/exception_filter.rb +4 -1
- data/lib/rails_error_dashboard/services/solid_queue_config_check.rb +261 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +3 -0
- data/lib/tasks/error_dashboard.rake +77 -5
- 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
|
@@ -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
|
|
@@ -181,7 +186,8 @@ RailsErrorDashboard.configure do |config|
|
|
|
181
186
|
<% if @enable_error_sampling -%>
|
|
182
187
|
# Error Sampling - ENABLED
|
|
183
188
|
# Samples non-critical errors to reduce storage volume.
|
|
184
|
-
# Critical
|
|
189
|
+
# Critical errors are always logged, regardless of this setting. So is the first
|
|
190
|
+
# occurrence of each error in each process.
|
|
185
191
|
# 0.5 = log 50% of non-critical occurrences — halves storage while keeping
|
|
186
192
|
# occurrence counts meaningful and error patterns visible.
|
|
187
193
|
# Tune lower (e.g. 0.1) if one error is flooding the DB; set to 1.0 to log everything.
|
|
@@ -239,7 +245,7 @@ RailsErrorDashboard.configure do |config|
|
|
|
239
245
|
# Automatically detect when error rates exceed normal patterns
|
|
240
246
|
config.enable_baseline_alerts = true
|
|
241
247
|
config.baseline_alert_threshold_std_devs = 2.0 # Alert when > 2 std devs above baseline
|
|
242
|
-
config.baseline_alert_severities = [ :critical, :high ]
|
|
248
|
+
config.baseline_alert_severities = [ :critical, :high ] # Only errors of these severities alert
|
|
243
249
|
config.baseline_alert_cooldown_minutes = 120 # 2 hours between alerts
|
|
244
250
|
# To disable: Set config.enable_baseline_alerts = false
|
|
245
251
|
|
|
@@ -248,7 +254,7 @@ RailsErrorDashboard.configure do |config|
|
|
|
248
254
|
# To enable: Set config.enable_baseline_alerts = true
|
|
249
255
|
config.enable_baseline_alerts = false
|
|
250
256
|
# config.baseline_alert_threshold_std_devs = 2.0
|
|
251
|
-
# config.baseline_alert_severities = [ :critical, :high ]
|
|
257
|
+
# config.baseline_alert_severities = [ :critical, :high ] # Only errors of these severities alert
|
|
252
258
|
# config.baseline_alert_cooldown_minutes = 120
|
|
253
259
|
|
|
254
260
|
<% end -%>
|
|
@@ -446,7 +452,8 @@ RailsErrorDashboard.configure do |config|
|
|
|
446
452
|
config.enable_internal_logging = false
|
|
447
453
|
|
|
448
454
|
# Log level (default: :silent)
|
|
449
|
-
# Options: :debug, :info, :warn, :error, :silent
|
|
455
|
+
# Options: :debug, :info, :warn, :error, :fatal, :silent
|
|
456
|
+
# (RED has no fatal-level messages, so :fatal logs nothing, like :silent)
|
|
450
457
|
config.log_level = :silent
|
|
451
458
|
|
|
452
459
|
# 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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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"
|
|
96
|
+
|
|
97
|
+
say "Step 7: Remove from Gemfile", :yellow
|
|
98
|
+
say " Remove: gem 'rails_error_dashboard'"
|
|
99
|
+
say " Run: bundle install"
|
|
100
|
+
say "\n"
|
|
86
101
|
|
|
87
|
-
say "Step
|
|
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
|
|
|
@@ -237,13 +236,22 @@ module RailsErrorDashboard
|
|
|
237
236
|
say "Remaining manual steps:", :cyan
|
|
238
237
|
say "\n"
|
|
239
238
|
|
|
240
|
-
|
|
239
|
+
unless options[:keep_data]
|
|
240
|
+
say "1. Regenerate the schema file:", :yellow
|
|
241
|
+
say " Run: bin/rails db:schema:dump (otherwise db:schema:load recreates the tables)"
|
|
242
|
+
say "\n"
|
|
243
|
+
|
|
244
|
+
say "2. Separate database only: remove its entry from config/database.yml", :yellow
|
|
245
|
+
say "\n"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
say "#{options[:keep_data] ? 1 : 3}. Remove from Gemfile:", :yellow
|
|
241
249
|
say " Open: Gemfile"
|
|
242
250
|
say " Remove: gem 'rails_error_dashboard'"
|
|
243
251
|
say " Run: bundle install"
|
|
244
252
|
say "\n"
|
|
245
253
|
|
|
246
|
-
say "2. Restart your application:", :yellow
|
|
254
|
+
say "#{options[:keep_data] ? 2 : 4}. Restart your application:", :yellow
|
|
247
255
|
say " Run: rails restart"
|
|
248
256
|
say " Or: kill and restart your server process"
|
|
249
257
|
say "\n"
|
|
@@ -282,7 +290,7 @@ module RailsErrorDashboard
|
|
|
282
290
|
end
|
|
283
291
|
|
|
284
292
|
def migration_files
|
|
285
|
-
Dir.glob(
|
|
293
|
+
Dir.glob(MIGRATION_GLOBS)
|
|
286
294
|
end
|
|
287
295
|
|
|
288
296
|
def migration_count
|
|
@@ -290,22 +298,27 @@ module RailsErrorDashboard
|
|
|
290
298
|
end
|
|
291
299
|
|
|
292
300
|
def tables_exist?
|
|
293
|
-
|
|
294
|
-
table_names.any? { |table| ActiveRecord::Base.connection.table_exists?(table) rescue false }
|
|
301
|
+
table_names.any?
|
|
295
302
|
end
|
|
296
303
|
|
|
304
|
+
# The tables Queries::UninstallPlan finds on the error database, in drop
|
|
305
|
+
# order. Empty (with the reason shown) when that database can't be reached.
|
|
297
306
|
def table_names
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
"
|
|
303
|
-
|
|
304
|
-
|
|
307
|
+
@table_names ||= begin
|
|
308
|
+
RailsErrorDashboard::Queries::UninstallPlan.call(drop_connection).map { |entry| entry[:table] }
|
|
309
|
+
rescue => e
|
|
310
|
+
@database_error = e.message
|
|
311
|
+
say " ⚠️ Could not inspect the error database: #{e.message}", :yellow
|
|
312
|
+
[]
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def drop_connection
|
|
317
|
+
RailsErrorDashboard::Commands::DropAllTables.connection
|
|
305
318
|
end
|
|
306
319
|
|
|
307
320
|
def table_count
|
|
308
|
-
table_names.count
|
|
321
|
+
table_names.count
|
|
309
322
|
end
|
|
310
323
|
|
|
311
324
|
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
|
|
@@ -878,8 +878,11 @@ module RailsErrorDashboard
|
|
|
878
878
|
# Return if no anomaly detected
|
|
879
879
|
return unless anomaly[:anomaly]
|
|
880
880
|
|
|
881
|
-
#
|
|
882
|
-
|
|
881
|
+
# baseline_alert_severities lists ERROR severities (critical/high/
|
|
882
|
+
# medium/low, as validated), not anomaly levels. Comparing it with
|
|
883
|
+
# anomaly[:level] (elevated/high/critical) skipped every anomaly below
|
|
884
|
+
# threshold + 1 sigma, and :medium/:low never matched at all.
|
|
885
|
+
return unless config.baseline_alert_severities.include?(error_log.severity)
|
|
883
886
|
|
|
884
887
|
# Enqueue alert job (which will handle throttling)
|
|
885
888
|
BaselineAlertJob.perform_later(error_log.id, anomaly, ApplicationJob.enqueue_locale)
|
|
@@ -8,6 +8,14 @@ module RailsErrorDashboard
|
|
|
8
8
|
DEFAULT_DASHBOARD_USERNAME = "gandalf"
|
|
9
9
|
DEFAULT_DASHBOARD_PASSWORD = "youshallnotpass"
|
|
10
10
|
|
|
11
|
+
# Settings that hold credentials. #inspect masks them, so printing the
|
|
12
|
+
# configuration in a console (IRB shows results with pp, which uses
|
|
13
|
+
# #inspect) or pasting it into an issue doesn't leak them.
|
|
14
|
+
SECRET_ATTRIBUTES = %i[
|
|
15
|
+
dashboard_password slack_webhook_url discord_webhook_url pagerduty_integration_key
|
|
16
|
+
webhook_urls issue_tracker_token issue_webhook_secret llm_api_key
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
11
19
|
# Dashboard authentication (always required)
|
|
12
20
|
attr_accessor :dashboard_username
|
|
13
21
|
attr_accessor :dashboard_password
|
|
@@ -351,7 +359,7 @@ module RailsErrorDashboard
|
|
|
351
359
|
|
|
352
360
|
# Rate limiting defaults
|
|
353
361
|
@enable_rate_limiting = false # OFF by default (opt-in)
|
|
354
|
-
@rate_limit_per_minute =
|
|
362
|
+
@rate_limit_per_minute = 300 # Requests per minute per IP, per dashboard path
|
|
355
363
|
|
|
356
364
|
# Enhanced metrics defaults
|
|
357
365
|
@app_version = ENV["APP_VERSION"]
|
|
@@ -380,7 +388,7 @@ module RailsErrorDashboard
|
|
|
380
388
|
# Baseline alert defaults
|
|
381
389
|
@enable_baseline_alerts = false # OFF by default (opt-in)
|
|
382
390
|
@baseline_alert_threshold_std_devs = ENV.fetch("BASELINE_ALERT_THRESHOLD", "2.0").to_f
|
|
383
|
-
@baseline_alert_severities = [ :critical, :high ] # Alert
|
|
391
|
+
@baseline_alert_severities = [ :critical, :high ] # Alert only for errors of these severities
|
|
384
392
|
@baseline_alert_cooldown_minutes = ENV.fetch("BASELINE_ALERT_COOLDOWN", "120").to_i
|
|
385
393
|
|
|
386
394
|
# Source code integration defaults - OFF by default (opt-in)
|
|
@@ -495,7 +503,7 @@ module RailsErrorDashboard
|
|
|
495
503
|
|
|
496
504
|
# Internal logging defaults - SILENT by default
|
|
497
505
|
@enable_internal_logging = false # Opt-in for debugging
|
|
498
|
-
@log_level = :silent # Silent by default
|
|
506
|
+
@log_level = :silent # Silent by default; see Logger::LOG_LEVELS for the levels
|
|
499
507
|
|
|
500
508
|
# Dashboard UI
|
|
501
509
|
@accent_color = :crimson # :crimson, :ruby, :ember, :violet
|
|
@@ -522,6 +530,30 @@ module RailsErrorDashboard
|
|
|
522
530
|
initialize
|
|
523
531
|
end
|
|
524
532
|
|
|
533
|
+
# Whether config/database.yml has the entry config.database names for this
|
|
534
|
+
# environment. Without it the engine skips connects_to and RED uses the
|
|
535
|
+
# main database, where its tables usually don't exist, so nothing is
|
|
536
|
+
# recorded. The engine and error_dashboard:verify both ask this.
|
|
537
|
+
#
|
|
538
|
+
# @param env_name [String] the Rails environment
|
|
539
|
+
# @return [Boolean]
|
|
540
|
+
def separate_database_entry?(env_name = Rails.env)
|
|
541
|
+
name = (database || :error_dashboard).to_s
|
|
542
|
+
ActiveRecord::Base.configurations.configs_for(env_name: env_name.to_s).any? { |config| config.name == name }
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Like Object#inspect, with every credential in SECRET_ATTRIBUTES replaced
|
|
546
|
+
# by [FILTERED] when set. Unset ones still show nil or [], which is useful
|
|
547
|
+
# when checking why a channel stays off.
|
|
548
|
+
def inspect
|
|
549
|
+
attributes = instance_variables.map do |ivar|
|
|
550
|
+
value = instance_variable_get(ivar)
|
|
551
|
+
secret = SECRET_ATTRIBUTES.include?(ivar.to_s.delete_prefix("@").to_sym) && value.present?
|
|
552
|
+
"#{ivar}=#{secret ? '[FILTERED]' : value.inspect}"
|
|
553
|
+
end
|
|
554
|
+
"#<#{self.class.name} #{attributes.join(', ')}>"
|
|
555
|
+
end
|
|
556
|
+
|
|
525
557
|
# Validate configuration values
|
|
526
558
|
# Raises ConfigurationError if any validation fails
|
|
527
559
|
# Logs warnings for non-fatal issues (e.g., Ruby version incompatibilities)
|
|
@@ -554,6 +586,14 @@ module RailsErrorDashboard
|
|
|
554
586
|
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
587
|
end
|
|
556
588
|
|
|
589
|
+
# In development the app boots on a blank credential, and the login then
|
|
590
|
+
# denies everyone: say so, or the developer is locked out with no clue
|
|
591
|
+
# why. Development only, because a test run boots on every CI job.
|
|
592
|
+
if default_credentials? && credentials_problem == :blank &&
|
|
593
|
+
defined?(Rails) && Rails.respond_to?(:env) && Rails.env.development?
|
|
594
|
+
warnings.concat(blank_credential_warnings)
|
|
595
|
+
end
|
|
596
|
+
|
|
557
597
|
# Validate sampling_rate (must be between 0.0 and 1.0)
|
|
558
598
|
if sampling_rate && (sampling_rate < 0.0 || sampling_rate > 1.0)
|
|
559
599
|
errors << "sampling_rate must be between 0.0 and 1.0 (got: #{sampling_rate})"
|
|
@@ -786,7 +826,9 @@ module RailsErrorDashboard
|
|
|
786
826
|
|
|
787
827
|
# Validate log level (must be valid symbol)
|
|
788
828
|
if log_level
|
|
789
|
-
|
|
829
|
+
# Referenced here, not as a class-body constant: this file loads before
|
|
830
|
+
# logger.rb, and a bare Logger in the class body would be ::Logger.
|
|
831
|
+
valid_log_levels = RailsErrorDashboard::Logger::LOG_LEVELS.keys
|
|
790
832
|
unless valid_log_levels.include?(log_level)
|
|
791
833
|
errors << "log_level must be one of #{valid_log_levels.inspect} (got: #{log_level.inspect})"
|
|
792
834
|
end
|
|
@@ -996,6 +1038,27 @@ module RailsErrorDashboard
|
|
|
996
1038
|
!Rails.env.development? && !Rails.env.test?
|
|
997
1039
|
end
|
|
998
1040
|
|
|
1041
|
+
# One warning per blank credential, naming where the blank came from: an
|
|
1042
|
+
# environment variable that is set but empty, or the initializer.
|
|
1043
|
+
#
|
|
1044
|
+
# @return [Array<String>]
|
|
1045
|
+
private def blank_credential_warnings
|
|
1046
|
+
{
|
|
1047
|
+
"ERROR_DASHBOARD_USER" => [ :dashboard_username, dashboard_username ],
|
|
1048
|
+
"ERROR_DASHBOARD_PASSWORD" => [ :dashboard_password, dashboard_password ]
|
|
1049
|
+
}.filter_map do |variable, (setting, value)|
|
|
1050
|
+
next unless self.class.blank_credential?(value)
|
|
1051
|
+
|
|
1052
|
+
if ENV.key?(variable) && ENV[variable].to_s == value.to_s
|
|
1053
|
+
"#{variable} is set but empty, so every dashboard login will be denied. " \
|
|
1054
|
+
"Unset it to use the development default, or give it a value."
|
|
1055
|
+
else
|
|
1056
|
+
"config.#{setting} is blank, so every dashboard login will be denied. " \
|
|
1057
|
+
"Remove that line from the initializer to use the development default, or give it a value."
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
1061
|
+
|
|
999
1062
|
# Resolve the effective issue tracker provider (auto-detect from git_repository_url).
|
|
1000
1063
|
# Linear is never auto-detected (it is not a git forge) — set issue_tracker_provider
|
|
1001
1064
|
# explicitly.
|
|
@@ -11,10 +11,11 @@ module RailsErrorDashboard
|
|
|
11
11
|
|
|
12
12
|
# Guard: skip connects_to if the database config doesn't exist yet in database.yml.
|
|
13
13
|
# This happens during `rails generate` when the initializer was just created but
|
|
14
|
-
# the user hasn't added the database.yml entry yet.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
# the user hasn't added the database.yml entry yet. Logged at error level:
|
|
15
|
+
# RED then uses the main database, and unless that has RED's tables no
|
|
16
|
+
# error is recorded. error_dashboard:verify reports it as FAILED.
|
|
17
|
+
unless RailsErrorDashboard.configuration.separate_database_entry?
|
|
18
|
+
Rails.logger.error "[Rails Error Dashboard] Separate database '#{database_name}' is not configured in database.yml for the '#{Rails.env}' environment. Skipping connects_to: errors are not recorded until it is added. See https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/DATABASE_OPTIONS.md"
|
|
18
19
|
next
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -76,6 +77,11 @@ module RailsErrorDashboard
|
|
|
76
77
|
# configured: then it is never consulted.
|
|
77
78
|
RailsErrorDashboard.detected_git_sha if RailsErrorDashboard.configuration.git_sha.blank?
|
|
78
79
|
|
|
80
|
+
# One error-level line if Solid Queue runs RED's jobs with a
|
|
81
|
+
# config/queue.yml that never would (no dispatcher, no worker for RED's
|
|
82
|
+
# queues). Reads the file once; never raises. The dashboard shows a banner.
|
|
83
|
+
RailsErrorDashboard::Services::SolidQueueConfigCheck.log_current_problems
|
|
84
|
+
|
|
79
85
|
if RailsErrorDashboard.configuration.enable_error_subscriber
|
|
80
86
|
Rails.error.subscribe(RailsErrorDashboard::ErrorReporter.new)
|
|
81
87
|
end
|