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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c0895575f5adc6767b228212ace922f4eb66eb565771c05bde602b4c4b48971
4
- data.tar.gz: b44315dc375c9fa4108a5fb889f76dd1a0520883f9c8952a675c8fd840bc981a
3
+ metadata.gz: 69e8587a6c8a55a3a4e5b50ed6b433c4557f067e867cc9c30520bac3f184a48c
4
+ data.tar.gz: d7c228db15571b67db8a59adc334aa5611717a61939662bc5abde5464cdfb3e4
5
5
  SHA512:
6
- metadata.gz: f5e263596880a31d83003ebe56700a6ed94d8bb6eafa60222b7424fbb1f8632b9a534b9952d6c892d41637a43f66df0a4d673d6e7f62c4f35c0279382a8486cc
7
- data.tar.gz: 80ad19cf0d82d6a4f543701a82736c6837d265b0daa57ef9a709df99c550452b2fb8e77f131add2e86e2871c0978729d06e84035539b42cc1eb5c40ff6e5f2f5
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
- ## Features
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
- ## Installation
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: 1200px; margin: 0 auto; }
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 truncate ml-0">
19
- <%= @max_memory_url["url"] %>
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
- <!-- Four column layout using flex -->
28
+ <!-- Single row with all metadata -->
24
29
  <div class="flex gap-4">
25
- <% if @max_memory_url["ram_before"] && @max_memory_url["ram_after"] %>
26
- <div class="flex-1 text-left">
27
- <dt class="text-sm font-medium text-gray-500 mb-1">RAM Usage</dt>
28
- <dd class="text-sm text-gray-600 ml-0">
29
- <%= @max_memory_url["ram_before"] %> MB → <%= @max_memory_url["ram_after"] %> MB
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
- <% end %>
37
+ </div>
33
38
 
34
39
  <div class="flex-1 text-left">
35
- <dt class="text-sm font-medium text-gray-500 mb-1">Recorded At</dt>
36
- <dd class="text-sm text-gray-600 ml-0"><%= Time.parse(@max_memory_url["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %></dd>
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
- <% if @max_memory_url["account_id"].present? %>
40
- <div class="flex-1 text-left">
41
- <dt class="text-sm font-medium text-gray-500 mb-1">Account ID</dt>
42
- <dd class="text-sm text-gray-600 ml-0"><%= @max_memory_url["account_id"] %></dd>
43
- </div>
44
- <% end %>
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
- <div class="bg-red-100 border border-red-300 rounded-md px-4 py-3">
48
- <dt class="text-sm font-medium text-red-700 mb-1">Memory Diff</dt>
49
- <dd class="text-lg font-bold text-red-800 ml-0">
50
- <%= @max_memory_url["memory_diff"] %> MB
51
- </dd>
52
- </div>
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-900">
133
- <div class="max-w-xs truncate">
134
- <code class="bg-gray-100 px-2 py-1 rounded text-xs"><%= url_data["url"] %></code>
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 "get_process_mem"
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
- request_url = build_request_url(env)
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
- track_memory_usage(memory_diff, request_url, before.round(2), after.round(2), account_info) if should_track
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, account_info = {})
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(account_info)
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(account_info)
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
- if current_count > config.max_stored_urls
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
- # Truncate very long URLs
128
- (url.length > 600) ? "#{url[0..650]}..." : url
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["warden"]&.user&.respond_to?(:account)
150
+ elsif env['warden']&.user&.respond_to?(:account)
140
151
  # Try to get from authenticated user
141
- account = env["warden"].user.account
152
+ account = env['warden'].user.account
142
153
  account_info[:account_id] = account.id
143
- elsif env["HTTP_X_ACCOUNT_ID"]
154
+ elsif env['HTTP_X_ACCOUNT_ID']
144
155
  # Fallback to header if available
145
- account_info[:account_id] = env["HTTP_X_ACCOUNT_ID"]
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
@@ -1,3 +1,3 @@
1
1
  module MemHealth
2
- VERSION = "0.1.0"
2
+ VERSION = '0.1.1'
3
3
  end
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.0
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: 1980-01-02 00:00:00.000000000 Z
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.6.7
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: []