rails_error_dashboard 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +162 -834
- data/app/controllers/rails_error_dashboard/errors_controller.rb +140 -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 +33 -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/_request_context.html.erb +18 -7
- data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +450 -0
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +182 -0
- data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +152 -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 +6 -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/database_health_summary.rb +82 -0
- data/lib/rails_error_dashboard/queries/job_health_summary.rb +101 -0
- 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/database_health_inspector.rb +168 -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/rspec_generator.rb +145 -0
- data/lib/rails_error_dashboard/services/swallowed_exception_tracker.rb +277 -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 +13 -0
- data/lib/tasks/error_dashboard.rake +34 -0
- metadata +29 -2
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
<% content_for :page_title, "DB Health" %>
|
|
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-database me-2"></i>
|
|
7
|
+
Database Health
|
|
8
|
+
</h1>
|
|
9
|
+
|
|
10
|
+
<div class="btn-group" role="group">
|
|
11
|
+
<%= link_to database_health_summary_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
12
|
+
7 Days
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= link_to database_health_summary_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
15
|
+
30 Days
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to database_health_summary_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
18
|
+
90 Days
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<%# ===== SECTION A: Live Database Health ===== %>
|
|
24
|
+
|
|
25
|
+
<h4 class="mb-3"><i class="bi bi-activity text-primary me-2"></i>Live Database Health</h4>
|
|
26
|
+
|
|
27
|
+
<% unless @live_health[:postgresql] %>
|
|
28
|
+
<div class="alert alert-info mb-3">
|
|
29
|
+
<i class="bi bi-info-circle me-2"></i>
|
|
30
|
+
<strong>Non-PostgreSQL adapter detected (<%= @live_health[:adapter] || "unknown" %>).</strong>
|
|
31
|
+
Live table, index, and activity stats require PostgreSQL. Connection pool and historical data are still available below.
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<%# Connection Pool (always works — all adapters) %>
|
|
36
|
+
<% if @live_health[:connection_pool] %>
|
|
37
|
+
<% pool = @live_health[:connection_pool] %>
|
|
38
|
+
<div class="row mb-4">
|
|
39
|
+
<div class="col-md-2">
|
|
40
|
+
<div class="card text-center">
|
|
41
|
+
<div class="card-body py-2">
|
|
42
|
+
<div class="h4 mb-0 text-primary"><%= pool[:size] %></div>
|
|
43
|
+
<small class="text-muted">Pool Size</small>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="col-md-2">
|
|
48
|
+
<div class="card text-center">
|
|
49
|
+
<div class="card-body py-2">
|
|
50
|
+
<% busy_color = pool[:size] > 0 && (pool[:busy].to_f / pool[:size]) >= 0.8 ? "danger" : "info" %>
|
|
51
|
+
<div class="h4 mb-0 text-<%= busy_color %>"><%= pool[:busy] %></div>
|
|
52
|
+
<small class="text-muted">Busy</small>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="col-md-2">
|
|
57
|
+
<div class="card text-center">
|
|
58
|
+
<div class="card-body py-2">
|
|
59
|
+
<div class="h4 mb-0 text-success"><%= pool[:idle] %></div>
|
|
60
|
+
<small class="text-muted">Idle</small>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="col-md-2">
|
|
65
|
+
<div class="card text-center">
|
|
66
|
+
<div class="card-body py-2">
|
|
67
|
+
<% dead_color = pool[:dead] > 0 ? "danger" : "success" %>
|
|
68
|
+
<div class="h4 mb-0 text-<%= dead_color %>"><%= pool[:dead] %></div>
|
|
69
|
+
<small class="text-muted">Dead</small>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="col-md-2">
|
|
74
|
+
<div class="card text-center">
|
|
75
|
+
<div class="card-body py-2">
|
|
76
|
+
<% waiting_color = pool[:waiting] > 0 ? "warning" : "success" %>
|
|
77
|
+
<div class="h4 mb-0 text-<%= waiting_color %>"><%= pool[:waiting] %></div>
|
|
78
|
+
<small class="text-muted">Waiting</small>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<% end %>
|
|
84
|
+
|
|
85
|
+
<% if @live_health[:postgresql] %>
|
|
86
|
+
<%# Summary cards for PostgreSQL %>
|
|
87
|
+
<% total_db_size = (@host_tables + @gem_tables).sum { |t| t[:total_bytes] } %>
|
|
88
|
+
<div class="row mb-4">
|
|
89
|
+
<div class="col-md-3">
|
|
90
|
+
<div class="card text-center">
|
|
91
|
+
<div class="card-body py-2">
|
|
92
|
+
<div class="h4 mb-0 text-primary"><%= number_to_human_size(total_db_size) %></div>
|
|
93
|
+
<small class="text-muted">Total DB Size</small>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="col-md-3">
|
|
98
|
+
<div class="card text-center">
|
|
99
|
+
<div class="card-body py-2">
|
|
100
|
+
<div class="h4 mb-0 text-info"><%= @host_tables.size + @gem_tables.size %></div>
|
|
101
|
+
<small class="text-muted">Tables</small>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="col-md-3">
|
|
106
|
+
<div class="card text-center">
|
|
107
|
+
<div class="card-body py-2">
|
|
108
|
+
<% unused_color = @live_health[:unused_indexes]&.any? ? "warning" : "success" %>
|
|
109
|
+
<div class="h4 mb-0 text-<%= unused_color %>"><%= @live_health[:unused_indexes]&.size || 0 %></div>
|
|
110
|
+
<small class="text-muted">Unused Indexes</small>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="col-md-3">
|
|
115
|
+
<div class="card text-center">
|
|
116
|
+
<div class="card-body py-2">
|
|
117
|
+
<div class="h4 mb-0 text-secondary"><%= @live_health[:activity] ? @live_health[:activity][:total] : "N/A" %></div>
|
|
118
|
+
<small class="text-muted">Active Connections</small>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<%# Host App Tables %>
|
|
125
|
+
<% if @host_tables.any? %>
|
|
126
|
+
<div class="card mb-4">
|
|
127
|
+
<div class="card-header bg-white">
|
|
128
|
+
<h5 class="mb-0">
|
|
129
|
+
<i class="bi bi-table text-primary me-2"></i>
|
|
130
|
+
Host App Tables
|
|
131
|
+
<span class="badge bg-primary"><%= @host_tables.size %></span>
|
|
132
|
+
</h5>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="card-body p-0">
|
|
135
|
+
<div class="table-responsive">
|
|
136
|
+
<table class="table table-hover mb-0">
|
|
137
|
+
<thead class="table-light">
|
|
138
|
+
<tr>
|
|
139
|
+
<th>Table</th>
|
|
140
|
+
<th width="100">Est. Rows</th>
|
|
141
|
+
<th width="100">Size</th>
|
|
142
|
+
<th width="80">Seq Scans</th>
|
|
143
|
+
<th width="80">Idx Scans</th>
|
|
144
|
+
<th width="100">Dead Tuples</th>
|
|
145
|
+
<th width="140">Last Vacuum</th>
|
|
146
|
+
</tr>
|
|
147
|
+
</thead>
|
|
148
|
+
<tbody>
|
|
149
|
+
<% @host_tables.each do |table| %>
|
|
150
|
+
<tr>
|
|
151
|
+
<td><code><%= table[:name] %></code></td>
|
|
152
|
+
<td><%= number_with_delimiter(table[:estimated_rows]) %></td>
|
|
153
|
+
<td><%= number_to_human_size(table[:total_bytes]) %></td>
|
|
154
|
+
<td><%= number_with_delimiter(table[:seq_scan]) %></td>
|
|
155
|
+
<td><%= number_with_delimiter(table[:idx_scan]) %></td>
|
|
156
|
+
<td>
|
|
157
|
+
<% if table[:dead_tuples] > 1000 %>
|
|
158
|
+
<span class="badge bg-warning text-dark"><%= number_with_delimiter(table[:dead_tuples]) %></span>
|
|
159
|
+
<% else %>
|
|
160
|
+
<%= number_with_delimiter(table[:dead_tuples]) %>
|
|
161
|
+
<% end %>
|
|
162
|
+
</td>
|
|
163
|
+
<td><%= table[:last_autovacuum] ? local_time_ago(Time.parse(table[:last_autovacuum])) : "Never" %></td>
|
|
164
|
+
</tr>
|
|
165
|
+
<% end %>
|
|
166
|
+
</tbody>
|
|
167
|
+
</table>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<% end %>
|
|
172
|
+
|
|
173
|
+
<%# Gem Tables (collapsible) %>
|
|
174
|
+
<% if @gem_tables.any? %>
|
|
175
|
+
<div class="card mb-4">
|
|
176
|
+
<div class="card-header bg-white">
|
|
177
|
+
<a class="text-decoration-none" data-bs-toggle="collapse" href="#gemTablesCollapse" role="button" aria-expanded="false" aria-controls="gemTablesCollapse">
|
|
178
|
+
<h5 class="mb-0">
|
|
179
|
+
<i class="bi bi-table text-secondary me-2"></i>
|
|
180
|
+
Error Dashboard Tables
|
|
181
|
+
<span class="badge bg-secondary"><%= @gem_tables.size %></span>
|
|
182
|
+
<i class="bi bi-chevron-down ms-2" style="font-size: 0.8em;"></i>
|
|
183
|
+
</h5>
|
|
184
|
+
</a>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="collapse" id="gemTablesCollapse">
|
|
187
|
+
<div class="card-body p-0">
|
|
188
|
+
<div class="table-responsive">
|
|
189
|
+
<table class="table table-hover mb-0">
|
|
190
|
+
<thead class="table-light">
|
|
191
|
+
<tr>
|
|
192
|
+
<th>Table</th>
|
|
193
|
+
<th width="100">Est. Rows</th>
|
|
194
|
+
<th width="100">Size</th>
|
|
195
|
+
<th width="80">Seq Scans</th>
|
|
196
|
+
<th width="80">Idx Scans</th>
|
|
197
|
+
<th width="100">Dead Tuples</th>
|
|
198
|
+
<th width="140">Last Vacuum</th>
|
|
199
|
+
</tr>
|
|
200
|
+
</thead>
|
|
201
|
+
<tbody>
|
|
202
|
+
<% @gem_tables.each do |table| %>
|
|
203
|
+
<tr>
|
|
204
|
+
<td><code><%= table[:name] %></code></td>
|
|
205
|
+
<td><%= number_with_delimiter(table[:estimated_rows]) %></td>
|
|
206
|
+
<td><%= number_to_human_size(table[:total_bytes]) %></td>
|
|
207
|
+
<td><%= number_with_delimiter(table[:seq_scan]) %></td>
|
|
208
|
+
<td><%= number_with_delimiter(table[:idx_scan]) %></td>
|
|
209
|
+
<td>
|
|
210
|
+
<% if table[:dead_tuples] > 1000 %>
|
|
211
|
+
<span class="badge bg-warning text-dark"><%= number_with_delimiter(table[:dead_tuples]) %></span>
|
|
212
|
+
<% else %>
|
|
213
|
+
<%= number_with_delimiter(table[:dead_tuples]) %>
|
|
214
|
+
<% end %>
|
|
215
|
+
</td>
|
|
216
|
+
<td><%= table[:last_autovacuum] ? local_time_ago(Time.parse(table[:last_autovacuum])) : "Never" %></td>
|
|
217
|
+
</tr>
|
|
218
|
+
<% end %>
|
|
219
|
+
</tbody>
|
|
220
|
+
</table>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<% end %>
|
|
226
|
+
|
|
227
|
+
<%# Unused Indexes %>
|
|
228
|
+
<% if @live_health[:unused_indexes]&.any? %>
|
|
229
|
+
<div class="card mb-4">
|
|
230
|
+
<div class="card-header bg-white">
|
|
231
|
+
<h5 class="mb-0">
|
|
232
|
+
<i class="bi bi-exclamation-triangle text-warning me-2"></i>
|
|
233
|
+
Unused Indexes
|
|
234
|
+
<span class="badge bg-warning text-dark"><%= @live_health[:unused_indexes].size %></span>
|
|
235
|
+
</h5>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="card-body p-0">
|
|
238
|
+
<div class="table-responsive">
|
|
239
|
+
<table class="table table-hover mb-0">
|
|
240
|
+
<thead class="table-light">
|
|
241
|
+
<tr>
|
|
242
|
+
<th>Index</th>
|
|
243
|
+
<th>Table</th>
|
|
244
|
+
<th width="100">Size</th>
|
|
245
|
+
</tr>
|
|
246
|
+
</thead>
|
|
247
|
+
<tbody>
|
|
248
|
+
<% @live_health[:unused_indexes].each do |idx| %>
|
|
249
|
+
<tr>
|
|
250
|
+
<td><code><%= idx[:name] %></code></td>
|
|
251
|
+
<td><code><%= idx[:table_name] %></code></td>
|
|
252
|
+
<td><span class="badge bg-warning text-dark"><%= number_to_human_size(idx[:size_bytes]) %></span></td>
|
|
253
|
+
</tr>
|
|
254
|
+
<% end %>
|
|
255
|
+
</tbody>
|
|
256
|
+
</table>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
<% end %>
|
|
261
|
+
|
|
262
|
+
<%# Activity by State %>
|
|
263
|
+
<% if @live_health[:activity] %>
|
|
264
|
+
<div class="card mb-4">
|
|
265
|
+
<div class="card-header bg-white">
|
|
266
|
+
<h5 class="mb-0">
|
|
267
|
+
<i class="bi bi-people text-info me-2"></i>
|
|
268
|
+
Connection Activity
|
|
269
|
+
<span class="badge bg-info"><%= @live_health[:activity][:total] %> total</span>
|
|
270
|
+
<% if @live_health[:activity][:total_waiting] > 0 %>
|
|
271
|
+
<span class="badge bg-warning text-dark"><%= @live_health[:activity][:total_waiting] %> waiting</span>
|
|
272
|
+
<% end %>
|
|
273
|
+
</h5>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="card-body p-0">
|
|
276
|
+
<div class="table-responsive">
|
|
277
|
+
<table class="table table-hover mb-0">
|
|
278
|
+
<thead class="table-light">
|
|
279
|
+
<tr>
|
|
280
|
+
<th>State</th>
|
|
281
|
+
<th width="100">Count</th>
|
|
282
|
+
<th width="100">Waiting</th>
|
|
283
|
+
</tr>
|
|
284
|
+
</thead>
|
|
285
|
+
<tbody>
|
|
286
|
+
<% @live_health[:activity][:by_state].each do |row| %>
|
|
287
|
+
<tr>
|
|
288
|
+
<td><span class="badge bg-secondary"><%= row[:state] %></span></td>
|
|
289
|
+
<td><%= row[:count] %></td>
|
|
290
|
+
<td>
|
|
291
|
+
<% if row[:waiting] > 0 %>
|
|
292
|
+
<span class="badge bg-warning text-dark"><%= row[:waiting] %></span>
|
|
293
|
+
<% else %>
|
|
294
|
+
0
|
|
295
|
+
<% end %>
|
|
296
|
+
</td>
|
|
297
|
+
</tr>
|
|
298
|
+
<% end %>
|
|
299
|
+
</tbody>
|
|
300
|
+
</table>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
<% end %>
|
|
305
|
+
<% end %>
|
|
306
|
+
|
|
307
|
+
<%# ===== SECTION B: Historical Connection Pool at Error Time ===== %>
|
|
308
|
+
|
|
309
|
+
<hr class="my-4">
|
|
310
|
+
<h4 class="mb-3"><i class="bi bi-clock-history text-secondary me-2"></i>Historical Connection Pool at Error Time</h4>
|
|
311
|
+
|
|
312
|
+
<% if @errors_with_pool == 0 %>
|
|
313
|
+
<div class="text-center py-5">
|
|
314
|
+
<i class="bi bi-check-circle display-1 text-success mb-3"></i>
|
|
315
|
+
<h4 class="text-muted">No Connection Pool Data Found</h4>
|
|
316
|
+
<p class="text-muted">
|
|
317
|
+
No connection pool stats were detected in system health snapshots over the last <%= @days %> days.
|
|
318
|
+
</p>
|
|
319
|
+
<div class="card mx-auto" style="max-width: 500px;">
|
|
320
|
+
<div class="card-body text-start">
|
|
321
|
+
<h6>How connection pool tracking works:</h6>
|
|
322
|
+
<ul class="mb-0">
|
|
323
|
+
<li>System health must be enabled (<code>enable_system_health = true</code>)</li>
|
|
324
|
+
<li>Connection pool stats are captured automatically at error time</li>
|
|
325
|
+
<li>Stats include pool size, busy, idle, dead, and waiting connections</li>
|
|
326
|
+
<li>This section shows pool health per-error, sorted by stress score</li>
|
|
327
|
+
</ul>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
<% else %>
|
|
332
|
+
<div class="row mb-4">
|
|
333
|
+
<div class="col-md-3">
|
|
334
|
+
<div class="card text-center">
|
|
335
|
+
<div class="card-body">
|
|
336
|
+
<div class="display-6 text-info"><%= @errors_with_pool %></div>
|
|
337
|
+
<small class="text-muted">Errors with Pool Data</small>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="col-md-3">
|
|
342
|
+
<div class="card text-center">
|
|
343
|
+
<div class="card-body">
|
|
344
|
+
<% util_color = @max_utilization >= 80 ? "danger" : (@max_utilization >= 60 ? "warning" : "success") %>
|
|
345
|
+
<div class="display-6 text-<%= util_color %>"><%= @max_utilization %>%</div>
|
|
346
|
+
<small class="text-muted">Peak Utilization</small>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="col-md-3">
|
|
351
|
+
<div class="card text-center">
|
|
352
|
+
<div class="card-body">
|
|
353
|
+
<% dead_color = @total_dead > 0 ? "danger" : "success" %>
|
|
354
|
+
<div class="display-6 text-<%= dead_color %>"><%= @total_dead %></div>
|
|
355
|
+
<small class="text-muted">Total Dead</small>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="col-md-3">
|
|
360
|
+
<div class="card text-center">
|
|
361
|
+
<div class="card-body">
|
|
362
|
+
<% waiting_color = @total_waiting > 0 ? "warning" : "success" %>
|
|
363
|
+
<div class="display-6 text-<%= waiting_color %>"><%= @total_waiting %></div>
|
|
364
|
+
<small class="text-muted">Total Waiting</small>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<div class="card mb-4">
|
|
371
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
372
|
+
<h5 class="mb-0">
|
|
373
|
+
<i class="bi bi-database text-info me-2"></i>
|
|
374
|
+
Connection Pool by Error
|
|
375
|
+
<span class="badge bg-info"><%= @errors_with_pool %></span>
|
|
376
|
+
</h5>
|
|
377
|
+
<small class="text-muted"><%== @pagy.info_tag %></small>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="card-body p-0">
|
|
380
|
+
<div class="table-responsive">
|
|
381
|
+
<table class="table table-hover mb-0">
|
|
382
|
+
<thead class="table-light">
|
|
383
|
+
<tr>
|
|
384
|
+
<th width="80">Error</th>
|
|
385
|
+
<th>Error Type</th>
|
|
386
|
+
<th width="110">Utilization</th>
|
|
387
|
+
<th width="60">Busy</th>
|
|
388
|
+
<th width="60">Idle</th>
|
|
389
|
+
<th width="60">Dead</th>
|
|
390
|
+
<th width="70">Waiting</th>
|
|
391
|
+
<th width="70">Pool Size</th>
|
|
392
|
+
<th width="140">Last Seen</th>
|
|
393
|
+
</tr>
|
|
394
|
+
</thead>
|
|
395
|
+
<tbody>
|
|
396
|
+
<% @entries.each do |entry| %>
|
|
397
|
+
<tr>
|
|
398
|
+
<td><%= link_to "##{entry[:error_id]}", error_path(entry[:error_id]), class: "text-decoration-none" %></td>
|
|
399
|
+
<td><code><%= truncate(entry[:error_type].to_s, length: 40) %></code></td>
|
|
400
|
+
<td>
|
|
401
|
+
<% util = entry[:utilization] %>
|
|
402
|
+
<% util_badge = util >= 80 ? "danger" : (util >= 60 ? "warning" : "success") %>
|
|
403
|
+
<div class="progress" style="height: 20px;">
|
|
404
|
+
<div class="progress-bar bg-<%= util_badge %>" role="progressbar" style="width: <%= [ util, 100 ].min %>%;" aria-valuenow="<%= util %>" aria-valuemin="0" aria-valuemax="100">
|
|
405
|
+
<%= util %>%
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</td>
|
|
409
|
+
<td><%= entry[:busy] %></td>
|
|
410
|
+
<td><%= entry[:idle] %></td>
|
|
411
|
+
<td>
|
|
412
|
+
<% if entry[:dead] > 0 %>
|
|
413
|
+
<span class="badge bg-danger"><%= entry[:dead] %></span>
|
|
414
|
+
<% else %>
|
|
415
|
+
0
|
|
416
|
+
<% end %>
|
|
417
|
+
</td>
|
|
418
|
+
<td>
|
|
419
|
+
<% if entry[:waiting] > 0 %>
|
|
420
|
+
<span class="badge bg-warning text-dark"><%= entry[:waiting] %></span>
|
|
421
|
+
<% else %>
|
|
422
|
+
0
|
|
423
|
+
<% end %>
|
|
424
|
+
</td>
|
|
425
|
+
<td><%= entry[:size] %></td>
|
|
426
|
+
<td><%= local_time_ago(entry[:occurred_at]) %></td>
|
|
427
|
+
</tr>
|
|
428
|
+
<% end %>
|
|
429
|
+
</tbody>
|
|
430
|
+
</table>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
|
|
434
|
+
<div>
|
|
435
|
+
<small class="text-muted">
|
|
436
|
+
<i class="bi bi-lightbulb text-warning"></i> High utilization during errors may indicate connection pool exhaustion or long-running queries.
|
|
437
|
+
</small>
|
|
438
|
+
<small class="ms-3">
|
|
439
|
+
<a href="https://guides.rubyonrails.org/configuring.html#configuring-a-database" target="_blank" rel="noopener" class="text-decoration-none">
|
|
440
|
+
<i class="bi bi-book"></i> Database Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
|
|
441
|
+
</a>
|
|
442
|
+
</small>
|
|
443
|
+
</div>
|
|
444
|
+
<div>
|
|
445
|
+
<%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
<% end %>
|
|
450
|
+
</div>
|
|
@@ -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>
|