rails_error_dashboard 0.3.1 → 0.4.1
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 +236 -841
- data/app/controllers/rails_error_dashboard/errors_controller.rb +89 -0
- data/app/jobs/rails_error_dashboard/swallowed_exception_flush_job.rb +32 -0
- data/app/models/rails_error_dashboard/diagnostic_dump.rb +14 -0
- data/app/models/rails_error_dashboard/swallowed_exception.rb +38 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +21 -0
- data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +55 -0
- data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +46 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +48 -0
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +182 -0
- data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +133 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +4 -0
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +126 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +33 -0
- data/db/migrate/20260306000001_add_local_variables_to_error_logs.rb +13 -0
- data/db/migrate/20260306000002_add_instance_variables_to_error_logs.rb +7 -0
- data/db/migrate/20260306000003_create_rails_error_dashboard_swallowed_exceptions.rb +34 -0
- data/db/migrate/20260307000001_create_rails_error_dashboard_diagnostic_dumps.rb +17 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +32 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +47 -0
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +103 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +68 -0
- data/lib/rails_error_dashboard/configuration.rb +122 -0
- data/lib/rails_error_dashboard/engine.rb +24 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +32 -11
- data/lib/rails_error_dashboard/queries/rack_attack_summary.rb +90 -0
- data/lib/rails_error_dashboard/queries/swallowed_exception_summary.rb +97 -0
- data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +12 -0
- data/lib/rails_error_dashboard/services/crash_capture.rb +234 -0
- data/lib/rails_error_dashboard/services/diagnostic_dump_generator.rb +98 -0
- data/lib/rails_error_dashboard/services/local_variable_capturer.rb +207 -0
- data/lib/rails_error_dashboard/services/swallowed_exception_tracker.rb +277 -0
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +33 -0
- data/lib/rails_error_dashboard/services/variable_serializer.rb +326 -0
- data/lib/rails_error_dashboard/subscribers/rack_attack_subscriber.rb +94 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +9 -0
- data/lib/tasks/error_dashboard.rake +34 -0
- metadata +23 -2
|
@@ -348,6 +348,95 @@ module RailsErrorDashboard
|
|
|
348
348
|
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
349
349
|
end
|
|
350
350
|
|
|
351
|
+
def swallowed_exceptions
|
|
352
|
+
unless RailsErrorDashboard.configuration.detect_swallowed_exceptions
|
|
353
|
+
# On Ruby < 3.3, validate! auto-disables this feature — tell the user why
|
|
354
|
+
if RUBY_VERSION < "3.3"
|
|
355
|
+
flash[:alert] = "Swallowed exception detection requires Ruby 3.3+ (you have #{RUBY_VERSION}). Upgrade Ruby to use this feature."
|
|
356
|
+
else
|
|
357
|
+
flash[:alert] = "Swallowed exception detection is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
|
|
358
|
+
end
|
|
359
|
+
redirect_to errors_path
|
|
360
|
+
return
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
days = (params[:days] || 30).to_i
|
|
364
|
+
@days = days
|
|
365
|
+
result = Queries::SwallowedExceptionSummary.call(days, application_id: @current_application_id)
|
|
366
|
+
all_entries = result[:entries]
|
|
367
|
+
|
|
368
|
+
# Summary stats (computed before pagination)
|
|
369
|
+
@unique_count = all_entries.size
|
|
370
|
+
@total_rescue_count = all_entries.sum { |e| e[:rescue_count] }
|
|
371
|
+
@total_raise_count = all_entries.sum { |e| e[:raise_count] }
|
|
372
|
+
|
|
373
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def rack_attack_summary
|
|
377
|
+
unless RailsErrorDashboard.configuration.enable_rack_attack_tracking &&
|
|
378
|
+
RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
379
|
+
flash[:alert] = "Rack Attack tracking is not enabled. Enable enable_rack_attack_tracking and enable_breadcrumbs in config/initializers/rails_error_dashboard.rb"
|
|
380
|
+
redirect_to errors_path
|
|
381
|
+
return
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
days = (params[:days] || 30).to_i
|
|
385
|
+
@days = days
|
|
386
|
+
result = Queries::RackAttackSummary.call(days, application_id: @current_application_id)
|
|
387
|
+
all_events = result[:events]
|
|
388
|
+
|
|
389
|
+
# Summary stats (computed before pagination)
|
|
390
|
+
@unique_rules = all_events.size
|
|
391
|
+
@total_events = all_events.sum { |e| e[:count] }
|
|
392
|
+
@unique_ips = all_events.flat_map { |e| e[:ips] }.uniq.size
|
|
393
|
+
|
|
394
|
+
@pagy, @events = pagy(:offset, all_events, limit: params[:per_page] || 25)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def diagnostic_dumps
|
|
398
|
+
unless RailsErrorDashboard.configuration.enable_diagnostic_dump
|
|
399
|
+
flash[:alert] = "Diagnostic dumps are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
|
|
400
|
+
redirect_to errors_path
|
|
401
|
+
return
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
scope = DiagnosticDump.recent
|
|
405
|
+
scope = scope.where(application_id: @current_application_id) if @current_application_id.present?
|
|
406
|
+
@total_dumps = scope.count
|
|
407
|
+
|
|
408
|
+
@pagy, @dumps = pagy(:offset, scope, limit: params[:per_page] || 25)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def create_diagnostic_dump
|
|
412
|
+
unless RailsErrorDashboard.configuration.enable_diagnostic_dump
|
|
413
|
+
flash[:alert] = "Diagnostic dumps are not enabled."
|
|
414
|
+
redirect_to errors_path
|
|
415
|
+
return
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
dump = Services::DiagnosticDumpGenerator.call
|
|
419
|
+
|
|
420
|
+
app_name = RailsErrorDashboard.configuration.application_name ||
|
|
421
|
+
ENV["APPLICATION_NAME"] ||
|
|
422
|
+
(defined?(Rails) && Rails.application.class.module_parent_name) ||
|
|
423
|
+
"Unknown"
|
|
424
|
+
app = Commands::FindOrCreateApplication.call(app_name)
|
|
425
|
+
|
|
426
|
+
DiagnosticDump.create!(
|
|
427
|
+
application_id: app.id,
|
|
428
|
+
dump_data: dump.to_json,
|
|
429
|
+
captured_at: Time.current,
|
|
430
|
+
note: params[:note].presence
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
flash[:notice] = "Diagnostic dump captured successfully."
|
|
434
|
+
redirect_to diagnostic_dumps_errors_path
|
|
435
|
+
rescue => e
|
|
436
|
+
flash[:alert] = "Failed to capture diagnostic dump: #{e.message}"
|
|
437
|
+
redirect_to diagnostic_dumps_errors_path
|
|
438
|
+
end
|
|
439
|
+
|
|
351
440
|
def settings
|
|
352
441
|
@config = RailsErrorDashboard.configuration
|
|
353
442
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Job: Persist swallowed exception counters to the database.
|
|
5
|
+
#
|
|
6
|
+
# Two usage modes:
|
|
7
|
+
# 1. With arguments (raise_counts, rescue_counts) — dispatched by the TracePoint
|
|
8
|
+
# periodic flush. Zero I/O in the request path; all DB writes happen here.
|
|
9
|
+
# 2. Without arguments — scheduled periodic sweep that flushes the current
|
|
10
|
+
# thread's counters (useful as a cron safety net).
|
|
11
|
+
#
|
|
12
|
+
# Example cron (via solid_queue or whenever):
|
|
13
|
+
# every 5.minutes { RailsErrorDashboard::SwallowedExceptionFlushJob.perform_later }
|
|
14
|
+
class SwallowedExceptionFlushJob < ApplicationJob
|
|
15
|
+
queue_as :default
|
|
16
|
+
|
|
17
|
+
def perform(raise_counts = nil, rescue_counts = nil)
|
|
18
|
+
return unless RailsErrorDashboard.configuration.detect_swallowed_exceptions
|
|
19
|
+
|
|
20
|
+
if raise_counts && rescue_counts
|
|
21
|
+
# Mode 1: Persist provided snapshots (dispatched from TracePoint flush)
|
|
22
|
+
Commands::FlushSwallowedExceptions.call(
|
|
23
|
+
raise_counts: raise_counts,
|
|
24
|
+
rescue_counts: rescue_counts
|
|
25
|
+
)
|
|
26
|
+
else
|
|
27
|
+
# Mode 2: Flush current thread's counters (scheduled cron safety net)
|
|
28
|
+
Services::SwallowedExceptionTracker.flush!
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
class DiagnosticDump < ErrorLogsRecord
|
|
5
|
+
self.table_name = "rails_error_dashboard_diagnostic_dumps"
|
|
6
|
+
|
|
7
|
+
belongs_to :application
|
|
8
|
+
|
|
9
|
+
validates :captured_at, presence: true
|
|
10
|
+
validates :dump_data, presence: true
|
|
11
|
+
|
|
12
|
+
scope :recent, -> { order(captured_at: :desc) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Stores aggregated counts of raised-then-rescued exceptions per hourly bucket.
|
|
5
|
+
#
|
|
6
|
+
# Swallowed exceptions are raised but silently rescued, never reaching the error
|
|
7
|
+
# dashboard. This table tracks raise/rescue counts keyed by exception class,
|
|
8
|
+
# raise location, rescue location, and hourly period.
|
|
9
|
+
#
|
|
10
|
+
# A high rescue_count/raise_count ratio indicates exceptions being swallowed.
|
|
11
|
+
class SwallowedException < ErrorLogsRecord
|
|
12
|
+
self.table_name = "rails_error_dashboard_swallowed_exceptions"
|
|
13
|
+
|
|
14
|
+
belongs_to :application, optional: true
|
|
15
|
+
|
|
16
|
+
validates :exception_class, presence: true
|
|
17
|
+
validates :raise_location, presence: true
|
|
18
|
+
validates :period_hour, presence: true
|
|
19
|
+
validates :raise_count, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
20
|
+
validates :rescue_count, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
21
|
+
|
|
22
|
+
scope :for_application, ->(app_id) { where(application_id: app_id) }
|
|
23
|
+
scope :since, ->(time) { where("period_hour >= ?", time) }
|
|
24
|
+
scope :recent, -> { order(period_hour: :desc) }
|
|
25
|
+
|
|
26
|
+
# Rescue ratio: fraction of raises that were rescued (0.0 to 1.0)
|
|
27
|
+
def rescue_ratio
|
|
28
|
+
return 0.0 if raise_count.zero?
|
|
29
|
+
rescue_count.to_f / raise_count
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Whether this exception is considered "swallowed" (rescue ratio >= threshold)
|
|
33
|
+
def swallowed?(threshold: nil)
|
|
34
|
+
threshold ||= RailsErrorDashboard.configuration.swallowed_exception_threshold
|
|
35
|
+
rescue_ratio >= threshold
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -1657,6 +1657,13 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
|
1657
1657
|
<% end %>
|
|
1658
1658
|
</li>
|
|
1659
1659
|
<% end %>
|
|
1660
|
+
<% if RailsErrorDashboard.configuration.enable_rack_attack_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
1661
|
+
<li class="nav-item">
|
|
1662
|
+
<%= link_to rack_attack_summary_errors_path(nav_params), class: "nav-link #{request.path == rack_attack_summary_errors_path ? 'active' : ''}" do %>
|
|
1663
|
+
<i class="bi bi-shield-exclamation"></i> Rate Limits
|
|
1664
|
+
<% end %>
|
|
1665
|
+
</li>
|
|
1666
|
+
<% end %>
|
|
1660
1667
|
<% if RailsErrorDashboard.configuration.enable_system_health %>
|
|
1661
1668
|
<li class="nav-item">
|
|
1662
1669
|
<%= link_to job_health_summary_errors_path(nav_params), class: "nav-link #{request.path == job_health_summary_errors_path ? 'active' : ''}" do %>
|
|
@@ -1669,6 +1676,20 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
|
1669
1676
|
<% end %>
|
|
1670
1677
|
</li>
|
|
1671
1678
|
<% end %>
|
|
1679
|
+
<% if RailsErrorDashboard.configuration.detect_swallowed_exceptions %>
|
|
1680
|
+
<li class="nav-item">
|
|
1681
|
+
<%= link_to swallowed_exceptions_errors_path(nav_params), class: "nav-link #{request.path == swallowed_exceptions_errors_path ? 'active' : ''}" do %>
|
|
1682
|
+
<i class="bi bi-eye-slash"></i> Swallowed
|
|
1683
|
+
<% end %>
|
|
1684
|
+
</li>
|
|
1685
|
+
<% end %>
|
|
1686
|
+
<% if RailsErrorDashboard.configuration.enable_diagnostic_dump %>
|
|
1687
|
+
<li class="nav-item">
|
|
1688
|
+
<%= link_to diagnostic_dumps_errors_path(nav_params), class: "nav-link #{request.path == diagnostic_dumps_errors_path ? 'active' : ''}" do %>
|
|
1689
|
+
<i class="bi bi-clipboard-pulse"></i> Diagnostics
|
|
1690
|
+
<% end %>
|
|
1691
|
+
</li>
|
|
1692
|
+
<% end %>
|
|
1672
1693
|
</ul>
|
|
1673
1694
|
|
|
1674
1695
|
<h6 class="mt-4">QUICK FILTERS</h6>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!-- Instance Variables (captured from receiver object at raise time) -->
|
|
2
|
+
<% if RailsErrorDashboard.configuration.enable_instance_variables && error.class.column_names.include?("instance_variables") && error.read_attribute(:instance_variables).present? %>
|
|
3
|
+
<% instance_vars = JSON.parse(error.read_attribute(:instance_variables)) rescue {} %>
|
|
4
|
+
<% self_class = instance_vars.delete("_self_class") %>
|
|
5
|
+
<% if instance_vars.any? %>
|
|
6
|
+
<div class="card mb-4" id="section-instance-variables">
|
|
7
|
+
<div class="card-header bg-white">
|
|
8
|
+
<h5 class="mb-0">
|
|
9
|
+
<i class="bi bi-box"></i> Instance Variables
|
|
10
|
+
<span class="badge bg-secondary"><%= instance_vars.size %> captured</span>
|
|
11
|
+
</h5>
|
|
12
|
+
<small class="text-muted">
|
|
13
|
+
Instance variables from
|
|
14
|
+
<% if self_class && self_class["value"] %>
|
|
15
|
+
<code><%= self_class["value"] %></code>
|
|
16
|
+
<% else %>
|
|
17
|
+
the object
|
|
18
|
+
<% end %>
|
|
19
|
+
where the exception was raised
|
|
20
|
+
</small>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="card-body p-0">
|
|
23
|
+
<div class="table-responsive">
|
|
24
|
+
<table class="table table-sm table-hover mb-0">
|
|
25
|
+
<thead class="table-light">
|
|
26
|
+
<tr>
|
|
27
|
+
<th width="200">Variable</th>
|
|
28
|
+
<th width="120">Type</th>
|
|
29
|
+
<th>Value</th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
<% instance_vars.each do |name, info| %>
|
|
34
|
+
<tr>
|
|
35
|
+
<td><code><%= name %></code></td>
|
|
36
|
+
<td><small class="text-muted"><%= info["type"] %></small></td>
|
|
37
|
+
<td>
|
|
38
|
+
<% if info["filtered"] %>
|
|
39
|
+
<span class="badge bg-warning text-dark">[FILTERED]</span>
|
|
40
|
+
<% else %>
|
|
41
|
+
<code style="word-break: break-all; font-size: 0.85em;"><%= truncate(info["value"].to_s, length: 500) %></code>
|
|
42
|
+
<% if info["truncated"] %>
|
|
43
|
+
<span class="badge bg-info text-dark ms-1" style="font-size: 0.7em;">truncated</span>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% end %>
|
|
46
|
+
</td>
|
|
47
|
+
</tr>
|
|
48
|
+
<% end %>
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% end %>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!-- Local Variables (captured via TracePoint) -->
|
|
2
|
+
<% if RailsErrorDashboard.configuration.enable_local_variables && error.respond_to?(:local_variables) && error.local_variables.present? %>
|
|
3
|
+
<% local_vars = JSON.parse(error.local_variables) rescue {} %>
|
|
4
|
+
<% if local_vars.any? %>
|
|
5
|
+
<div class="card mb-4" id="section-local-variables">
|
|
6
|
+
<div class="card-header bg-white">
|
|
7
|
+
<h5 class="mb-0">
|
|
8
|
+
<i class="bi bi-braces"></i> Local Variables
|
|
9
|
+
<span class="badge bg-secondary"><%= local_vars.size %> captured</span>
|
|
10
|
+
</h5>
|
|
11
|
+
<small class="text-muted">Variables from the stack frame where the exception was raised</small>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="card-body p-0">
|
|
14
|
+
<div class="table-responsive">
|
|
15
|
+
<table class="table table-sm table-hover mb-0">
|
|
16
|
+
<thead class="table-light">
|
|
17
|
+
<tr>
|
|
18
|
+
<th width="200">Variable</th>
|
|
19
|
+
<th width="120">Type</th>
|
|
20
|
+
<th>Value</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<% local_vars.each do |name, info| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td><code><%= name %></code></td>
|
|
27
|
+
<td><small class="text-muted"><%= info["type"] %></small></td>
|
|
28
|
+
<td>
|
|
29
|
+
<% if info["filtered"] %>
|
|
30
|
+
<span class="badge bg-warning text-dark">[FILTERED]</span>
|
|
31
|
+
<% else %>
|
|
32
|
+
<code style="word-break: break-all; font-size: 0.85em;"><%= truncate(info["value"].to_s, length: 500) %></code>
|
|
33
|
+
<% if info["truncated"] %>
|
|
34
|
+
<span class="badge bg-info text-dark ms-1" style="font-size: 0.7em;">truncated</span>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% end %>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
<% end %>
|
|
@@ -334,6 +334,54 @@
|
|
|
334
334
|
</code>
|
|
335
335
|
</div>
|
|
336
336
|
<% end %>
|
|
337
|
+
|
|
338
|
+
<% if health[:ruby_vm] %>
|
|
339
|
+
<% vm = health[:ruby_vm] %>
|
|
340
|
+
<div class="mb-1">
|
|
341
|
+
<small class="text-muted">VM Cache Invalidations:</small>
|
|
342
|
+
<% invals = vm[:constant_cache_invalidations].to_i %>
|
|
343
|
+
<code class="ms-1 <%= 'text-danger' if invals > 10_000 %>"><%= begin; number_with_delimiter(invals); rescue; invals; end %></code>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="mb-1">
|
|
346
|
+
<small class="text-muted">VM Cache Misses:</small>
|
|
347
|
+
<code class="ms-1"><%= begin; number_with_delimiter(vm[:constant_cache_misses]); rescue; vm[:constant_cache_misses]; end %></code>
|
|
348
|
+
</div>
|
|
349
|
+
<% if vm[:shape_cache_size] %>
|
|
350
|
+
<div class="mb-1">
|
|
351
|
+
<small class="text-muted">Shape Cache Size:</small>
|
|
352
|
+
<code class="ms-1"><%= begin; number_with_delimiter(vm[:shape_cache_size]); rescue; vm[:shape_cache_size]; end %></code>
|
|
353
|
+
</div>
|
|
354
|
+
<% end %>
|
|
355
|
+
<% end %>
|
|
356
|
+
|
|
357
|
+
<% if health[:yjit] %>
|
|
358
|
+
<% yj = health[:yjit] %>
|
|
359
|
+
<% if yj[:compiled_iseq_count] || yj[:compiled_block_count] %>
|
|
360
|
+
<div class="mb-1">
|
|
361
|
+
<small class="text-muted">YJIT Compiled:</small>
|
|
362
|
+
<code class="ms-1"><%= yj[:compiled_iseq_count] %> iseqs / <%= yj[:compiled_block_count] %> blocks</code>
|
|
363
|
+
</div>
|
|
364
|
+
<% end %>
|
|
365
|
+
<% if yj[:invalidation_count] %>
|
|
366
|
+
<div class="mb-1">
|
|
367
|
+
<small class="text-muted">YJIT Invalidations:</small>
|
|
368
|
+
<% yj_invals = yj[:invalidation_count].to_i %>
|
|
369
|
+
<code class="ms-1 <%= 'text-danger' if yj_invals > 100 %>"><%= yj_invals %></code>
|
|
370
|
+
</div>
|
|
371
|
+
<% end %>
|
|
372
|
+
<% if yj[:code_region_size] %>
|
|
373
|
+
<div class="mb-1">
|
|
374
|
+
<small class="text-muted">YJIT Code Size:</small>
|
|
375
|
+
<code class="ms-1"><%= (yj[:code_region_size].to_f / 1024).round(1) %> KB</code>
|
|
376
|
+
</div>
|
|
377
|
+
<% end %>
|
|
378
|
+
<% if yj[:compile_time_ns] %>
|
|
379
|
+
<div class="mb-1">
|
|
380
|
+
<small class="text-muted">YJIT Compile Time:</small>
|
|
381
|
+
<code class="ms-1"><%= (yj[:compile_time_ns].to_f / 1_000_000).round(2) %> ms</code>
|
|
382
|
+
</div>
|
|
383
|
+
<% end %>
|
|
384
|
+
<% end %>
|
|
337
385
|
</div>
|
|
338
386
|
</div>
|
|
339
387
|
<% end %>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<% content_for :page_title, "Diagnostic Dumps" %>
|
|
2
|
+
|
|
3
|
+
<div class="container-fluid py-4">
|
|
4
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
5
|
+
<h1 class="h3 mb-0">
|
|
6
|
+
<i class="bi bi-clipboard-pulse me-2"></i>
|
|
7
|
+
Diagnostic Dumps
|
|
8
|
+
</h1>
|
|
9
|
+
|
|
10
|
+
<%= button_to create_diagnostic_dump_errors_path, method: :post, class: "btn btn-primary btn-sm" do %>
|
|
11
|
+
<i class="bi bi-camera me-1"></i> Capture Dump
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<% if @total_dumps == 0 %>
|
|
16
|
+
<div class="text-center py-5">
|
|
17
|
+
<i class="bi bi-clipboard-pulse display-1 text-muted mb-3"></i>
|
|
18
|
+
<h4 class="text-muted">No Diagnostic Dumps Yet</h4>
|
|
19
|
+
<p class="text-muted">
|
|
20
|
+
Capture an on-demand snapshot of your system state for debugging.
|
|
21
|
+
</p>
|
|
22
|
+
<div class="card mx-auto" style="max-width: 500px;">
|
|
23
|
+
<div class="card-body text-start">
|
|
24
|
+
<h6>How diagnostic dumps work:</h6>
|
|
25
|
+
<ul class="mb-0">
|
|
26
|
+
<li>Click <strong>Capture Dump</strong> above or run <code>rails error_dashboard:diagnostic_dump</code></li>
|
|
27
|
+
<li>Captures: environment, GC stats, threads, connection pool, memory, job queue</li>
|
|
28
|
+
<li>Each dump is saved to the database for historical comparison</li>
|
|
29
|
+
<li>Use dumps to investigate production issues or verify system health</li>
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<% else %>
|
|
35
|
+
<div class="row mb-4">
|
|
36
|
+
<div class="col-md-4">
|
|
37
|
+
<div class="card text-center">
|
|
38
|
+
<div class="card-body">
|
|
39
|
+
<div class="display-6 text-info"><%= @total_dumps %></div>
|
|
40
|
+
<small class="text-muted">Total Dumps</small>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="col-md-4">
|
|
45
|
+
<div class="card text-center">
|
|
46
|
+
<div class="card-body">
|
|
47
|
+
<%
|
|
48
|
+
latest = @dumps.first
|
|
49
|
+
latest_data = begin; JSON.parse(latest.dump_data); rescue; {}; end if latest
|
|
50
|
+
thread_count = latest_data&.dig("system_health", "thread_count")
|
|
51
|
+
%>
|
|
52
|
+
<div class="display-6 text-secondary"><%= thread_count || "N/A" %></div>
|
|
53
|
+
<small class="text-muted">Threads (Latest)</small>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="col-md-4">
|
|
58
|
+
<div class="card text-center">
|
|
59
|
+
<div class="card-body">
|
|
60
|
+
<%
|
|
61
|
+
memory_mb = latest_data&.dig("system_health", "process_memory_mb")
|
|
62
|
+
%>
|
|
63
|
+
<div class="display-6 text-secondary"><%= memory_mb ? "#{memory_mb} MB" : "N/A" %></div>
|
|
64
|
+
<small class="text-muted">Memory (Latest)</small>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="card mb-4">
|
|
71
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
72
|
+
<h5 class="mb-0">
|
|
73
|
+
<i class="bi bi-clipboard-pulse text-info me-2"></i>
|
|
74
|
+
Dump History
|
|
75
|
+
<span class="badge bg-info"><%= @total_dumps %></span>
|
|
76
|
+
</h5>
|
|
77
|
+
<small class="text-muted"><%== @pagy.info_tag %></small>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="card-body p-0">
|
|
80
|
+
<% @dumps.each do |dump| %>
|
|
81
|
+
<%
|
|
82
|
+
data = begin; JSON.parse(dump.dump_data); rescue; {}; end
|
|
83
|
+
env = data["environment"] || {}
|
|
84
|
+
health = data["system_health"] || {}
|
|
85
|
+
gc = data["gc"] || {}
|
|
86
|
+
threads = data["threads"] || []
|
|
87
|
+
%>
|
|
88
|
+
<div class="border-bottom p-3">
|
|
89
|
+
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
90
|
+
<div>
|
|
91
|
+
<strong>
|
|
92
|
+
<i class="bi bi-clock me-1"></i>
|
|
93
|
+
<%= dump.captured_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>
|
|
94
|
+
</strong>
|
|
95
|
+
<span class="text-muted ms-2">PID: <%= data["pid"] %></span>
|
|
96
|
+
<% if data["uptime_seconds"] %>
|
|
97
|
+
<span class="text-muted ms-2">
|
|
98
|
+
Uptime: <%= (data["uptime_seconds"].to_i / 60) %>m
|
|
99
|
+
</span>
|
|
100
|
+
<% end %>
|
|
101
|
+
<% if dump.note.present? %>
|
|
102
|
+
<span class="badge bg-secondary ms-2"><%= dump.note %></span>
|
|
103
|
+
<% end %>
|
|
104
|
+
</div>
|
|
105
|
+
<button class="btn btn-outline-secondary btn-sm"
|
|
106
|
+
type="button"
|
|
107
|
+
data-bs-toggle="collapse"
|
|
108
|
+
data-bs-target="#dump-<%= dump.id %>"
|
|
109
|
+
aria-expanded="false">
|
|
110
|
+
<i class="bi bi-chevron-down"></i> Details
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="row">
|
|
115
|
+
<div class="col-md-3">
|
|
116
|
+
<small class="text-muted d-block">Environment</small>
|
|
117
|
+
<small>
|
|
118
|
+
Ruby <%= env["ruby_version"] || "?" %> /
|
|
119
|
+
Rails <%= env["rails_version"] || "?" %>
|
|
120
|
+
<% if env["server"] && env["server"] != "unknown" %>
|
|
121
|
+
/ <%= env["server"].capitalize %>
|
|
122
|
+
<% end %>
|
|
123
|
+
</small>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="col-md-3">
|
|
126
|
+
<small class="text-muted d-block">System Health</small>
|
|
127
|
+
<small>
|
|
128
|
+
<% mem = health["process_memory_mb"] %>
|
|
129
|
+
<% tc = health["thread_count"] %>
|
|
130
|
+
<% pool = health["connection_pool"] %>
|
|
131
|
+
<%= mem ? "#{mem} MB" : "N/A" %> /
|
|
132
|
+
<%= tc || "?" %> threads
|
|
133
|
+
<% if pool %>
|
|
134
|
+
/ Pool: <%= pool["busy"] %>/<%= pool["size"] %>
|
|
135
|
+
<% end %>
|
|
136
|
+
</small>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="col-md-3">
|
|
139
|
+
<small class="text-muted d-block">GC</small>
|
|
140
|
+
<small>
|
|
141
|
+
<% gc_health = health["gc"] || {} %>
|
|
142
|
+
Live: <%= gc_health["heap_live_slots"]&.to_s(:delimited) rescue gc_health["heap_live_slots"] || "?" %> /
|
|
143
|
+
Major GC: <%= gc_health["major_gc_count"] || "?" %>
|
|
144
|
+
</small>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="col-md-3">
|
|
147
|
+
<small class="text-muted d-block">Job Queue</small>
|
|
148
|
+
<small>
|
|
149
|
+
<% jq = health["job_queue"] %>
|
|
150
|
+
<% if jq %>
|
|
151
|
+
<%= jq["adapter"] %>
|
|
152
|
+
<% if jq["failed"] || jq["errored"] %>
|
|
153
|
+
— failed: <%= jq["failed"] || jq["errored"] %>
|
|
154
|
+
<% end %>
|
|
155
|
+
<% else %>
|
|
156
|
+
N/A
|
|
157
|
+
<% end %>
|
|
158
|
+
</small>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="collapse mt-3" id="dump-<%= dump.id %>">
|
|
163
|
+
<pre class="bg-light p-3 rounded" style="max-height: 400px; overflow-y: auto; font-size: 0.8rem;"><%= JSON.pretty_generate(data) rescue dump.dump_data %></pre>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<% end %>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
|
|
169
|
+
<div>
|
|
170
|
+
<small class="text-muted">
|
|
171
|
+
<i class="bi bi-lightbulb text-warning"></i>
|
|
172
|
+
You can also capture dumps via: <code>rails error_dashboard:diagnostic_dump</code>
|
|
173
|
+
<span class="ms-2">Add a note: <code>NOTE="deploy check" rails error_dashboard:diagnostic_dump</code></span>
|
|
174
|
+
</small>
|
|
175
|
+
</div>
|
|
176
|
+
<div>
|
|
177
|
+
<%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<% end %>
|
|
182
|
+
</div>
|