memhealth 0.1.0 → 0.1.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 +40 -42
- data/app/views/layouts/memhealth/application.html.erb +12 -1
- data/app/views/mem_health/dashboard/index.html.erb +52 -26
- data/lib/mem_health/middleware.rb +32 -21
- data/lib/mem_health/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69e8587a6c8a55a3a4e5b50ed6b433c4557f067e867cc9c30520bac3f184a48c
|
|
4
|
+
data.tar.gz: d7c228db15571b67db8a59adc334aa5611717a61939662bc5abde5464cdfb3e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a82811c70c3ee43ed58dce8f0e8a169d9b7008f2a72303c67e0b654b5b7b653e25c05be27542fc9c168846516299566a763851ac3b98c8a0051908cf813e7fda
|
|
7
|
+
data.tar.gz: e7f878a2dfffe6c9e4f2ab276c5447889af056be98380e54e94a8783ff099fa8b51a94c63a32986add73eaa3b9417bd6a7c606bbf3d21c967f8fc9ddec641f56
|
data/README.md
CHANGED
|
@@ -2,7 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
A Rails engine for monitoring memory health, detecting growth patterns (leaks & bloats) and memory swap operations. Helps you identify requests that consume high amounts of RAM and is compatible with Heroku.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "memhealth"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Use initializer `/config/memhealth.rb` to configure Redis connection:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
MemHealth.configure do |config|
|
|
17
|
+
config.redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Enable memory tracking via ENV variable:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
MEM_HEALTH_ENABLED=true
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Dashboard setup
|
|
28
|
+
|
|
29
|
+
Mount the engine in your routes within an authenticated section:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# config/routes.rb
|
|
33
|
+
Rails.application.routes.draw do
|
|
34
|
+
# ... other routes ...
|
|
35
|
+
|
|
36
|
+
# Mount within authenticated admin section
|
|
37
|
+
authenticate :admin_user do
|
|
38
|
+
mount MemHealth::Engine, at: "/admin/memhealth"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
# Features
|
|
6
44
|
|
|
7
45
|
- Real-time memory usage monitoring
|
|
8
46
|
- Track highest memory consuming requests
|
|
@@ -23,25 +61,7 @@ If you're getting **R14 - Memory quota exceeded** errors, it means your applicat
|
|
|
23
61
|
|
|
24
62
|
MemHealth helps you identify which specific requests are consuming excessive memory, allowing you to pinpoint and fix the root cause of these performance issues.
|
|
25
63
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
Add this line to your application's Gemfile:
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
gem "memhealth", git: "https://github.com/topkeyhq/memhealth"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
And then execute:
|
|
35
|
-
|
|
36
|
-
$ bundle install
|
|
37
|
-
|
|
38
|
-
## Configuration
|
|
39
|
-
|
|
40
|
-
```ruby
|
|
41
|
-
MemHealth.configure do |config|
|
|
42
|
-
config.redis_url = ENV.fetch(ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
|
|
43
|
-
end
|
|
44
|
-
```
|
|
64
|
+
## ENVIRONMENT VARIABLES
|
|
45
65
|
|
|
46
66
|
Configure Memhealth using environment variables:
|
|
47
67
|
|
|
@@ -56,28 +76,6 @@ Configure Memhealth using environment variables:
|
|
|
56
76
|
|
|
57
77
|
The gem will read the Redis connection URL from the environment variable specified in `MEM_HEALTH_REDIS_KEY`, falling back to `REDIS_URL` if not specified.
|
|
58
78
|
|
|
59
|
-
## Usage
|
|
60
|
-
|
|
61
|
-
Mount the engine in your routes within an authenticated section:
|
|
62
|
-
|
|
63
|
-
```ruby
|
|
64
|
-
# config/routes.rb
|
|
65
|
-
Rails.application.routes.draw do
|
|
66
|
-
# ... other routes ...
|
|
67
|
-
|
|
68
|
-
# Mount within authenticated admin section
|
|
69
|
-
authenticate :admin_user do
|
|
70
|
-
mount MemHealth::Engine, at: "/admin/memhealth"
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Enable memory tracking by setting:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
MEM_HEALTH_ENABLED=true
|
|
79
|
-
```
|
|
80
|
-
|
|
81
79
|
### ActiveAdmin Integration
|
|
82
80
|
|
|
83
81
|
To add Memhealth to your ActiveAdmin Operations menu, add this to your ActiveAdmin initializer:
|
|
@@ -129,6 +129,17 @@
|
|
|
129
129
|
|
|
130
130
|
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
131
131
|
.whitespace-nowrap { white-space: nowrap; }
|
|
132
|
+
|
|
133
|
+
/* URL truncation in table */
|
|
134
|
+
td.url-cell {
|
|
135
|
+
max-width: 500px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
td.url-cell > div {
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
text-overflow: ellipsis;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
132
143
|
.text-center { text-align: center; }
|
|
133
144
|
.text-left { text-align: left; }
|
|
134
145
|
.uppercase { text-transform: uppercase; }
|
|
@@ -158,7 +169,7 @@
|
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
/* Container styles */
|
|
161
|
-
.container { max-width:
|
|
172
|
+
.container { max-width: 1600px; margin: 0 auto; }
|
|
162
173
|
|
|
163
174
|
/* Header styles */
|
|
164
175
|
.header {
|
|
@@ -15,41 +15,51 @@
|
|
|
15
15
|
<!-- URL Section -->
|
|
16
16
|
<div class="mb-4">
|
|
17
17
|
<dt class="text-sm font-medium text-gray-500 mb-1">URL</dt>
|
|
18
|
-
<dd class="text-sm text-gray-900 font-mono bg-gray-100 px-2 py-1 rounded
|
|
19
|
-
|
|
18
|
+
<dd class="text-sm text-gray-900 font-mono bg-gray-100 px-2 py-1 rounded ml-0 flex items-center gap-2" title="<%= @max_memory_url["url"] %>">
|
|
19
|
+
<% if @max_memory_url["request_method"] %>
|
|
20
|
+
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded flex-shrink-0 <%= @max_memory_url["request_method"] == 'GET' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>">
|
|
21
|
+
<%= @max_memory_url["request_method"] %>
|
|
22
|
+
</span>
|
|
23
|
+
<% end %>
|
|
24
|
+
<span class="truncate"><%= @max_memory_url["url"] %></span>
|
|
20
25
|
</dd>
|
|
21
26
|
</div>
|
|
22
27
|
|
|
23
|
-
<!--
|
|
28
|
+
<!-- Single row with all metadata -->
|
|
24
29
|
<div class="flex gap-4">
|
|
25
|
-
|
|
26
|
-
<div class="
|
|
27
|
-
<dt class="text-sm font-medium text-
|
|
28
|
-
<dd class="text-
|
|
29
|
-
<%= @max_memory_url["
|
|
30
|
+
<div class="flex-1 text-left">
|
|
31
|
+
<div class="bg-red-100 border border-red-300 rounded-md px-4 py-3">
|
|
32
|
+
<dt class="text-sm font-medium text-red-700 mb-1">Memory Diff</dt>
|
|
33
|
+
<dd class="text-lg font-bold text-red-800 ml-0">
|
|
34
|
+
<%= @max_memory_url["memory_diff"] %> MB
|
|
30
35
|
</dd>
|
|
31
36
|
</div>
|
|
32
|
-
|
|
37
|
+
</div>
|
|
33
38
|
|
|
34
39
|
<div class="flex-1 text-left">
|
|
35
|
-
<dt class="text-sm font-medium text-gray-500 mb-1">
|
|
36
|
-
<dd class="text-sm text-gray-600 ml-0"
|
|
40
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">RAM Usage</dt>
|
|
41
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
42
|
+
<%= @max_memory_url["ram_before"] %> MB → <%= @max_memory_url["ram_after"] %> MB
|
|
43
|
+
</dd>
|
|
37
44
|
</div>
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</
|
|
44
|
-
|
|
46
|
+
<div class="flex-1 text-left">
|
|
47
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">Execution Time</dt>
|
|
48
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
49
|
+
<%= @max_memory_url["execution_time"] ? "#{@max_memory_url["execution_time"]} s" : "-" %>
|
|
50
|
+
</dd>
|
|
51
|
+
</div>
|
|
45
52
|
|
|
46
53
|
<div class="flex-1 text-left">
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">Info</dt>
|
|
55
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
56
|
+
<% info_parts = [] %>
|
|
57
|
+
<% info_parts << @max_memory_url["dyno"] if @max_memory_url["dyno"] %>
|
|
58
|
+
<% info_parts << "T#{@max_memory_url["puma_thread_index"]}" if @max_memory_url["puma_thread_index"] %>
|
|
59
|
+
<% info_parts << "##{@max_memory_url["account_id"]}" if @max_memory_url["account_id"].present? %>
|
|
60
|
+
<% info_parts << Time.parse(@max_memory_url["recorded_at"]).utc.strftime("%m/%d %H:%M") %>
|
|
61
|
+
<%= info_parts.join(" / ") %>
|
|
62
|
+
</dd>
|
|
53
63
|
</div>
|
|
54
64
|
</div>
|
|
55
65
|
</div>
|
|
@@ -100,7 +110,9 @@
|
|
|
100
110
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory Diff</th>
|
|
101
111
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM Before</th>
|
|
102
112
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM After</th>
|
|
113
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Exec Time</th>
|
|
103
114
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
|
115
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dyno/Thread</th>
|
|
104
116
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account ID</th>
|
|
105
117
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Recorded At</th>
|
|
106
118
|
</tr>
|
|
@@ -129,11 +141,25 @@
|
|
|
129
141
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
130
142
|
<%= url_data["ram_after"] ? "#{url_data["ram_after"]} MB" : "/" %>
|
|
131
143
|
</td>
|
|
132
|
-
<td class="px-6 py-4 text-sm text-gray-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
145
|
+
<%= url_data["execution_time"] ? "#{url_data["execution_time"]} s" : "/" %>
|
|
146
|
+
</td>
|
|
147
|
+
<td class="px-6 py-4 text-gray-700 url-cell">
|
|
148
|
+
<div title="<%= url_data["url"] %>" style="font-size: 0.75rem; line-height: 1rem;">
|
|
149
|
+
<% if url_data["request_method"] %>
|
|
150
|
+
<span class="inline-flex px-1 py-0.5 font-semibold rounded <%= url_data["request_method"] == 'GET' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>" style="font-size: 0.65rem;">
|
|
151
|
+
<%= url_data["request_method"] %>
|
|
152
|
+
</span>
|
|
153
|
+
<% end %>
|
|
154
|
+
<code class="bg-gray-100 px-1 py-0.5 rounded"><%= url_data["url"] %></code>
|
|
135
155
|
</div>
|
|
136
156
|
</td>
|
|
157
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
158
|
+
<% dyno_thread = [] %>
|
|
159
|
+
<% dyno_thread << url_data["dyno"] if url_data["dyno"] %>
|
|
160
|
+
<% dyno_thread << "T#{url_data["puma_thread_index"]}" if url_data["puma_thread_index"] %>
|
|
161
|
+
<%= dyno_thread.any? ? dyno_thread.join("/") : "/" %>
|
|
162
|
+
</td>
|
|
137
163
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
138
164
|
<%= url_data["account_id"].present? ? url_data["account_id"] : "/" %>
|
|
139
165
|
</td>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require
|
|
1
|
+
require 'get_process_mem'
|
|
2
2
|
|
|
3
3
|
module MemHealth
|
|
4
4
|
class Middleware
|
|
@@ -16,19 +16,38 @@ module MemHealth
|
|
|
16
16
|
def call(env)
|
|
17
17
|
@@request_count += 1
|
|
18
18
|
|
|
19
|
+
# Track execution time
|
|
20
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
21
|
+
|
|
19
22
|
before = GetProcessMem.new.mb
|
|
20
23
|
status, headers, response = @app.call(env)
|
|
21
24
|
after = GetProcessMem.new.mb
|
|
22
25
|
|
|
26
|
+
# Calculate execution time in seconds
|
|
27
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
28
|
+
execution_time = (end_time - start_time).round(2)
|
|
29
|
+
|
|
23
30
|
memory_diff = (after - before).round(2)
|
|
24
|
-
|
|
31
|
+
request = Rack::Request.new(env)
|
|
32
|
+
request_url = request.fullpath
|
|
25
33
|
account_info = extract_account_info(env)
|
|
26
34
|
|
|
35
|
+
# Collect additional metadata
|
|
36
|
+
metadata = {
|
|
37
|
+
puma_thread_index: Thread.current[:puma_thread_index],
|
|
38
|
+
dyno: ENV['DYNO'],
|
|
39
|
+
execution_time: execution_time,
|
|
40
|
+
request_method: request.request_method
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
# Skip the first few requests as they have large memory jumps due to class loading
|
|
28
44
|
if @@request_count > config.skip_requests
|
|
29
45
|
redis.incr(redis_tracked_requests_key)
|
|
30
46
|
should_track = memory_diff > config.memory_threshold_mb && before.round(2) > config.ram_before_threshold_mb
|
|
31
|
-
|
|
47
|
+
if should_track
|
|
48
|
+
track_memory_usage(memory_diff, request_url, before.round(2), after.round(2),
|
|
49
|
+
account_info.merge(metadata))
|
|
50
|
+
end
|
|
32
51
|
else
|
|
33
52
|
@@skipped_requests_count += 1
|
|
34
53
|
end
|
|
@@ -80,7 +99,7 @@ module MemHealth
|
|
|
80
99
|
self.class.redis_tracked_requests_key
|
|
81
100
|
end
|
|
82
101
|
|
|
83
|
-
def track_memory_usage(memory_diff, request_url, ram_before, ram_after,
|
|
102
|
+
def track_memory_usage(memory_diff, request_url, ram_before, ram_after, request_metadata = {})
|
|
84
103
|
# Update max memory diff seen so far
|
|
85
104
|
current_max = redis.get(redis_max_diff_key)&.to_f || 0.0
|
|
86
105
|
if memory_diff > current_max
|
|
@@ -94,7 +113,7 @@ module MemHealth
|
|
|
94
113
|
ram_after: ram_after,
|
|
95
114
|
timestamp: Time.current.to_i,
|
|
96
115
|
recorded_at: Time.current.iso8601
|
|
97
|
-
}.merge(
|
|
116
|
+
}.merge(request_metadata)
|
|
98
117
|
redis.set(redis_max_diff_url_key, max_url_data.to_json)
|
|
99
118
|
end
|
|
100
119
|
|
|
@@ -107,25 +126,17 @@ module MemHealth
|
|
|
107
126
|
ram_after: ram_after,
|
|
108
127
|
timestamp: timestamp,
|
|
109
128
|
recorded_at: Time.current.iso8601
|
|
110
|
-
}.merge(
|
|
129
|
+
}.merge(request_metadata)
|
|
111
130
|
|
|
112
131
|
# Add URL to sorted set (score = memory_diff for DESC ordering)
|
|
113
132
|
redis.zadd(redis_high_usage_urls_key, memory_diff, url_data.to_json)
|
|
114
133
|
|
|
115
134
|
# Keep only top N URLs by removing lowest scores
|
|
116
135
|
current_count = redis.zcard(redis_high_usage_urls_key)
|
|
117
|
-
|
|
118
|
-
# Remove the lowest scoring URLs (keep top max_stored_urls)
|
|
119
|
-
redis.zremrangebyrank(redis_high_usage_urls_key, 0, current_count - config.max_stored_urls - 1)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def build_request_url(env)
|
|
124
|
-
request = Rack::Request.new(env)
|
|
125
|
-
url = "#{request.request_method} #{request.fullpath}"
|
|
136
|
+
return unless current_count > config.max_stored_urls
|
|
126
137
|
|
|
127
|
-
#
|
|
128
|
-
(
|
|
138
|
+
# Remove the lowest scoring URLs (keep top max_stored_urls)
|
|
139
|
+
redis.zremrangebyrank(redis_high_usage_urls_key, 0, current_count - config.max_stored_urls - 1)
|
|
129
140
|
end
|
|
130
141
|
|
|
131
142
|
def extract_account_info(env)
|
|
@@ -136,13 +147,13 @@ module MemHealth
|
|
|
136
147
|
if defined?(ActsAsTenant) && ActsAsTenant.current_tenant
|
|
137
148
|
account = ActsAsTenant.current_tenant
|
|
138
149
|
account_info[:account_id] = account.id
|
|
139
|
-
elsif env[
|
|
150
|
+
elsif env['warden']&.user&.respond_to?(:account)
|
|
140
151
|
# Try to get from authenticated user
|
|
141
|
-
account = env[
|
|
152
|
+
account = env['warden'].user.account
|
|
142
153
|
account_info[:account_id] = account.id
|
|
143
|
-
elsif env[
|
|
154
|
+
elsif env['HTTP_X_ACCOUNT_ID']
|
|
144
155
|
# Fallback to header if available
|
|
145
|
-
account_info[:account_id] = env[
|
|
156
|
+
account_info[:account_id] = env['HTTP_X_ACCOUNT_ID']
|
|
146
157
|
end
|
|
147
158
|
rescue StandardError => _e
|
|
148
159
|
# Silently fail if account extraction fails
|
data/lib/mem_health/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: memhealth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Klemen Nagode
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2025-09-15 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rails
|
|
@@ -91,6 +92,7 @@ licenses:
|
|
|
91
92
|
metadata:
|
|
92
93
|
homepage_uri: https://github.com/topkeyhq/memhealth
|
|
93
94
|
source_code_uri: https://github.com/topkeyhq/memhealth
|
|
95
|
+
post_install_message:
|
|
94
96
|
rdoc_options: []
|
|
95
97
|
require_paths:
|
|
96
98
|
- lib
|
|
@@ -105,7 +107,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
105
107
|
- !ruby/object:Gem::Version
|
|
106
108
|
version: '0'
|
|
107
109
|
requirements: []
|
|
108
|
-
rubygems_version: 3.
|
|
110
|
+
rubygems_version: 3.5.16
|
|
111
|
+
signing_key:
|
|
109
112
|
specification_version: 4
|
|
110
113
|
summary: Rails memory health monitoring and tracking
|
|
111
114
|
test_files: []
|