rails_error_dashboard 0.3.0 → 0.3.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 +30 -1
- data/app/controllers/rails_error_dashboard/errors_controller.rb +51 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +12 -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/job_health_summary.html.erb +152 -0
- data/config/routes.rb +2 -0
- 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/services/database_health_inspector.rb +168 -0
- data/lib/rails_error_dashboard/services/rspec_generator.rb +145 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +4 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f95521c0c769dd28fdd0934540282c64f32a8d23a8c6be9a63805326384b1c01
|
|
4
|
+
data.tar.gz: 3c66324e1e40e8fa0b474a6566babe67eb831927269f37a0108e77db6c928749
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ca327e64bea48cc347e96969db0d2da76c4ffe347ba5aad41c8576b6f18b5696fdd64d6df8f910c2b8df4a64dafc2f75f925f9aa9610b20346535d5e05ffbfc
|
|
7
|
+
data.tar.gz: 5188358be68c4831caff5808db350058f334bc9d9e3310acd4bccab8ebef667785035b7481fd2f46ed5bc12e793f4b481f44ae15765babf410ea9292dedfb3d4
|
data/README.md
CHANGED
|
@@ -54,10 +54,18 @@ Experience the full dashboard with 480+ realistic Rails errors, LOTR-themed demo
|
|
|
54
54
|
|
|
55
55
|

