rails_error_dashboard 0.5.0 → 0.5.2
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/views/layouts/rails_error_dashboard.html.erb +7 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +93 -4
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +37 -4
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +125 -11
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1826105c19a4a203cf377fca2041763849b7b2ff7bb8a9f3a18601e22150f6b
|
|
4
|
+
data.tar.gz: fdabbc92770c81339e7499d739e941f4f2a7f3fda381a2a55ec4dcab5bb757cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c365ae991e67cb59dd9e62deaabe694e06c7e9b64896a75bfa36473f3c735556c95482a929e8ade0328ffab05e10128e5d5e6832c94ead44cfffcf6567c05aa
|
|
7
|
+
data.tar.gz: 6b63c5a587bf93ec1599571343c8fcaf8230364266d97564f59217d8308632817557a05fec2398f3e3557ca33d83dbc9968c3f820722e3b1395e046cd58de15a
|
data/README.md
CHANGED
|
@@ -22,7 +22,7 @@ gem 'rails_error_dashboard'
|
|
|
22
22
|
|
|
23
23
|
**[rails-error-dashboard.anjan.dev](https://rails-error-dashboard.anjan.dev)** — Username: `gandalf` · Password: `youshallnotpass`
|
|
24
24
|
|
|
25
|
-
> **Beta Software** — Functional and tested (2,
|
|
25
|
+
> **Beta Software** — Functional and tested (2,700+ tests passing), but the API may change before v1.0. Supports Rails 7.0-8.1 and Ruby 3.2-4.0.
|
|
26
26
|
|
|
27
27
|
### Screenshots
|
|
28
28
|
|
|
@@ -87,12 +87,13 @@ config.enable_breadcrumbs = true
|
|
|
87
87
|
<details>
|
|
88
88
|
<summary><strong>System Health Snapshot</strong></summary>
|
|
89
89
|
|
|
90
|
-
Know your app's runtime state at the moment of failure — GC stats, process memory, thread count, connection pool utilization, Puma thread stats, RubyVM cache health,
|
|
90
|
+
Know your app's runtime state at the moment of failure — GC stats, process memory, thread count, connection pool utilization, Puma thread stats, RubyVM cache health, YJIT compilation stats, and deep runtime insights captured automatically.
|
|
91
91
|
|
|
92
92
|
- Sub-millisecond total snapshot, every metric individually rescue-wrapped
|
|
93
93
|
- No ObjectSpace scanning, no Thread backtraces, no subprocess calls
|
|
94
94
|
- RubyVM.stat: constant cache invalidations, shape cache stats
|
|
95
95
|
- YJIT runtime stats: compiled iseqs, invalidation count, code region sizes
|
|
96
|
+
- **v0.5.2** — File descriptor utilization, system load averages, system memory pressure, TCP connection states, GC context (trigger reason, last major/minor), process swap and peak RSS — all with color-coded danger indicators
|
|
96
97
|
|
|
97
98
|
```ruby
|
|
98
99
|
config.enable_system_health = true
|
|
@@ -1664,6 +1664,13 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
|
1664
1664
|
<% end %>
|
|
1665
1665
|
</li>
|
|
1666
1666
|
<% end %>
|
|
1667
|
+
<% if RailsErrorDashboard.configuration.enable_actioncable_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
1668
|
+
<li class="nav-item">
|
|
1669
|
+
<%= link_to actioncable_health_summary_errors_path(nav_params), class: "nav-link #{request.path == actioncable_health_summary_errors_path ? 'active' : ''}" do %>
|
|
1670
|
+
<i class="bi bi-broadcast"></i> ActionCable
|
|
1671
|
+
<% end %>
|
|
1672
|
+
</li>
|
|
1673
|
+
<% end %>
|
|
1667
1674
|
<% if RailsErrorDashboard.configuration.enable_system_health %>
|
|
1668
1675
|
<li class="nav-item">
|
|
1669
1676
|
<%= link_to job_health_summary_errors_path(nav_params), class: "nav-link #{request.path == job_health_summary_errors_path ? 'active' : ''}" do %>
|
|
@@ -299,7 +299,37 @@
|
|
|
299
299
|
<div class="sidebar-section-title"><i class="bi bi-heart-pulse"></i> System Health</div>
|
|
300
300
|
<small class="sidebar-section-hint">Runtime state when error fired</small>
|
|
301
301
|
<div class="sidebar-section-body">
|
|
302
|
-
<% if health[:
|
|
302
|
+
<% if health[:process_memory] %>
|
|
303
|
+
<% pm = health[:process_memory] %>
|
|
304
|
+
<div class="mb-1">
|
|
305
|
+
<small class="text-muted">Memory (RSS):</small>
|
|
306
|
+
<% mem = pm[:rss_mb].to_f %>
|
|
307
|
+
<code class="ms-1 text-<%= mem > 1024 ? 'danger' : mem > 512 ? 'warning' : 'success' %>">
|
|
308
|
+
<%= mem %> MB
|
|
309
|
+
</code>
|
|
310
|
+
</div>
|
|
311
|
+
<% if pm[:swap_mb].to_f > 0 %>
|
|
312
|
+
<div class="mb-1">
|
|
313
|
+
<small class="text-muted">Process Swap:</small>
|
|
314
|
+
<code class="ms-1 text-danger"><%= pm[:swap_mb] %> MB</code>
|
|
315
|
+
</div>
|
|
316
|
+
<% end %>
|
|
317
|
+
<% if pm[:rss_peak_mb] %>
|
|
318
|
+
<div class="mb-1">
|
|
319
|
+
<small class="text-muted">Peak RSS:</small>
|
|
320
|
+
<code class="ms-1"><%= pm[:rss_peak_mb] %> MB</code>
|
|
321
|
+
</div>
|
|
322
|
+
<% end %>
|
|
323
|
+
<% if pm[:os_threads] %>
|
|
324
|
+
<div class="mb-1">
|
|
325
|
+
<small class="text-muted">OS Threads:</small>
|
|
326
|
+
<code class="ms-1"><%= pm[:os_threads] %></code>
|
|
327
|
+
<% if health[:thread_count] %>
|
|
328
|
+
<small class="text-muted ms-1">(Ruby: <%= health[:thread_count] %>)</small>
|
|
329
|
+
<% end %>
|
|
330
|
+
</div>
|
|
331
|
+
<% end %>
|
|
332
|
+
<% elsif health[:process_memory_mb] %>
|
|
303
333
|
<div class="mb-1">
|
|
304
334
|
<small class="text-muted">Memory (RSS):</small>
|
|
305
335
|
<% mem = health[:process_memory_mb].to_f %>
|
|
@@ -307,12 +337,61 @@
|
|
|
307
337
|
<%= mem %> MB
|
|
308
338
|
</code>
|
|
309
339
|
</div>
|
|
340
|
+
<% if health[:thread_count] %>
|
|
341
|
+
<div class="mb-1">
|
|
342
|
+
<small class="text-muted">Threads:</small>
|
|
343
|
+
<code class="ms-1"><%= health[:thread_count] %></code>
|
|
344
|
+
</div>
|
|
345
|
+
<% end %>
|
|
310
346
|
<% end %>
|
|
311
347
|
|
|
312
|
-
<% if health[:
|
|
348
|
+
<% if health[:file_descriptors] %>
|
|
349
|
+
<% fd = health[:file_descriptors] %>
|
|
313
350
|
<div class="mb-1">
|
|
314
|
-
<small class="text-muted">
|
|
315
|
-
|
|
351
|
+
<small class="text-muted">File Descriptors:</small>
|
|
352
|
+
<% fd_pct = fd[:utilization_pct].to_f %>
|
|
353
|
+
<code class="ms-1 text-<%= fd_pct > 80 ? 'danger' : fd_pct > 60 ? 'warning' : 'success' %>">
|
|
354
|
+
<%= fd[:open] %> / <%= fd[:limit] %> (<%= fd_pct %>%)
|
|
355
|
+
</code>
|
|
356
|
+
</div>
|
|
357
|
+
<% end %>
|
|
358
|
+
|
|
359
|
+
<% if health[:system_load] %>
|
|
360
|
+
<% sl = health[:system_load] %>
|
|
361
|
+
<div class="mb-1">
|
|
362
|
+
<small class="text-muted">Load Average:</small>
|
|
363
|
+
<% lr = sl[:load_ratio].to_f %>
|
|
364
|
+
<code class="ms-1 text-<%= lr > 2.0 ? 'danger' : lr > 1.0 ? 'warning' : 'success' %>">
|
|
365
|
+
<%= sl[:load_1m] %><% if sl[:cpu_count] %> / <%= sl[:cpu_count] %> CPUs<% end %>
|
|
366
|
+
</code>
|
|
367
|
+
<small class="text-muted ms-1">(5m: <%= sl[:load_5m] %>, 15m: <%= sl[:load_15m] %>)</small>
|
|
368
|
+
</div>
|
|
369
|
+
<% end %>
|
|
370
|
+
|
|
371
|
+
<% if health[:system_memory] %>
|
|
372
|
+
<% sm = health[:system_memory] %>
|
|
373
|
+
<div class="mb-1">
|
|
374
|
+
<small class="text-muted">System Memory:</small>
|
|
375
|
+
<% used_pct = sm[:used_pct].to_f %>
|
|
376
|
+
<code class="ms-1 text-<%= used_pct > 90 ? 'danger' : used_pct > 80 ? 'warning' : 'success' %>">
|
|
377
|
+
<%= sm[:available_mb] %> MB free / <%= sm[:total_mb] %> MB (<%= used_pct %>% used)
|
|
378
|
+
</code>
|
|
379
|
+
</div>
|
|
380
|
+
<% if sm[:swap_used_mb].to_i > 0 %>
|
|
381
|
+
<div class="mb-1">
|
|
382
|
+
<small class="text-muted">System Swap:</small>
|
|
383
|
+
<code class="ms-1 text-danger"><%= sm[:swap_used_mb] %> MB used</code>
|
|
384
|
+
</div>
|
|
385
|
+
<% end %>
|
|
386
|
+
<% end %>
|
|
387
|
+
|
|
388
|
+
<% if health[:tcp_connections] %>
|
|
389
|
+
<% tcp = health[:tcp_connections] %>
|
|
390
|
+
<div class="mb-1">
|
|
391
|
+
<small class="text-muted">TCP Connections:</small>
|
|
392
|
+
<code class="ms-1">
|
|
393
|
+
<%= tcp[:established] %> established<% if tcp[:close_wait].to_i > 0 %>, <span class="text-danger"><%= tcp[:close_wait] %> close_wait</span><% end %><% if tcp[:time_wait].to_i > 0 %>, <span class="text-warning"><%= tcp[:time_wait] %> time_wait</span><% end %>
|
|
394
|
+
</code>
|
|
316
395
|
</div>
|
|
317
396
|
<% end %>
|
|
318
397
|
|
|
@@ -327,6 +406,16 @@
|
|
|
327
406
|
</div>
|
|
328
407
|
<% end %>
|
|
329
408
|
|
|
409
|
+
<% if health[:gc_latest] %>
|
|
410
|
+
<% gcl = health[:gc_latest] %>
|
|
411
|
+
<div class="mb-1">
|
|
412
|
+
<small class="text-muted">Last GC:</small>
|
|
413
|
+
<code class="ms-1">
|
|
414
|
+
<%= gcl[:major_by] ? "major (#{gcl[:major_by]})" : "minor" %> by <%= gcl[:gc_by] %><% if gcl[:state].to_s != "none" %>, <span class="text-warning">state: <%= gcl[:state] %></span><% end %>
|
|
415
|
+
</code>
|
|
416
|
+
</div>
|
|
417
|
+
<% end %>
|
|
418
|
+
|
|
330
419
|
<% if health[:connection_pool] %>
|
|
331
420
|
<% cp = health[:connection_pool] %>
|
|
332
421
|
<div class="mb-1">
|
|
@@ -226,7 +226,27 @@ module RailsErrorDashboard
|
|
|
226
226
|
say "\n"
|
|
227
227
|
end
|
|
228
228
|
|
|
229
|
+
def detect_existing_config
|
|
230
|
+
initializer_path = File.join(destination_root, "config/initializers/rails_error_dashboard.rb")
|
|
231
|
+
return unless File.exist?(initializer_path)
|
|
232
|
+
|
|
233
|
+
content = File.read(initializer_path)
|
|
234
|
+
@existing_install_detected = true
|
|
235
|
+
|
|
236
|
+
# Detect separate database from existing config
|
|
237
|
+
if content.match?(/config\.use_separate_database\s*=\s*true/)
|
|
238
|
+
@database_mode = :separate
|
|
239
|
+
@database_name = content[/config\.database\s*=\s*:(\w+)/, 1] || "error_dashboard"
|
|
240
|
+
@enable_separate_database = true
|
|
241
|
+
@application_name = detect_application_name
|
|
242
|
+
say_status "detected", "existing separate database configuration", :green
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
229
246
|
def select_database_mode
|
|
247
|
+
# Skip if existing config already detected database mode
|
|
248
|
+
return if @existing_install_detected && @database_mode
|
|
249
|
+
|
|
230
250
|
# Skip if not interactive or if --separate_database was passed via CLI
|
|
231
251
|
if options[:separate_database]
|
|
232
252
|
@database_mode = :separate
|
|
@@ -333,6 +353,12 @@ module RailsErrorDashboard
|
|
|
333
353
|
@enable_crash_capture = @selected_features&.dig(:crash_capture) || options[:crash_capture]
|
|
334
354
|
@enable_diagnostic_dump = @selected_features&.dig(:diagnostic_dump) || options[:diagnostic_dump]
|
|
335
355
|
|
|
356
|
+
# Don't overwrite existing initializer on upgrade — user's config is precious
|
|
357
|
+
if @existing_install_detected
|
|
358
|
+
say_status "skip", "config/initializers/rails_error_dashboard.rb (preserving existing config)", :yellow
|
|
359
|
+
return
|
|
360
|
+
end
|
|
361
|
+
|
|
336
362
|
template "initializer.rb", "config/initializers/rails_error_dashboard.rb"
|
|
337
363
|
end
|
|
338
364
|
|
|
@@ -343,10 +369,17 @@ module RailsErrorDashboard
|
|
|
343
369
|
|
|
344
370
|
FileUtils.mkdir_p(target_dir)
|
|
345
371
|
|
|
346
|
-
# Check
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
372
|
+
# Check BOTH migration directories to prevent cross-directory duplication (#93)
|
|
373
|
+
# A user who installed with separate DB and re-runs without the flag should not
|
|
374
|
+
# get duplicate migrations in db/migrate/
|
|
375
|
+
existing = Set.new
|
|
376
|
+
[ "db/migrate", "db/error_dashboard_migrate" ].each do |dir|
|
|
377
|
+
full_path = File.join(destination_root, dir)
|
|
378
|
+
next unless Dir.exist?(full_path)
|
|
379
|
+
Dir.glob(File.join(full_path, "*rails_error_dashboard*.rb")).each do |f|
|
|
380
|
+
existing.add(File.basename(f).sub(/^\d+_/, ""))
|
|
381
|
+
end
|
|
382
|
+
end
|
|
350
383
|
|
|
351
384
|
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
|
352
385
|
|
|
@@ -4,16 +4,21 @@ module RailsErrorDashboard
|
|
|
4
4
|
module Services
|
|
5
5
|
# Pure algorithm: Capture runtime health metrics at error time
|
|
6
6
|
#
|
|
7
|
-
# Captures GC stats, process RSS
|
|
8
|
-
#
|
|
7
|
+
# Captures GC stats, process memory (RSS/swap/peak), thread count, connection pool,
|
|
8
|
+
# Puma stats, job queue, RubyVM/YJIT, ActionCable, file descriptors, system load,
|
|
9
|
+
# system memory pressure, GC context, and TCP connection states.
|
|
10
|
+
#
|
|
11
|
+
# NOT memoized — fresh data every call (unlike EnvironmentSnapshot).
|
|
9
12
|
# Every metric call individually wrapped in rescue => nil.
|
|
10
13
|
#
|
|
11
14
|
# Safety contract (from HOST_APP_SAFETY.md):
|
|
12
|
-
# - Total snapshot < 1ms budget
|
|
15
|
+
# - Total snapshot < 1ms budget (~0.3ms typical on Linux)
|
|
13
16
|
# - NEVER ObjectSpace.each_object or ObjectSpace.count_objects (heap scan)
|
|
14
17
|
# - NEVER Thread.list.map(&:backtrace) (GVL hold)
|
|
15
18
|
# - Thread.list.count only (O(1), safe)
|
|
16
|
-
# - Process
|
|
19
|
+
# - Process/system metrics: Linux procfs ONLY, no fork/subprocess ever
|
|
20
|
+
# - All procfs reads guarded with File.exist? — returns nil on macOS/non-Linux
|
|
21
|
+
# - TCP file size guard (skip if > 1MB) to protect against connection leak scenarios
|
|
17
22
|
# - No new gems, no global state, no Thread.current, no mutex
|
|
18
23
|
class SystemHealthSnapshot
|
|
19
24
|
# Capture current system health metrics
|
|
@@ -27,9 +32,12 @@ module RailsErrorDashboard
|
|
|
27
32
|
|
|
28
33
|
# @return [Hash] Health snapshot
|
|
29
34
|
def capture
|
|
35
|
+
mem = process_memory
|
|
30
36
|
{
|
|
31
37
|
gc: gc_stats,
|
|
32
|
-
|
|
38
|
+
gc_latest: gc_latest,
|
|
39
|
+
process_memory: mem,
|
|
40
|
+
process_memory_mb: mem&.dig(:rss_mb), # backward compat
|
|
33
41
|
thread_count: thread_count,
|
|
34
42
|
connection_pool: connection_pool_stats,
|
|
35
43
|
puma: puma_stats,
|
|
@@ -37,6 +45,10 @@ module RailsErrorDashboard
|
|
|
37
45
|
ruby_vm: ruby_vm_stats,
|
|
38
46
|
yjit: yjit_stats,
|
|
39
47
|
actioncable: actioncable_stats,
|
|
48
|
+
file_descriptors: file_descriptors,
|
|
49
|
+
system_load: system_load,
|
|
50
|
+
system_memory: system_memory,
|
|
51
|
+
tcp_connections: tcp_connections,
|
|
40
52
|
captured_at: Time.current.iso8601
|
|
41
53
|
}
|
|
42
54
|
end
|
|
@@ -55,13 +67,33 @@ module RailsErrorDashboard
|
|
|
55
67
|
nil
|
|
56
68
|
end
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
# GC.latest_gc_info — context about the most recent GC run
|
|
71
|
+
# Works on all platforms (Ruby API, no procfs)
|
|
72
|
+
def gc_latest
|
|
73
|
+
info = GC.latest_gc_info
|
|
74
|
+
{
|
|
75
|
+
major_by: info[:major_by]&.to_s,
|
|
76
|
+
gc_by: info[:gc_by]&.to_s,
|
|
77
|
+
state: info[:state]&.to_s,
|
|
78
|
+
immediate_sweep: info[:immediate_sweep]
|
|
79
|
+
}
|
|
80
|
+
rescue => e
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Process memory from /proc/self/status — single file read, 4 fields extracted
|
|
85
|
+
# Linux ONLY — returns nil on macOS/non-Linux (~0.02ms)
|
|
86
|
+
def process_memory
|
|
60
87
|
return nil unless File.exist?("/proc/self/status")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return nil unless
|
|
64
|
-
|
|
88
|
+
status = File.read("/proc/self/status")
|
|
89
|
+
rss = status[/^VmRSS:\s+(\d+)/, 1]&.to_i
|
|
90
|
+
return nil unless rss
|
|
91
|
+
{
|
|
92
|
+
rss_mb: (rss / 1024.0).round(1),
|
|
93
|
+
swap_mb: (status[/^VmSwap:\s+(\d+)/, 1].to_i / 1024.0).round(1),
|
|
94
|
+
rss_peak_mb: (status[/^VmHWM:\s+(\d+)/, 1].to_i / 1024.0).round(1),
|
|
95
|
+
os_threads: status[/^Threads:\s+(\d+)/, 1]&.to_i
|
|
96
|
+
}
|
|
65
97
|
rescue => e
|
|
66
98
|
nil
|
|
67
99
|
end
|
|
@@ -186,6 +218,88 @@ module RailsErrorDashboard
|
|
|
186
218
|
rescue => e
|
|
187
219
|
nil
|
|
188
220
|
end
|
|
221
|
+
|
|
222
|
+
# File descriptor count vs ulimit — detects FD exhaustion
|
|
223
|
+
# Linux ONLY — /proc/self/fd (~0.05ms)
|
|
224
|
+
def file_descriptors
|
|
225
|
+
return nil unless File.exist?("/proc/self/fd")
|
|
226
|
+
open_count = Dir.children("/proc/self/fd").size
|
|
227
|
+
soft_limit, _hard = Process.getrlimit(:NOFILE)
|
|
228
|
+
{
|
|
229
|
+
open: open_count,
|
|
230
|
+
limit: soft_limit,
|
|
231
|
+
utilization_pct: soft_limit > 0 ? (open_count.to_f / soft_limit * 100).round(1) : nil
|
|
232
|
+
}
|
|
233
|
+
rescue => e
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# System load averages from /proc/loadavg + CPU count
|
|
238
|
+
# Linux ONLY — returns nil on macOS (~0.01ms)
|
|
239
|
+
def system_load
|
|
240
|
+
return nil unless File.exist?("/proc/loadavg")
|
|
241
|
+
parts = File.read("/proc/loadavg").split
|
|
242
|
+
require "etc" unless defined?(Etc)
|
|
243
|
+
cpu_count = Etc.nprocessors rescue nil
|
|
244
|
+
load_1m = parts[0].to_f
|
|
245
|
+
{
|
|
246
|
+
load_1m: load_1m,
|
|
247
|
+
load_5m: parts[1].to_f,
|
|
248
|
+
load_15m: parts[2].to_f,
|
|
249
|
+
cpu_count: cpu_count,
|
|
250
|
+
load_ratio: cpu_count && cpu_count > 0 ? (load_1m / cpu_count).round(2) : nil
|
|
251
|
+
}
|
|
252
|
+
rescue => e
|
|
253
|
+
nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# System-wide memory pressure from /proc/meminfo
|
|
257
|
+
# Linux ONLY — returns nil on macOS (~0.02ms)
|
|
258
|
+
def system_memory
|
|
259
|
+
return nil unless File.exist?("/proc/meminfo")
|
|
260
|
+
meminfo = File.read("/proc/meminfo")
|
|
261
|
+
total = meminfo[/^MemTotal:\s+(\d+)/, 1]&.to_i
|
|
262
|
+
available = meminfo[/^MemAvailable:\s+(\d+)/, 1]&.to_i
|
|
263
|
+
swap_total = meminfo[/^SwapTotal:\s+(\d+)/, 1]&.to_i
|
|
264
|
+
swap_free = meminfo[/^SwapFree:\s+(\d+)/, 1]&.to_i
|
|
265
|
+
{
|
|
266
|
+
total_mb: total ? (total / 1024.0).round(0) : nil,
|
|
267
|
+
available_mb: available ? (available / 1024.0).round(0) : nil,
|
|
268
|
+
used_pct: total && available && total > 0 ? ((1 - available.to_f / total) * 100).round(1) : nil,
|
|
269
|
+
swap_used_mb: swap_total && swap_free ? ((swap_total - swap_free) / 1024.0).round(0) : nil
|
|
270
|
+
}
|
|
271
|
+
rescue => e
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# TCP connection states from /proc/self/net/tcp (+tcp6)
|
|
276
|
+
# Linux ONLY — returns nil on macOS (~0.05ms typical)
|
|
277
|
+
# Safety: skips if file > 1MB (protects against connection leak scenarios)
|
|
278
|
+
def tcp_connections
|
|
279
|
+
path = "/proc/self/net/tcp"
|
|
280
|
+
return nil unless File.exist?(path)
|
|
281
|
+
return nil if File.size(path) > 1_048_576
|
|
282
|
+
lines = File.readlines(path).drop(1)
|
|
283
|
+
states = lines.map { |l| l.strip.split[3] }
|
|
284
|
+
result = {
|
|
285
|
+
established: states.count("01"),
|
|
286
|
+
close_wait: states.count("08"),
|
|
287
|
+
time_wait: states.count("06"),
|
|
288
|
+
listen: states.count("0A")
|
|
289
|
+
}
|
|
290
|
+
path6 = "/proc/self/net/tcp6"
|
|
291
|
+
if File.exist?(path6) && File.size(path6) <= 1_048_576
|
|
292
|
+
lines6 = File.readlines(path6).drop(1)
|
|
293
|
+
states6 = lines6.map { |l| l.strip.split[3] }
|
|
294
|
+
result[:established] += states6.count("01")
|
|
295
|
+
result[:close_wait] += states6.count("08")
|
|
296
|
+
result[:time_wait] += states6.count("06")
|
|
297
|
+
result[:listen] += states6.count("0A")
|
|
298
|
+
end
|
|
299
|
+
result
|
|
300
|
+
rescue => e
|
|
301
|
+
nil
|
|
302
|
+
end
|
|
189
303
|
end
|
|
190
304
|
end
|
|
191
305
|
end
|
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.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -465,7 +465,7 @@ metadata:
|
|
|
465
465
|
bug_tracker_uri: https://github.com/AnjanJ/rails_error_dashboard/issues
|
|
466
466
|
funding_uri: https://buymeacoffee.com/anjanj
|
|
467
467
|
post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
|
|
468
|
-
\ Rails Error Dashboard v0.5.
|
|
468
|
+
\ Rails Error Dashboard v0.5.2\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
|
|
469
469
|
First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
|
|
470
470
|
db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
|
|
471
471
|
=> '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n
|