rails_error_dashboard 0.11.5 → 0.11.7
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 +3 -2
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +31 -3
- data/app/jobs/rails_error_dashboard/baseline_calculation_job.rb +21 -0
- data/app/models/rails_error_dashboard/error_log.rb +4 -7
- data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
- data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +7 -0
- data/db/migrate/20260908000001_add_release_to_error_occurrences.rb +45 -0
- data/db/migrate/20260908220001_add_missing_postgresql_indexes_to_error_logs.rb +42 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +20 -9
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +63 -28
- data/lib/rails_error_dashboard/commands/log_error.rb +11 -2
- data/lib/rails_error_dashboard/configuration.rb +7 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +4 -1
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +39 -4
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +5 -16
- data/lib/rails_error_dashboard/queries/release_timeline.rb +72 -1
- data/lib/rails_error_dashboard/services/baseline_calculator.rb +75 -62
- data/lib/rails_error_dashboard/services/error_hash_generator.rb +9 -0
- data/lib/rails_error_dashboard/services/local_variable_capturer.rb +33 -3
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +83 -22
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +52 -12
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +61 -2
- data/lib/rails_error_dashboard/value_objects/error_context.rb +19 -2
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/tasks/error_dashboard.rake +24 -0
- metadata +5 -2
|
@@ -8,11 +8,18 @@ module RailsErrorDashboard
|
|
|
8
8
|
# Puma stats, job queue, RubyVM/YJIT, ActionCable, file descriptors, system load,
|
|
9
9
|
# system memory pressure, GC context, and TCP connection states.
|
|
10
10
|
#
|
|
11
|
-
# NOT memoized — fresh data every call (unlike EnvironmentSnapshot)
|
|
11
|
+
# NOT memoized — fresh data every call (unlike EnvironmentSnapshot), with
|
|
12
|
+
# one exception: job-queue depth counts are queries against the queue
|
|
13
|
+
# store, so they are cached per process for
|
|
14
|
+
# config.system_health_queue_stats_cache_seconds (failures included), with
|
|
15
|
+
# one refresh in flight at a time (and can be switched off with
|
|
16
|
+
# config.system_health_queue_stats = false).
|
|
12
17
|
# Every metric call individually wrapped in rescue => nil.
|
|
13
18
|
#
|
|
14
19
|
# Safety contract (from HOST_APP_SAFETY.md):
|
|
15
|
-
# -
|
|
20
|
+
# - In-process metrics < 1ms budget (~0.3ms typical on Linux). The
|
|
21
|
+
# queue-depth counts are the documented exception: their latency is
|
|
22
|
+
# the queue store's, which is why they are cached and optional.
|
|
16
23
|
# - NEVER ObjectSpace.each_object or ObjectSpace.count_objects (heap scan)
|
|
17
24
|
# - NEVER Thread.list.map(&:backtrace) (GVL hold)
|
|
18
25
|
# - Thread.list.count only (O(1), safe)
|
|
@@ -133,7 +140,59 @@ module RailsErrorDashboard
|
|
|
133
140
|
|
|
134
141
|
# Auto-detect and capture job queue stats
|
|
135
142
|
# @return [Hash, nil] Job queue stats with :adapter key, or nil
|
|
143
|
+
# Process-wide cache for the queue-depth counts: { at: monotonic, stats: Hash|nil }.
|
|
144
|
+
# A nil stats entry is a FAILED collection, cached for the same interval
|
|
145
|
+
# so a broken queue store is not re-queried on every error.
|
|
146
|
+
def self.queue_stats_cache
|
|
147
|
+
@queue_stats_cache ||= Concurrent::AtomicReference.new(nil)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Single-flight guard for the refresh. Never blocks: a thread that finds
|
|
151
|
+
# a refresh already running serves the stale entry (or nil) instead of
|
|
152
|
+
# starting a second one, so a burst of errors on a cold cache runs the
|
|
153
|
+
# queue counts once, not once per thread.
|
|
154
|
+
def self.queue_stats_refresh_lock
|
|
155
|
+
@queue_stats_refresh_lock ||= Mutex.new
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.reset_queue_stats_cache!
|
|
159
|
+
queue_stats_cache.set(nil)
|
|
160
|
+
end
|
|
161
|
+
|
|
136
162
|
def job_queue_stats
|
|
163
|
+
config = RailsErrorDashboard.configuration
|
|
164
|
+
return nil unless config.system_health_queue_stats
|
|
165
|
+
|
|
166
|
+
ttl = config.system_health_queue_stats_cache_seconds.to_f
|
|
167
|
+
return collect_job_queue_stats if ttl <= 0
|
|
168
|
+
|
|
169
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
170
|
+
cached = self.class.queue_stats_cache.get
|
|
171
|
+
return present_cached(cached, now) if cached && (now - cached[:at]) < ttl
|
|
172
|
+
|
|
173
|
+
lock = self.class.queue_stats_refresh_lock
|
|
174
|
+
return present_cached(cached, now, stale: true) unless lock.try_lock
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
stats = collect_job_queue_stats
|
|
178
|
+
self.class.queue_stats_cache.set({ at: now, stats: stats })
|
|
179
|
+
stats
|
|
180
|
+
ensure
|
|
181
|
+
lock.unlock
|
|
182
|
+
end
|
|
183
|
+
rescue => e
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def present_cached(cached, now, stale: false)
|
|
188
|
+
return nil unless cached && cached[:stats]
|
|
189
|
+
|
|
190
|
+
presented = cached[:stats].merge(cached_age_seconds: (now - cached[:at]).round(1))
|
|
191
|
+
presented[:stale] = true if stale
|
|
192
|
+
presented
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def collect_job_queue_stats
|
|
137
196
|
if defined?(::Sidekiq::Stats)
|
|
138
197
|
sidekiq_stats
|
|
139
198
|
elsif defined?(::SolidQueue)
|
|
@@ -7,7 +7,7 @@ module RailsErrorDashboard
|
|
|
7
7
|
class ErrorContext
|
|
8
8
|
attr_reader :user_id, :request_url, :request_params, :user_agent, :ip_address, :platform,
|
|
9
9
|
:controller_name, :action_name, :request_id, :session_id,
|
|
10
|
-
:http_method, :hostname, :content_type, :request_duration_ms
|
|
10
|
+
:http_method, :hostname, :content_type, :request_duration_ms, :environment
|
|
11
11
|
|
|
12
12
|
def initialize(context, source = nil)
|
|
13
13
|
@context = context
|
|
@@ -27,8 +27,15 @@ module RailsErrorDashboard
|
|
|
27
27
|
@hostname = extract_hostname
|
|
28
28
|
@content_type = extract_content_type
|
|
29
29
|
@request_duration_ms = extract_request_duration_ms
|
|
30
|
+
@environment = extract_environment
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
# Everything a second ErrorContext needs to rebuild THIS context from a
|
|
34
|
+
# plain hash. ErrorReporter hands `to_h` to LogError, which constructs a
|
|
35
|
+
# new ErrorContext from it, so any reader that is not represented here
|
|
36
|
+
# is silently nil on the far side of that hop. request_id/session_id
|
|
37
|
+
# were exactly that: extracted from the request, dropped here, and every
|
|
38
|
+
# occurrence on the main capture path stored nil for both.
|
|
32
39
|
def to_h
|
|
33
40
|
{
|
|
34
41
|
user_id: user_id,
|
|
@@ -39,10 +46,13 @@ module RailsErrorDashboard
|
|
|
39
46
|
platform: platform,
|
|
40
47
|
controller_name: controller_name,
|
|
41
48
|
action_name: action_name,
|
|
49
|
+
request_id: request_id,
|
|
50
|
+
session_id: session_id,
|
|
42
51
|
http_method: http_method,
|
|
43
52
|
hostname: hostname,
|
|
44
53
|
content_type: content_type,
|
|
45
|
-
request_duration_ms: request_duration_ms
|
|
54
|
+
request_duration_ms: request_duration_ms,
|
|
55
|
+
environment: environment
|
|
46
56
|
}
|
|
47
57
|
end
|
|
48
58
|
|
|
@@ -246,6 +256,13 @@ module RailsErrorDashboard
|
|
|
246
256
|
nil
|
|
247
257
|
end
|
|
248
258
|
|
|
259
|
+
# An explicit environment from the caller (a sender attributing an event
|
|
260
|
+
# to its own environment). nil means "resolve from configuration".
|
|
261
|
+
def extract_environment
|
|
262
|
+
value = @context[:environment].to_s.strip
|
|
263
|
+
value.empty? ? nil : value
|
|
264
|
+
end
|
|
265
|
+
|
|
249
266
|
# Auto-detect user_id from ActiveSupport::CurrentAttributes
|
|
250
267
|
# Checks for common patterns: Current.user, Current.account (with .id)
|
|
251
268
|
# Returns nil if CurrentAttributes is not used or user is not set
|
|
@@ -52,6 +52,30 @@ namespace :error_dashboard do
|
|
|
52
52
|
checks_failed += 1
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# 3b. MySQL only: named-zone conversion needs the server's time zone
|
|
56
|
+
# tables; without them groupdate (charts, baselines) raises.
|
|
57
|
+
begin
|
|
58
|
+
conn = RailsErrorDashboard::ErrorLogsRecord.connection
|
|
59
|
+
if conn.adapter_name.match?(/mysql|trilogy/i)
|
|
60
|
+
print " MySQL time zone tables... "
|
|
61
|
+
zone = (Time.zone || ActiveSupport::TimeZone["UTC"]).tzinfo.name
|
|
62
|
+
converted = conn.select_value("SELECT CONVERT_TZ(NOW(), '+00:00', #{conn.quote(zone)})")
|
|
63
|
+
if converted.nil?
|
|
64
|
+
puts "MISSING"
|
|
65
|
+
puts " CONVERT_TZ to '#{zone}' returned NULL: the server has no time zone tables,"
|
|
66
|
+
puts " so the dashboard's time-bucketed charts and baselines will raise."
|
|
67
|
+
puts " Fix (on the MySQL server): mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql"
|
|
68
|
+
puts " See docs/guides/DATABASE_OPTIONS.md (MySQL)."
|
|
69
|
+
warnings += 1
|
|
70
|
+
else
|
|
71
|
+
puts "OK (#{zone})"
|
|
72
|
+
checks_passed += 1
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
rescue => e
|
|
76
|
+
puts "SKIPPED (#{e.message.truncate(60)})"
|
|
77
|
+
end
|
|
78
|
+
|
|
55
79
|
# 4. Tables check
|
|
56
80
|
print " Required tables... "
|
|
57
81
|
required_tables = %w[
|
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.11.
|
|
4
|
+
version: 0.11.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -279,6 +279,7 @@ files:
|
|
|
279
279
|
- app/jobs/rails_error_dashboard/application_job.rb
|
|
280
280
|
- app/jobs/rails_error_dashboard/async_error_logging_job.rb
|
|
281
281
|
- app/jobs/rails_error_dashboard/baseline_alert_job.rb
|
|
282
|
+
- app/jobs/rails_error_dashboard/baseline_calculation_job.rb
|
|
282
283
|
- app/jobs/rails_error_dashboard/close_linked_issue_job.rb
|
|
283
284
|
- app/jobs/rails_error_dashboard/concerns/localized_job.rb
|
|
284
285
|
- app/jobs/rails_error_dashboard/create_issue_job.rb
|
|
@@ -407,6 +408,8 @@ files:
|
|
|
407
408
|
- db/migrate/20260730000001_create_rails_error_dashboard_rack_attack_events.rb
|
|
408
409
|
- db/migrate/20260824000001_add_user_agent_to_rack_attack_events.rb
|
|
409
410
|
- db/migrate/20260826000001_add_environment_to_error_logs.rb
|
|
411
|
+
- db/migrate/20260908000001_add_release_to_error_occurrences.rb
|
|
412
|
+
- db/migrate/20260908220001_add_missing_postgresql_indexes_to_error_logs.rb
|
|
410
413
|
- lib/generators/rails_error_dashboard/install/install_generator.rb
|
|
411
414
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
412
415
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
@@ -575,7 +578,7 @@ metadata:
|
|
|
575
578
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
576
579
|
post_install_message: |
|
|
577
580
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
578
|
-
RED (Rails Error Dashboard) v0.11.
|
|
581
|
+
RED (Rails Error Dashboard) v0.11.7
|
|
579
582
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
580
583
|
|
|
581
584
|
First install:
|