|
|
56
56
|
|
|
57
|
+
**Job Health** — Background job queue stats across errors, sorted by failed count.
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
**Database Health** — Connection pool utilization, PostgreSQL table stats, and per-error stress scores.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
57
65
|
---
|
|
58
66
|
|
|
59
67
|
### ⚠️ BETA SOFTWARE
|
|
60
|
-
This Rails Engine is in beta and under active development. While functional and tested (2,
|
|
68
|
+
This Rails Engine is in beta and under active development. While functional and tested (2,226+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
|
|
61
69
|
|
|
62
70
|
**Supports**: Rails 7.0 - 8.1 | Ruby 3.2 - 4.0
|
|
63
71
|
|
|
@@ -228,6 +236,27 @@ config.enable_system_health = true
|
|
|
228
236
|
|
|
229
237
|
**📖 [Complete documentation →](docs/FEATURES.md#system-health-snapshot-new)**
|
|
230
238
|
|
|
239
|
+
#### 🏭 Job Health Page
|
|
240
|
+
|
|
241
|
+
**See background job queue health alongside your errors** — auto-detects Sidekiq, SolidQueue, or GoodJob stats captured at error time.
|
|
242
|
+
|
|
243
|
+
- **Per-error table** — Adapter badge, failed count (color-coded), queued/enqueued, other stats
|
|
244
|
+
- **Summary cards** — Errors with job data, total failed, adapters detected
|
|
245
|
+
- **Sorted worst-first** — Highest failed count first
|
|
246
|
+
|
|
247
|
+
**📖 [Complete documentation →](docs/FEATURES.md#job-health-page)**
|
|
248
|
+
|
|
249
|
+
#### 🗄️ Database Health Page
|
|
250
|
+
|
|
251
|
+
**PgHero-style database health built into the dashboard** — live PostgreSQL stats + historical connection pool data from error snapshots.
|
|
252
|
+
|
|
253
|
+
- **Live stats** (PostgreSQL) — Table sizes, unused indexes, dead tuples, vacuum timestamps, connection activity
|
|
254
|
+
- **Historical pool data** (all adapters) — Per-error connection pool utilization, sorted by stress score
|
|
255
|
+
- **Color-coded** — Utilization >=80% danger, >=60% warning; dead/waiting badges
|
|
256
|
+
- **Non-PG friendly** — SQLite/MySQL still see connection pool stats and historical data
|
|
257
|
+
|
|
258
|
+
**📖 [Complete documentation →](docs/FEATURES.md#database-health-page)**
|
|
259
|
+
|
|
231
260
|
#### 🆕 v0.2 Quick Wins (NEW!)
|
|
232
261
|
|
|
233
262
|
**11 features that make error tracking smarter, safer, and more actionable:**
|
|
@@ -297,6 +297,57 @@ module RailsErrorDashboard
|
|
|
297
297
|
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
298
298
|
end
|
|
299
299
|
|
|
300
|
+
def job_health_summary
|
|
301
|
+
unless RailsErrorDashboard.configuration.enable_system_health
|
|
302
|
+
flash[:alert] = "System health is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
|
|
303
|
+
redirect_to errors_path
|
|
304
|
+
return
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
days = (params[:days] || 30).to_i
|
|
308
|
+
@days = days
|
|
309
|
+
result = Queries::JobHealthSummary.call(days, application_id: @current_application_id)
|
|
310
|
+
all_entries = result[:entries]
|
|
311
|
+
|
|
312
|
+
# Summary stats (computed before pagination)
|
|
313
|
+
@errors_with_jobs = all_entries.size
|
|
314
|
+
@total_failed = all_entries.sum { |e| e[:failed] || e[:errored] || 0 }
|
|
315
|
+
@adapters_detected = all_entries.map { |e| e[:adapter] }.uniq
|
|
316
|
+
|
|
317
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def database_health_summary
|
|
321
|
+
unless RailsErrorDashboard.configuration.enable_system_health
|
|
322
|
+
flash[:alert] = "System health is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
|
|
323
|
+
redirect_to errors_path
|
|
324
|
+
return
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
days = (params[:days] || 30).to_i
|
|
328
|
+
@days = days
|
|
329
|
+
|
|
330
|
+
# Live database health (display-time only)
|
|
331
|
+
@live_health = Services::DatabaseHealthInspector.call
|
|
332
|
+
|
|
333
|
+
# Separate host vs gem tables from live data
|
|
334
|
+
all_tables = @live_health[:tables] || []
|
|
335
|
+
@host_tables = all_tables.reject { |t| t[:gem_table] }
|
|
336
|
+
@gem_tables = all_tables.select { |t| t[:gem_table] }
|
|
337
|
+
|
|
338
|
+
# Historical connection pool stats
|
|
339
|
+
result = Queries::DatabaseHealthSummary.call(days, application_id: @current_application_id)
|
|
340
|
+
all_entries = result[:entries]
|
|
341
|
+
|
|
342
|
+
# Summary stats (computed before pagination)
|
|
343
|
+
@errors_with_pool = all_entries.size
|
|
344
|
+
@max_utilization = all_entries.map { |e| e[:utilization] }.max || 0
|
|
345
|
+
@total_dead = all_entries.sum { |e| e[:dead] }
|
|
346
|
+
@total_waiting = all_entries.sum { |e| e[:waiting] }
|
|
347
|
+
|
|
348
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
349
|
+
end
|
|
350
|
+
|
|
300
351
|
def settings
|
|
301
352
|
@config = RailsErrorDashboard.configuration
|
|
302
353
|
end
|
|
@@ -1657,6 +1657,18 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
|
1657
1657
|
<% end %>
|
|
1658
1658
|
</li>
|
|
1659
1659
|
<% end %>
|
|
1660
|
+
<% if RailsErrorDashboard.configuration.enable_system_health %>
|
|
1661
|
+
<li class="nav-item">
|
|
1662
|
+
<%= link_to job_health_summary_errors_path(nav_params), class: "nav-link #{request.path == job_health_summary_errors_path ? 'active' : ''}" do %>
|
|
1663
|
+
<i class="bi bi-cpu"></i> Job Health
|
|
1664
|
+
<% end %>
|
|
1665
|
+
</li>
|
|
1666
|
+
<li class="nav-item">
|
|
1667
|
+
<%= link_to database_health_summary_errors_path(nav_params), class: "nav-link #{request.path == database_health_summary_errors_path ? 'active' : ''}" do %>
|
|
1668
|
+
<i class="bi bi-database"></i> DB Health
|
|
1669
|
+
<% end %>
|
|
1670
|
+
</li>
|
|
1671
|
+
<% end %>
|
|
1660
1672
|
</ul>
|
|
1661
1673
|
|
|
1662
1674
|
<h6 class="mt-4">QUICK FILTERS</h6>
|
|
@@ -78,16 +78,27 @@
|
|
|
78
78
|
<td><code><%= error.ip_address || 'N/A' %></code></td>
|
|
79
79
|
</tr>
|
|
80
80
|
<% curl_cmd = RailsErrorDashboard::Services::CurlGenerator.call(error) %>
|
|
81
|
-
<%
|
|
81
|
+
<% rspec_cmd = RailsErrorDashboard::Services::RspecGenerator.call(error) %>
|
|
82
|
+
<% if curl_cmd.present? || rspec_cmd.present? %>
|
|
82
83
|
<tr>
|
|
83
84
|
<th>Replay Request:</th>
|
|
84
85
|
<td>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
<% if curl_cmd.present? %>
|
|
87
|
+
<button class="btn btn-sm btn-outline-primary me-2"
|
|
88
|
+
onclick="copyToClipboard(this.dataset.curl, this)"
|
|
89
|
+
data-curl="<%= h curl_cmd %>"
|
|
90
|
+
title="Copy curl command to clipboard">
|
|
91
|
+
<i class="bi bi-terminal"></i> Copy as curl
|
|
92
|
+
</button>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% if rspec_cmd.present? %>
|
|
95
|
+
<button class="btn btn-sm btn-outline-success"
|
|
96
|
+
onclick="copyToClipboard(this.dataset.curl, this)"
|
|
97
|
+
data-curl="<%= h rspec_cmd %>"
|
|
98
|
+
title="Copy RSpec request spec to clipboard">
|
|
99
|
+
<i class="bi bi-code-slash"></i> Copy as RSpec
|
|
100
|
+
</button>
|
|
101
|
+
<% end %>
|
|
91
102
|
</td>
|
|
92
103
|
</tr>
|
|
93
104
|
<% end %>
|
|
@@ -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>
|