memhealth 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c0895575f5adc6767b228212ace922f4eb66eb565771c05bde602b4c4b48971
4
- data.tar.gz: b44315dc375c9fa4108a5fb889f76dd1a0520883f9c8952a675c8fd840bc981a
3
+ metadata.gz: 5fda50a609f167feffba18ed772f70c528fe8ae883605520a8b51283ea9e45af
4
+ data.tar.gz: 30bb76d54747594b6fe1b2b9432759a53f201b2ed8b6e5852a78fe7775f654a1
5
5
  SHA512:
6
- metadata.gz: f5e263596880a31d83003ebe56700a6ed94d8bb6eafa60222b7424fbb1f8632b9a534b9952d6c892d41637a43f66df0a4d673d6e7f62c4f35c0279382a8486cc
7
- data.tar.gz: 80ad19cf0d82d6a4f543701a82736c6837d265b0daa57ef9a709df99c550452b2fb8e77f131add2e86e2871c0978729d06e84035539b42cc1eb5c40ff6e5f2f5
6
+ metadata.gz: 2d69b6f4a6f3341f1eb02f64301d690479788fbd6de6e8ef65e9da88ce40c709af7743c685d290d42e020003d67bd093318ed6189ebe722fa1a37eb25017a800
7
+ data.tar.gz: 9160e5176c77f8d06e3f6ade37ecc39b0ba171829370eb93f33b332b51c775096c403aba3cd52e977b17912fc33123d8f9ba2b9d47da73aaf47138bab8c98072
data/README.md CHANGED
@@ -2,61 +2,29 @@
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
6
-
7
- - Real-time memory usage monitoring
8
- - Track highest memory consuming requests
9
- - Account-level tracking for multi-tenant apps
10
- - Redis-based data storage
11
- - Web dashboard for viewing statistics
12
- - Configurable thresholds and limits
13
-
14
- <img width="1139" height="680" alt="s_2" src="https://github.com/user-attachments/assets/5e170097-77cf-4ec5-a7b0-47aeaf92135f" />
15
-
16
- <img width="1142" height="696" alt="s_1" src="https://github.com/user-attachments/assets/68eeb503-e259-4dc0-b3b1-3438375b42d4" />
17
-
18
- ## Heroku Memory Issues
19
-
20
- If you're getting **R14 - Memory quota exceeded** errors, it means your application is using swap memory. Swap uses the disk to store memory instead of RAM. Disk speed is significantly slower than RAM, so page access time is greatly increased. This leads to a significant degradation in application performance. An application that is swapping will be much slower than one that is not. No one wants a slow application, so getting rid of R14 Memory quota exceeded errors on your application is very important.
21
-
22
- <img width="909" height="272" alt="Screenshot 2025-08-30 at 18 46 41" src="https://github.com/user-attachments/assets/65bb2131-4dfd-4974-9647-805e44bc35f7" />
23
-
24
- 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
-
26
5
  ## Installation
27
6
 
28
7
  Add this line to your application's Gemfile:
29
8
 
30
9
  ```ruby
31
- gem "memhealth", git: "https://github.com/topkeyhq/memhealth"
10
+ gem "memhealth"
32
11
  ```
33
12
 
34
- And then execute:
35
-
36
- $ bundle install
37
-
38
- ## Configuration
13
+ Use initializer `/config/memhealth.rb` to configure Redis connection:
39
14
 
40
15
  ```ruby
41
16
  MemHealth.configure do |config|
42
- config.redis_url = ENV.fetch(ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
17
+ config.redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
43
18
  end
44
19
  ```
45
20
 
46
- Configure Memhealth using environment variables:
47
-
48
- | Environment Variable | Description | Default Value |
49
- | --------------------------------- | ------------------------------------------------------------------------------ | ------------- |
50
- | `MEM_HEALTH_ENABLED` | Enable/disable memory tracking | `false` |
51
- | `MEM_HEALTH_SKIP_REQUESTS` | Number of initial requests to skip (avoids class loading overhead) | `10` |
52
- | `MEM_HEALTH_MEMORY_THRESHOLD_MB` | Minimum memory difference (MB) to track a request | `1` |
53
- | `MEM_HEALTH_RAM_BEFORE_THRESHOLD` | Minimum RAM usage (MB) before tracking (prevents tracking low-memory requests) | `0` |
54
- | `MEM_HEALTH_MAX_STORED_URLS` | Maximum number of URLs to store in Redis | `20` |
55
- | `MEM_HEALTH_REDIS_KEY` | Name of environment variable containing Redis URL (e.g., `REDISCLOUD_URL`) | `REDIS_URL` |
21
+ Enable memory tracking via ENV variable:
56
22
 
57
- 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.
23
+ ```bash
24
+ MEM_HEALTH_ENABLED=true
25
+ ```
58
26
 
59
- ## Usage
27
+ ### Dashboard setup
60
28
 
61
29
  Mount the engine in your routes within an authenticated section:
62
30
 
@@ -72,12 +40,59 @@ Rails.application.routes.draw do
72
40
  end
73
41
  ```
74
42
 
75
- Enable memory tracking by setting:
43
+ ### Worker tracking setup (Sidekiq)
76
44
 
77
- ```bash
78
- MEM_HEALTH_ENABLED=true
45
+ To track memory usage in background jobs, add the middleware to your Sidekiq configuration:
46
+
47
+ ```ruby
48
+ # config/initializers/sidekiq.rb
49
+ Sidekiq.configure_server do |config|
50
+ config.server_middleware do |chain|
51
+ chain.add MemHealth::JobTrackingMiddleware
52
+ end
53
+ end
79
54
  ```
80
55
 
56
+ The dashboard will show two tabs:
57
+ - **Web Servers**: Tracks memory usage for HTTP requests
58
+ - **Worker Servers**: Tracks memory usage for Sidekiq background jobs
59
+
60
+ # Features
61
+
62
+ - Real-time memory usage monitoring for web and worker servers
63
+ - Track highest memory consuming requests and background jobs
64
+ - Account-level tracking for multi-tenant apps
65
+ - Redis-based data storage
66
+ - Web dashboard with separate views for web and worker metrics
67
+ - Configurable thresholds and limits
68
+
69
+ <img width="1139" height="680" alt="s_2" src="https://github.com/user-attachments/assets/5e170097-77cf-4ec5-a7b0-47aeaf92135f" />
70
+
71
+ <img width="1142" height="696" alt="s_1" src="https://github.com/user-attachments/assets/68eeb503-e259-4dc0-b3b1-3438375b42d4" />
72
+
73
+ ## Heroku Memory Issues
74
+
75
+ If you're getting **R14 - Memory quota exceeded** errors, it means your application is using swap memory. Swap uses the disk to store memory instead of RAM. Disk speed is significantly slower than RAM, so page access time is greatly increased. This leads to a significant degradation in application performance. An application that is swapping will be much slower than one that is not. No one wants a slow application, so getting rid of R14 Memory quota exceeded errors on your application is very important.
76
+
77
+ <img width="909" height="272" alt="Screenshot 2025-08-30 at 18 46 41" src="https://github.com/user-attachments/assets/65bb2131-4dfd-4974-9647-805e44bc35f7" />
78
+
79
+ MemHealth helps you identify which specific requests are consuming excessive memory, allowing you to pinpoint and fix the root cause of these performance issues.
80
+
81
+ ## ENVIRONMENT VARIABLES
82
+
83
+ Configure Memhealth using environment variables:
84
+
85
+ | Environment Variable | Description | Default Value |
86
+ | --------------------------------- | ------------------------------------------------------------------------------ | ------------- |
87
+ | `MEM_HEALTH_ENABLED` | Enable/disable memory tracking | `false` |
88
+ | `MEM_HEALTH_SKIP_REQUESTS` | Number of initial requests to skip (avoids class loading overhead) | `10` |
89
+ | `MEM_HEALTH_MEMORY_THRESHOLD_MB` | Minimum memory difference (MB) to track a request | `1` |
90
+ | `MEM_HEALTH_RAM_BEFORE_THRESHOLD` | Minimum RAM usage (MB) before tracking (prevents tracking low-memory requests) | `0` |
91
+ | `MEM_HEALTH_MAX_STORED_URLS` | Maximum number of URLs to store in Redis | `20` |
92
+ | `MEM_HEALTH_REDIS_KEY` | Name of environment variable containing Redis URL (e.g., `REDISCLOUD_URL`) | `REDIS_URL` |
93
+
94
+ 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.
95
+
81
96
  ### ActiveAdmin Integration
82
97
 
83
98
  To add Memhealth to your ActiveAdmin Operations menu, add this to your ActiveAdmin initializer:
@@ -99,13 +114,23 @@ end
99
114
  ## Console Usage
100
115
 
101
116
  ```ruby
102
- # View statistics
117
+ # View web statistics
103
118
  MemHealth::Tracker.print_stats
104
119
 
105
- # Get top memory consuming URLs
120
+ # Get top memory consuming URLs (web)
106
121
  MemHealth::Tracker.top_memory_urls
107
122
 
108
- # Clear all data
123
+ # Get top memory consuming jobs (worker)
124
+ MemHealth::Tracker.top_memory_jobs
125
+
126
+ # Get worker statistics
127
+ MemHealth::Tracker.worker_stats
128
+
129
+ # Get max memory diff for web or worker
130
+ MemHealth::Tracker.max_memory_diff(type: :web)
131
+ MemHealth::Tracker.max_memory_diff(type: :worker)
132
+
133
+ # Clear all data (web and worker)
109
134
  MemHealth::Tracker.clear_all_data
110
135
  ```
111
136
 
@@ -2,18 +2,25 @@ module MemHealth
2
2
  class DashboardController < ActionController::Base
3
3
  protect_from_forgery with: :exception
4
4
  layout "memhealth/application"
5
-
5
+
6
6
  def index
7
7
  @memory_hunter_enabled = MemHealth.configuration.enabled?
8
+ @view_type = params[:view] || 'web'
8
9
 
9
10
  if @memory_hunter_enabled
10
- @stats = MemHealth::Tracker.stats
11
- @top_urls = MemHealth::Tracker.top_memory_urls
12
- @max_memory_url = MemHealth::Tracker.max_memory_url
11
+ if @view_type == 'worker'
12
+ @stats = MemHealth::Tracker.worker_stats
13
+ @top_items = MemHealth::Tracker.top_memory_jobs
14
+ @max_memory_item = MemHealth::Tracker.max_memory_job
15
+ else
16
+ @stats = MemHealth::Tracker.stats
17
+ @top_items = MemHealth::Tracker.top_memory_urls
18
+ @max_memory_item = MemHealth::Tracker.max_memory_url
19
+ end
13
20
  else
14
21
  @stats = nil
15
- @top_urls = []
16
- @max_memory_url = nil
22
+ @top_items = []
23
+ @max_memory_item = nil
17
24
  end
18
25
  end
19
26
 
@@ -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 {
@@ -0,0 +1,172 @@
1
+ <!-- Highest Memory Usage URL - Prominent display -->
2
+ <% if @max_memory_item %>
3
+ <div class="bg-red-50 border border-red-200 rounded-lg px-6 py-6 mb-6">
4
+ <h3 class="text-lg font-semibold text-red-800 mb-4">🚨 Highest Memory Usage URL</h3>
5
+
6
+ <div class="bg-white rounded-md px-6 py-4 border border-red-100">
7
+ <!-- URL Section -->
8
+ <div class="mb-4">
9
+ <dt class="text-sm font-medium text-gray-500 mb-1">URL</dt>
10
+ <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_item["url"] %>">
11
+ <% if @max_memory_item["request_method"] %>
12
+ <span class="inline-flex px-2 py-1 text-xs font-semibold rounded flex-shrink-0 <%= @max_memory_item["request_method"] == 'GET' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>">
13
+ <%= @max_memory_item["request_method"] %>
14
+ </span>
15
+ <% end %>
16
+ <span class="truncate"><%= @max_memory_item["url"] %></span>
17
+ </dd>
18
+ </div>
19
+
20
+ <!-- Single row with all metadata -->
21
+ <div class="flex gap-4">
22
+ <div class="flex-1 text-left">
23
+ <div class="bg-red-100 border border-red-300 rounded-md px-4 py-3">
24
+ <dt class="text-sm font-medium text-red-700 mb-1">Memory Diff</dt>
25
+ <dd class="text-lg font-bold text-red-800 ml-0">
26
+ <%= @max_memory_item["memory_diff"] %> MB
27
+ </dd>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="flex-1 text-left">
32
+ <dt class="text-sm font-medium text-gray-500 mb-1">RAM Usage</dt>
33
+ <dd class="text-sm text-gray-600 ml-0">
34
+ <%= @max_memory_item["ram_before"] %> MB → <%= @max_memory_item["ram_after"] %> MB
35
+ </dd>
36
+ </div>
37
+
38
+ <div class="flex-1 text-left">
39
+ <dt class="text-sm font-medium text-gray-500 mb-1">Execution Time</dt>
40
+ <dd class="text-sm text-gray-600 ml-0">
41
+ <%= @max_memory_item["execution_time"] ? "#{@max_memory_item["execution_time"]} s" : "-" %>
42
+ </dd>
43
+ </div>
44
+
45
+ <div class="flex-1 text-left">
46
+ <dt class="text-sm font-medium text-gray-500 mb-1">Info</dt>
47
+ <dd class="text-sm text-gray-600 ml-0">
48
+ <% info_parts = [] %>
49
+ <% info_parts << @max_memory_item["dyno"] if @max_memory_item["dyno"] %>
50
+ <% info_parts << "T#{@max_memory_item["puma_thread_index"]}" if @max_memory_item["puma_thread_index"] %>
51
+ <% info_parts << "##{@max_memory_item["account_id"]}" if @max_memory_item["account_id"].present? %>
52
+ <% info_parts << Time.parse(@max_memory_item["recorded_at"]).utc.strftime("%m/%d %H:%M") %>
53
+ <%= info_parts.join(" / ") %>
54
+ </dd>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <% end %>
60
+
61
+ <!-- Stats summary card -->
62
+ <div class="bg-white rounded-lg shadow px-6 py-6 mb-6">
63
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Memory Usage Statistics</h3>
64
+
65
+ <div class="flex gap-4">
66
+ <div class="bg-blue-50 p-4 rounded-md flex-1 text-left">
67
+ <dt class="text-sm font-medium text-blue-600">Max Memory Difference</dt>
68
+ <dd class="mt-1 text-2xl font-semibold text-blue-900 text-left ml-0"><%= @stats[:max_memory_diff] %> MB</dd>
69
+ </div>
70
+
71
+ <div class="bg-green-50 p-4 rounded-md flex-1 text-left">
72
+ <dt class="text-sm font-medium text-green-600">Stored URLs</dt>
73
+ <dd class="mt-1 text-2xl font-semibold text-green-900 text-left ml-0"><%= @stats[:stored_urls_count] %>/<%= @stats[:max_stored_urls] %></dd>
74
+ </div>
75
+
76
+ <div class="bg-yellow-50 p-4 rounded-md flex-1 text-left">
77
+ <dt class="text-sm font-medium text-yellow-600">Requests Tracked</dt>
78
+ <dd class="mt-1 text-2xl font-semibold text-yellow-900 text-left ml-0"><%= @stats[:tracked_requests_count] %></dd>
79
+ </div>
80
+
81
+ <div class="bg-purple-50 p-4 rounded-md flex-1 text-left">
82
+ <dt class="text-sm font-medium text-purple-600">Total Requests on current dyno/Rails instance</dt>
83
+ <dd class="mt-1 text-2xl font-semibold text-purple-900 text-left ml-0"><%= @stats[:total_requests_count] %></dd>
84
+ <% if @stats[:skipped_requests_count] > 0 %>
85
+ <p class="text-xs text-purple-500 mt-1"><%= @stats[:skipped_requests_count] %> skipped</p>
86
+ <% end %>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Top URLs table -->
92
+ <% if @top_items.any? %>
93
+ <div class="bg-white rounded-lg shadow overflow-hidden">
94
+ <div class="px-6 py-4 border-b border-gray-200">
95
+ <h3 class="text-lg font-medium text-gray-900">Top <%= MemHealth.configuration.max_stored_urls %> URLs by Memory Usage</h3>
96
+ <p class="text-sm text-gray-600 mt-1">URLs ordered by memory difference (highest first) - maximum <%= MemHealth.configuration.max_stored_urls %> URLs stored</p>
97
+ </div>
98
+
99
+ <table class="min-w-full divide-y divide-gray-200">
100
+ <thead class="bg-gray-50">
101
+ <tr>
102
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory Diff</th>
103
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM Before</th>
104
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM After</th>
105
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Exec Time</th>
106
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
107
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dyno/Thread</th>
108
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account ID</th>
109
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Recorded At</th>
110
+ </tr>
111
+ </thead>
112
+
113
+ <tbody class="bg-white divide-y divide-gray-200">
114
+ <% @top_items.each do |url_data| %>
115
+ <tr>
116
+ <td class="px-6 py-4 whitespace-nowrap">
117
+ <%
118
+ memory_color = if url_data["memory_diff"] > 10
119
+ "bg-red-100 text-red-800"
120
+ elsif url_data["memory_diff"] > 5
121
+ "bg-yellow-100 text-yellow-800"
122
+ else
123
+ "bg-green-100 text-green-800"
124
+ end
125
+ %>
126
+ <span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full <%= memory_color %>">
127
+ <%= url_data["memory_diff"] %> MB
128
+ </span>
129
+ </td>
130
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
131
+ <%= url_data["ram_before"] ? "#{url_data["ram_before"]} MB" : "/" %>
132
+ </td>
133
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
134
+ <%= url_data["ram_after"] ? "#{url_data["ram_after"]} MB" : "/" %>
135
+ </td>
136
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
137
+ <%= url_data["execution_time"] ? "#{url_data["execution_time"]} s" : "/" %>
138
+ </td>
139
+ <td class="px-6 py-4 text-gray-700 url-cell">
140
+ <div title="<%= url_data["url"] %>" style="font-size: 0.75rem; line-height: 1rem;">
141
+ <% if url_data["request_method"] %>
142
+ <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;">
143
+ <%= url_data["request_method"] %>
144
+ </span>
145
+ <% end %>
146
+ <code class="bg-gray-100 px-1 py-0.5 rounded"><%= url_data["url"] %></code>
147
+ </div>
148
+ </td>
149
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
150
+ <% dyno_thread = [] %>
151
+ <% dyno_thread << url_data["dyno"] if url_data["dyno"] %>
152
+ <% dyno_thread << "T#{url_data["puma_thread_index"]}" if url_data["puma_thread_index"] %>
153
+ <%= dyno_thread.any? ? dyno_thread.join("/") : "/" %>
154
+ </td>
155
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
156
+ <%= url_data["account_id"].present? ? url_data["account_id"] : "/" %>
157
+ </td>
158
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
159
+ <%= Time.parse(url_data["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %>
160
+ </td>
161
+ </tr>
162
+ <% end %>
163
+ </tbody>
164
+ </table>
165
+ </div>
166
+ <% else %>
167
+ <div class="bg-white rounded-lg shadow px-6 py-4 text-center">
168
+ <h3 class="text-lg font-medium text-gray-900 mb-2">No URLs Tracked Yet</h3>
169
+ <p class="text-gray-600">No URLs have been recorded yet.</p>
170
+ <p class="text-sm text-gray-500 mt-2">Memory tracking will begin after the first <%= MemHealth.configuration.skip_requests %> requests to your application.</p>
171
+ </div>
172
+ <% end %>
@@ -0,0 +1,166 @@
1
+ <!-- Highest Memory Usage Job - Prominent display -->
2
+ <% if @max_memory_item %>
3
+ <div class="bg-red-50 border border-red-200 rounded-lg px-6 py-6 mb-6">
4
+ <h3 class="text-lg font-semibold text-red-800 mb-4">🚨 Highest Memory Usage Job</h3>
5
+
6
+ <div class="bg-white rounded-md px-6 py-4 border border-red-100">
7
+ <!-- Job Section -->
8
+ <div class="mb-4">
9
+ <dt class="text-sm font-medium text-gray-500 mb-1">Worker Class</dt>
10
+ <dd class="text-sm text-gray-900 font-mono bg-gray-100 px-2 py-1 rounded ml-0">
11
+ <%= @max_memory_item["worker_class"] %>
12
+ </dd>
13
+ <% if @max_memory_item["job_args"].present? %>
14
+ <dt class="text-sm font-medium text-gray-500 mb-1 mt-2">Arguments</dt>
15
+ <dd class="text-xs text-gray-600 font-mono bg-gray-50 px-2 py-1 rounded ml-0 break-all">
16
+ <%= @max_memory_item["job_args"] %>
17
+ </dd>
18
+ <% end %>
19
+ </div>
20
+
21
+ <!-- Single row with all metadata -->
22
+ <div class="flex gap-4">
23
+ <div class="flex-1 text-left">
24
+ <div class="bg-red-100 border border-red-300 rounded-md px-4 py-3">
25
+ <dt class="text-sm font-medium text-red-700 mb-1">Memory Diff</dt>
26
+ <dd class="text-lg font-bold text-red-800 ml-0">
27
+ <%= @max_memory_item["memory_diff"] %> MB
28
+ </dd>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="flex-1 text-left">
33
+ <dt class="text-sm font-medium text-gray-500 mb-1">RAM Usage</dt>
34
+ <dd class="text-sm text-gray-600 ml-0">
35
+ <%= @max_memory_item["ram_before"] %> MB → <%= @max_memory_item["ram_after"] %> MB
36
+ </dd>
37
+ </div>
38
+
39
+ <div class="flex-1 text-left">
40
+ <dt class="text-sm font-medium text-gray-500 mb-1">Execution Time</dt>
41
+ <dd class="text-sm text-gray-600 ml-0">
42
+ <%= @max_memory_item["execution_time"] ? "#{@max_memory_item["execution_time"]} s" : "-" %>
43
+ </dd>
44
+ </div>
45
+
46
+ <div class="flex-1 text-left">
47
+ <dt class="text-sm font-medium text-gray-500 mb-1">Queue</dt>
48
+ <dd class="text-sm text-gray-600 ml-0">
49
+ <%= @max_memory_item["queue"] || "-" %>
50
+ </dd>
51
+ </div>
52
+
53
+ <div class="flex-1 text-left">
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_item["dyno"] if @max_memory_item["dyno"] %>
58
+ <% info_parts << Time.parse(@max_memory_item["recorded_at"]).utc.strftime("%m/%d %H:%M") %>
59
+ <%= info_parts.join(" / ") %>
60
+ </dd>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <% end %>
66
+
67
+ <!-- Stats summary card -->
68
+ <div class="bg-white rounded-lg shadow px-6 py-6 mb-6">
69
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Worker Memory Usage Statistics</h3>
70
+
71
+ <div class="flex gap-4">
72
+ <div class="bg-blue-50 p-4 rounded-md flex-1 text-left">
73
+ <dt class="text-sm font-medium text-blue-600">Max Memory Difference</dt>
74
+ <dd class="mt-1 text-2xl font-semibold text-blue-900 text-left ml-0"><%= @stats[:max_memory_diff] %> MB</dd>
75
+ </div>
76
+
77
+ <div class="bg-green-50 p-4 rounded-md flex-1 text-left">
78
+ <dt class="text-sm font-medium text-green-600">Stored Jobs</dt>
79
+ <dd class="mt-1 text-2xl font-semibold text-green-900 text-left ml-0"><%= @stats[:stored_jobs_count] %>/<%= @stats[:max_stored_jobs] %></dd>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Top Jobs table -->
85
+ <% if @top_items.any? %>
86
+ <div class="bg-white rounded-lg shadow overflow-hidden">
87
+ <div class="px-6 py-4 border-b border-gray-200">
88
+ <h3 class="text-lg font-medium text-gray-900">Top <%= MemHealth.configuration.max_stored_urls %> Jobs by Memory Usage</h3>
89
+ <p class="text-sm text-gray-600 mt-1">Jobs ordered by memory difference (highest first) - maximum <%= MemHealth.configuration.max_stored_urls %> jobs stored</p>
90
+ </div>
91
+
92
+ <table class="min-w-full divide-y divide-gray-200">
93
+ <thead class="bg-gray-50">
94
+ <tr>
95
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory Diff</th>
96
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM Before</th>
97
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM After</th>
98
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Exec Time</th>
99
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Worker Class / Args</th>
100
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Queue</th>
101
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dyno</th>
102
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job ID</th>
103
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Recorded At</th>
104
+ </tr>
105
+ </thead>
106
+
107
+ <tbody class="bg-white divide-y divide-gray-200">
108
+ <% @top_items.each do |job_data| %>
109
+ <tr>
110
+ <td class="px-6 py-4 whitespace-nowrap">
111
+ <%
112
+ memory_color = if job_data["memory_diff"] > 10
113
+ "bg-red-100 text-red-800"
114
+ elsif job_data["memory_diff"] > 5
115
+ "bg-yellow-100 text-yellow-800"
116
+ else
117
+ "bg-green-100 text-green-800"
118
+ end
119
+ %>
120
+ <span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full <%= memory_color %>">
121
+ <%= job_data["memory_diff"] %> MB
122
+ </span>
123
+ </td>
124
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
125
+ <%= job_data["ram_before"] ? "#{job_data["ram_before"]} MB" : "/" %>
126
+ </td>
127
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
128
+ <%= job_data["ram_after"] ? "#{job_data["ram_after"]} MB" : "/" %>
129
+ </td>
130
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
131
+ <%= job_data["execution_time"] ? "#{job_data["execution_time"]} s" : "/" %>
132
+ </td>
133
+ <td class="px-6 py-4 text-gray-700">
134
+ <div>
135
+ <code class="bg-gray-100 px-1 py-0.5 rounded text-xs"><%= job_data["worker_class"] %></code>
136
+ <% if job_data["job_args"].present? %>
137
+ <div class="text-xs text-gray-500 mt-1 font-mono truncate" title="<%= job_data["job_args"] %>">
138
+ <%= job_data["job_args"] %>
139
+ </div>
140
+ <% end %>
141
+ </div>
142
+ </td>
143
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
144
+ <%= job_data["queue"] || "/" %>
145
+ </td>
146
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
147
+ <%= job_data["dyno"] || "/" %>
148
+ </td>
149
+ <td class="px-6 py-4 text-sm text-gray-500 font-mono">
150
+ <%= job_data["job_id"] ? job_data["job_id"][0..7] : "/" %>
151
+ </td>
152
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
153
+ <%= Time.parse(job_data["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %>
154
+ </td>
155
+ </tr>
156
+ <% end %>
157
+ </tbody>
158
+ </table>
159
+ </div>
160
+ <% else %>
161
+ <div class="bg-white rounded-lg shadow px-6 py-4 text-center">
162
+ <h3 class="text-lg font-medium text-gray-900 mb-2">No Jobs Tracked Yet</h3>
163
+ <p class="text-gray-600">No background jobs have been recorded yet.</p>
164
+ <p class="text-sm text-gray-500 mt-2">Memory tracking will begin once background jobs start running with the JobTrackingMiddleware enabled.</p>
165
+ </div>
166
+ <% end %>
@@ -6,151 +6,21 @@
6
6
  <p class="text-sm text-green-700">Memory profiling is active and tracking requests. To disable, set <code class="bg-green-100 px-1 rounded">ENV["MEM_HEALTH_ENABLED"]=false</code></p>
7
7
  </div>
8
8
 
9
- <!-- Highest Memory Usage URL - Prominent display -->
10
- <% if @max_memory_url %>
11
- <div class="bg-red-50 border border-red-200 rounded-lg px-6 py-6 mb-6">
12
- <h3 class="text-lg font-semibold text-red-800 mb-4">🚨 Highest Memory Usage URL</h3>
13
-
14
- <div class="bg-white rounded-md px-6 py-4 border border-red-100">
15
- <!-- URL Section -->
16
- <div class="mb-4">
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"] %>
20
- </dd>
21
- </div>
22
-
23
- <!-- Four column layout using flex -->
24
- <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
- </dd>
31
- </div>
32
- <% end %>
33
-
34
- <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>
37
- </div>
38
-
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 %>
45
-
46
- <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>
53
- </div>
54
- </div>
55
- </div>
56
- </div>
57
- <% end %>
58
-
59
- <!-- Stats summary card -->
60
- <div class="bg-white rounded-lg shadow px-6 py-6 mb-6">
61
- <h3 class="text-lg font-medium text-gray-900 mb-4">Memory Usage Statistics</h3>
62
-
63
- <div class="flex gap-4">
64
- <div class="bg-blue-50 p-4 rounded-md flex-1 text-left">
65
- <dt class="text-sm font-medium text-blue-600">Max Memory Difference</dt>
66
- <dd class="mt-1 text-2xl font-semibold text-blue-900 text-left ml-0"><%= @stats[:max_memory_diff] %> MB</dd>
67
- </div>
68
-
69
- <div class="bg-green-50 p-4 rounded-md flex-1 text-left">
70
- <dt class="text-sm font-medium text-green-600">Stored URLs</dt>
71
- <dd class="mt-1 text-2xl font-semibold text-green-900 text-left ml-0"><%= @stats[:stored_urls_count] %>/<%= @stats[:max_stored_urls] %></dd>
72
- </div>
73
-
74
- <div class="bg-yellow-50 p-4 rounded-md flex-1 text-left">
75
- <dt class="text-sm font-medium text-yellow-600">Requests Tracked</dt>
76
- <dd class="mt-1 text-2xl font-semibold text-yellow-900 text-left ml-0"><%= @stats[:tracked_requests_count] %></dd>
77
- </div>
78
-
79
- <div class="bg-purple-50 p-4 rounded-md flex-1 text-left">
80
- <dt class="text-sm font-medium text-purple-600">Total Requests on current dyno/Rails instance</dt>
81
- <dd class="mt-1 text-2xl font-semibold text-purple-900 text-left ml-0"><%= @stats[:total_requests_count] %></dd>
82
- <% if @stats[:skipped_requests_count] > 0 %>
83
- <p class="text-xs text-purple-500 mt-1"><%= @stats[:skipped_requests_count] %> skipped</p>
84
- <% end %>
85
- </div>
86
- </div>
9
+ <!-- Tab Navigation -->
10
+ <div class="mb-6">
11
+ <nav class="flex space-x-4 border-b border-gray-200">
12
+ <%= link_to "Web Servers", root_path(view: 'web'),
13
+ class: "px-4 py-2 text-sm font-medium border-b-2 #{@view_type == 'web' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}" %>
14
+ <%= link_to "Worker Servers", root_path(view: 'worker'),
15
+ class: "px-4 py-2 text-sm font-medium border-b-2 #{@view_type == 'worker' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}" %>
16
+ </nav>
87
17
  </div>
88
18
 
89
- <!-- Top URLs table -->
90
- <% if @top_urls.any? %>
91
- <div class="bg-white rounded-lg shadow overflow-hidden">
92
- <div class="px-6 py-4 border-b border-gray-200">
93
- <h3 class="text-lg font-medium text-gray-900">Top <%= MemHealth.configuration.max_stored_urls %> URLs by Memory Usage</h3>
94
- <p class="text-sm text-gray-600 mt-1">URLs ordered by memory difference (highest first) - maximum <%= MemHealth.configuration.max_stored_urls %> URLs stored</p>
95
- </div>
96
-
97
- <table class="min-w-full divide-y divide-gray-200">
98
- <thead class="bg-gray-50">
99
- <tr>
100
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory Diff</th>
101
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM Before</th>
102
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM After</th>
103
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
104
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account ID</th>
105
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Recorded At</th>
106
- </tr>
107
- </thead>
108
-
109
- <tbody class="bg-white divide-y divide-gray-200">
110
- <% @top_urls.each do |url_data| %>
111
- <tr>
112
- <td class="px-6 py-4 whitespace-nowrap">
113
- <%
114
- memory_color = if url_data["memory_diff"] > 10
115
- "bg-red-100 text-red-800"
116
- elsif url_data["memory_diff"] > 5
117
- "bg-yellow-100 text-yellow-800"
118
- else
119
- "bg-green-100 text-green-800"
120
- end
121
- %>
122
- <span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full <%= memory_color %>">
123
- <%= url_data["memory_diff"] %> MB
124
- </span>
125
- </td>
126
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
127
- <%= url_data["ram_before"] ? "#{url_data["ram_before"]} MB" : "/" %>
128
- </td>
129
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
130
- <%= url_data["ram_after"] ? "#{url_data["ram_after"]} MB" : "/" %>
131
- </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>
135
- </div>
136
- </td>
137
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
138
- <%= url_data["account_id"].present? ? url_data["account_id"] : "/" %>
139
- </td>
140
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
141
- <%= Time.parse(url_data["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %>
142
- </td>
143
- </tr>
144
- <% end %>
145
- </tbody>
146
- </table>
147
- </div>
19
+ <!-- View Content -->
20
+ <% if @view_type == 'worker' %>
21
+ <%= render 'worker_view' %>
148
22
  <% else %>
149
- <div class="bg-white rounded-lg shadow px-6 py-4 text-center">
150
- <h3 class="text-lg font-medium text-gray-900 mb-2">No URLs Tracked Yet</h3>
151
- <p class="text-gray-600">No URLs have been recorded yet.</p>
152
- <p class="text-sm text-gray-500 mt-2">Memory tracking will begin after the first <%= MemHealth.configuration.skip_requests %> requests to your application.</p>
153
- </div>
23
+ <%= render 'web_view' %>
154
24
  <% end %>
155
25
 
156
26
  <!-- Action buttons -->
@@ -0,0 +1,97 @@
1
+ module MemHealth
2
+ class JobTrackingMiddleware
3
+ include TrackingConcern
4
+
5
+ def call(worker, job, queue, &block)
6
+ return yield unless config.enabled?
7
+
8
+ metrics = measure_memory(&block)
9
+
10
+ # Track if memory usage is significant
11
+ unless metrics[:memory_diff] > config.memory_threshold_mb && metrics[:before] > config.ram_before_threshold_mb
12
+ return
13
+ end
14
+
15
+ related_model = extract_related_model(job)
16
+ job_class = extract_job_class(worker, job)
17
+
18
+ metadata = {
19
+ worker_class: related_model ? "#{job_class} (#{related_model})" : job_class,
20
+ queue: queue,
21
+ dyno: ENV['DYNO'],
22
+ job_id: job['jid'],
23
+ job_args: extract_job_args(job)
24
+ }
25
+
26
+ track_memory_usage(metrics[:memory_diff], metrics[:before], metrics[:after],
27
+ metadata.merge(execution_time: metrics[:execution_time]), type: :worker)
28
+ end
29
+
30
+ private
31
+
32
+ def extract_job_class(worker, job)
33
+ # For ActiveJob jobs, extract the actual job class from the wrapper
34
+ if worker.class.name == 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper'
35
+ job['wrapped'] || job['args']&.first&.dig('job_class') || worker.class.name
36
+ else
37
+ worker.class.name
38
+ end
39
+ end
40
+
41
+ def extract_job_args(job)
42
+ # Extract arguments, limiting size to avoid storing huge data
43
+ args = if job['wrapped'] # ActiveJob
44
+ job['args']&.first&.dig('arguments')
45
+ else # Native Sidekiq
46
+ job['args']
47
+ end
48
+
49
+ return nil unless args
50
+
51
+ # Convert to string and truncate if too long
52
+ args_string = args.inspect
53
+ args_string.length > 2000 ? "#{args_string[0..1970]}..." : args_string
54
+ rescue StandardError
55
+ nil
56
+ end
57
+
58
+ def extract_related_model(job)
59
+ # Try to find related model from job arguments
60
+ args = if job['wrapped'] # ActiveJob
61
+ job['args']&.first&.dig('arguments')
62
+ else
63
+ job['args']
64
+ end
65
+
66
+ return nil unless args
67
+
68
+ # Look for GlobalID patterns that indicate the actual model being processed
69
+ global_ids = extract_global_ids(args)
70
+ return nil if global_ids.empty?
71
+
72
+ # Return the first non-ActiveJob related model
73
+ global_ids.first
74
+ rescue StandardError
75
+ nil
76
+ end
77
+
78
+ def extract_global_ids(obj, results = [])
79
+ case obj
80
+ when Hash
81
+ if obj['_aj_globalid']
82
+ # Extract model class from GlobalID (e.g., "gid://app/ModelName/123" -> "ModelName")
83
+ gid = obj['_aj_globalid']
84
+ if gid =~ %r{gid://[^/]+/([^/]+)/}
85
+ model_name = $1
86
+ results << model_name unless results.include?(model_name)
87
+ end
88
+ else
89
+ obj.each_value { |v| extract_global_ids(v, results) }
90
+ end
91
+ when Array
92
+ obj.each { |v| extract_global_ids(v, results) }
93
+ end
94
+ results
95
+ end
96
+ end
97
+ end
@@ -1,7 +1,7 @@
1
- require "get_process_mem"
2
-
3
1
  module MemHealth
4
2
  class Middleware
3
+ include TrackingConcern
4
+
5
5
  @@request_count = 0
6
6
  @@skipped_requests_count = 0
7
7
 
@@ -16,24 +16,34 @@ module MemHealth
16
16
  def call(env)
17
17
  @@request_count += 1
18
18
 
19
- before = GetProcessMem.new.mb
20
- status, headers, response = @app.call(env)
21
- after = GetProcessMem.new.mb
19
+ request = Rack::Request.new(env)
22
20
 
23
- memory_diff = (after - before).round(2)
24
- request_url = build_request_url(env)
25
- account_info = extract_account_info(env)
21
+ metrics = measure_memory do
22
+ @status, @headers, @response = @app.call(env)
23
+ end
26
24
 
27
25
  # Skip the first few requests as they have large memory jumps due to class loading
28
26
  if @@request_count > config.skip_requests
29
27
  redis.incr(redis_tracked_requests_key)
30
- 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
28
+ should_track = metrics[:memory_diff] > config.memory_threshold_mb &&
29
+ metrics[:before] > config.ram_before_threshold_mb
30
+
31
+ if should_track
32
+ metadata = {
33
+ url: request.fullpath,
34
+ puma_thread_index: Thread.current[:puma_thread_index],
35
+ dyno: ENV['DYNO'],
36
+ request_method: request.request_method
37
+ }.merge(extract_account_info(env))
38
+
39
+ track_memory_usage(metrics[:memory_diff], metrics[:before], metrics[:after],
40
+ metadata.merge(execution_time: metrics[:execution_time]), type: :web)
41
+ end
32
42
  else
33
43
  @@skipped_requests_count += 1
34
44
  end
35
45
 
36
- [status, headers, response]
46
+ [@status, @headers, @response]
37
47
  end
38
48
 
39
49
  def self.reset_data
@@ -56,78 +66,10 @@ module MemHealth
56
66
 
57
67
  private
58
68
 
59
- def config
60
- MemHealth.configuration
61
- end
62
-
63
- def redis
64
- config.redis
65
- end
66
-
67
- def redis_max_diff_key
68
- "#{config.redis_key_prefix}:max_diff"
69
- end
70
-
71
- def redis_max_diff_url_key
72
- "#{config.redis_key_prefix}:max_diff_url"
73
- end
74
-
75
- def redis_high_usage_urls_key
76
- "#{config.redis_key_prefix}:high_usage_urls"
77
- end
78
-
79
69
  def redis_tracked_requests_key
80
70
  self.class.redis_tracked_requests_key
81
71
  end
82
72
 
83
- def track_memory_usage(memory_diff, request_url, ram_before, ram_after, account_info = {})
84
- # Update max memory diff seen so far
85
- current_max = redis.get(redis_max_diff_key)&.to_f || 0.0
86
- if memory_diff > current_max
87
- redis.set(redis_max_diff_key, memory_diff)
88
-
89
- # Store the URL data that caused the maximum memory usage
90
- max_url_data = {
91
- url: request_url,
92
- memory_diff: memory_diff,
93
- ram_before: ram_before,
94
- ram_after: ram_after,
95
- timestamp: Time.current.to_i,
96
- recorded_at: Time.current.iso8601
97
- }.merge(account_info)
98
- redis.set(redis_max_diff_url_key, max_url_data.to_json)
99
- end
100
-
101
- # Store all URLs
102
- timestamp = Time.current.to_i
103
- url_data = {
104
- url: request_url,
105
- memory_diff: memory_diff,
106
- ram_before: ram_before,
107
- ram_after: ram_after,
108
- timestamp: timestamp,
109
- recorded_at: Time.current.iso8601
110
- }.merge(account_info)
111
-
112
- # Add URL to sorted set (score = memory_diff for DESC ordering)
113
- redis.zadd(redis_high_usage_urls_key, memory_diff, url_data.to_json)
114
-
115
- # Keep only top N URLs by removing lowest scores
116
- 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}"
126
-
127
- # Truncate very long URLs
128
- (url.length > 600) ? "#{url[0..650]}..." : url
129
- end
130
-
131
73
  def extract_account_info(env)
132
74
  account_info = {}
133
75
 
@@ -136,13 +78,13 @@ module MemHealth
136
78
  if defined?(ActsAsTenant) && ActsAsTenant.current_tenant
137
79
  account = ActsAsTenant.current_tenant
138
80
  account_info[:account_id] = account.id
139
- elsif env["warden"]&.user&.respond_to?(:account)
81
+ elsif env['warden']&.user&.respond_to?(:account)
140
82
  # Try to get from authenticated user
141
- account = env["warden"].user.account
83
+ account = env['warden'].user.account
142
84
  account_info[:account_id] = account.id
143
- elsif env["HTTP_X_ACCOUNT_ID"]
85
+ elsif env['HTTP_X_ACCOUNT_ID']
144
86
  # Fallback to header if available
145
- account_info[:account_id] = env["HTTP_X_ACCOUNT_ID"]
87
+ account_info[:account_id] = env['HTTP_X_ACCOUNT_ID']
146
88
  end
147
89
  rescue StandardError => _e
148
90
  # Silently fail if account extraction fails
@@ -1,41 +1,71 @@
1
1
  module MemHealth
2
2
  class Tracker
3
3
  class << self
4
- # Get the maximum memory difference recorded
5
- def max_memory_diff
6
- redis.get(redis_max_diff_key)&.to_f || 0.0
4
+ # Generic methods for web and worker tracking
5
+ def max_memory_diff(type: :web)
6
+ key = type == :worker ? redis_max_diff_worker_key : redis_max_diff_key
7
+ redis.get(key)&.to_f || 0.0
7
8
  end
8
9
 
9
- # Get all stored URLs, ordered by memory usage (highest first)
10
- def top_memory_urls(limit: config.max_stored_urls)
11
- redis.zrevrange(redis_high_usage_urls_key, 0, limit - 1, with_scores: true).map do |json_data, score|
10
+ def top_memory_items(type: :web, limit: config.max_stored_urls)
11
+ key = type == :worker ? redis_high_usage_jobs_key : redis_high_usage_urls_key
12
+ redis.zrevrange(key, 0, limit - 1, with_scores: true).map do |json_data, score|
12
13
  data = JSON.parse(json_data)
13
14
  data.merge("memory_diff" => score)
14
15
  end
15
16
  end
16
17
 
17
- # Alias for backward compatibility
18
- alias_method :high_usage_urls, :top_memory_urls
19
-
20
- # Get URLs that used more than a specific threshold
21
- def urls_above_threshold(threshold_mb)
22
- redis.zrangebyscore(redis_high_usage_urls_key, threshold_mb, "+inf", with_scores: true).map do |json_data, score|
18
+ def items_above_threshold(threshold_mb, type: :web)
19
+ key = type == :worker ? redis_high_usage_jobs_key : redis_high_usage_urls_key
20
+ redis.zrangebyscore(key, threshold_mb, "+inf", with_scores: true).map do |json_data, score|
23
21
  data = JSON.parse(json_data)
24
22
  data.merge("memory_diff" => score)
25
23
  end
26
24
  end
27
25
 
28
- # Get the URL with the highest memory usage (the one that set the max diff)
29
- def max_memory_url
30
- json_data = redis.get(redis_max_diff_url_key)
26
+ def max_memory_item(type: :web)
27
+ key = type == :worker ? redis_max_diff_job_key : redis_max_diff_url_key
28
+ json_data = redis.get(key)
31
29
  return nil if json_data.nil?
32
30
 
33
31
  JSON.parse(json_data)
34
32
  end
35
33
 
34
+ # Convenience methods with backward compatibility
35
+ def max_memory_diff_worker
36
+ max_memory_diff(type: :worker)
37
+ end
38
+
39
+ def top_memory_urls(limit: config.max_stored_urls)
40
+ top_memory_items(type: :web, limit: limit)
41
+ end
42
+
43
+ alias_method :high_usage_urls, :top_memory_urls
44
+
45
+ def top_memory_jobs(limit: config.max_stored_urls)
46
+ top_memory_items(type: :worker, limit: limit)
47
+ end
48
+
49
+ def urls_above_threshold(threshold_mb)
50
+ items_above_threshold(threshold_mb, type: :web)
51
+ end
52
+
53
+ def jobs_above_threshold(threshold_mb)
54
+ items_above_threshold(threshold_mb, type: :worker)
55
+ end
56
+
57
+ def max_memory_url
58
+ max_memory_item(type: :web)
59
+ end
60
+
61
+ def max_memory_job
62
+ max_memory_item(type: :worker)
63
+ end
64
+
36
65
  # Clear all memory tracking data
37
66
  def clear_all_data
38
- redis.del(redis_max_diff_key, redis_max_diff_url_key, redis_high_usage_urls_key)
67
+ redis.del(redis_max_diff_key, redis_max_diff_url_key, redis_high_usage_urls_key,
68
+ redis_max_diff_worker_key, redis_max_diff_job_key, redis_high_usage_jobs_key)
39
69
  MemHealth::Middleware.reset_data
40
70
  end
41
71
 
@@ -58,6 +88,18 @@ module MemHealth
58
88
  }
59
89
  end
60
90
 
91
+ # Get worker stats summary
92
+ def worker_stats
93
+ max_diff = max_memory_diff_worker
94
+ stored_jobs_count = redis.zcard(redis_high_usage_jobs_key)
95
+
96
+ {
97
+ max_memory_diff: max_diff,
98
+ stored_jobs_count: stored_jobs_count,
99
+ max_stored_jobs: config.max_stored_urls
100
+ }
101
+ end
102
+
61
103
  # Pretty print stats for console use
62
104
  def print_stats
63
105
  stats_data = stats
@@ -99,6 +141,18 @@ module MemHealth
99
141
  def redis_high_usage_urls_key
100
142
  "#{config.redis_key_prefix}:high_usage_urls"
101
143
  end
144
+
145
+ def redis_max_diff_worker_key
146
+ "#{config.redis_key_prefix}:worker:max_diff"
147
+ end
148
+
149
+ def redis_max_diff_job_key
150
+ "#{config.redis_key_prefix}:worker:max_diff_job"
151
+ end
152
+
153
+ def redis_high_usage_jobs_key
154
+ "#{config.redis_key_prefix}:worker:high_usage_jobs"
155
+ end
102
156
  end
103
157
  end
104
158
  end
@@ -0,0 +1,77 @@
1
+ require 'get_process_mem'
2
+
3
+ module MemHealth
4
+ module TrackingConcern
5
+ private
6
+
7
+ def config
8
+ MemHealth.configuration
9
+ end
10
+
11
+ def redis
12
+ config.redis
13
+ end
14
+
15
+ def track_memory_usage(memory_diff, ram_before, ram_after, metadata, type: :web)
16
+ prefix = type == :worker ? 'worker:' : ''
17
+
18
+ # Update max memory diff seen so far
19
+ max_diff_key = "#{config.redis_key_prefix}:#{prefix}max_diff"
20
+ max_item_key = "#{config.redis_key_prefix}:#{prefix}max_diff_#{type == :worker ? 'job' : 'url'}"
21
+ high_usage_key = "#{config.redis_key_prefix}:#{prefix}high_usage_#{type == :worker ? 'jobs' : 'urls'}"
22
+
23
+ current_max = redis.get(max_diff_key)&.to_f || 0.0
24
+ if memory_diff > current_max
25
+ redis.set(max_diff_key, memory_diff)
26
+
27
+ # Store the item data that caused the maximum memory usage
28
+ max_item_data = {
29
+ memory_diff: memory_diff,
30
+ ram_before: ram_before,
31
+ ram_after: ram_after,
32
+ timestamp: Time.current.to_i,
33
+ recorded_at: Time.current.iso8601
34
+ }.merge(metadata)
35
+ redis.set(max_item_key, max_item_data.to_json)
36
+ end
37
+
38
+ # Store all items
39
+ item_data = {
40
+ memory_diff: memory_diff,
41
+ ram_before: ram_before,
42
+ ram_after: ram_after,
43
+ timestamp: Time.current.to_i,
44
+ recorded_at: Time.current.iso8601
45
+ }.merge(metadata)
46
+
47
+ # Add item to sorted set (score = memory_diff for DESC ordering)
48
+ redis.zadd(high_usage_key, memory_diff, item_data.to_json)
49
+
50
+ # Keep only top N items by removing lowest scores
51
+ current_count = redis.zcard(high_usage_key)
52
+ return unless current_count > config.max_stored_urls
53
+
54
+ # Remove the lowest scoring items (keep top max_stored_urls)
55
+ redis.zremrangebyrank(high_usage_key, 0, current_count - config.max_stored_urls - 1)
56
+ end
57
+
58
+ def measure_memory
59
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
60
+ before = GetProcessMem.new.mb
61
+
62
+ yield
63
+
64
+ after = GetProcessMem.new.mb
65
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
+ execution_time = (end_time - start_time).round(2)
67
+ memory_diff = (after - before).round(2)
68
+
69
+ {
70
+ before: before.round(2),
71
+ after: after.round(2),
72
+ memory_diff: memory_diff,
73
+ execution_time: execution_time
74
+ }
75
+ end
76
+ end
77
+ end
@@ -1,3 +1,3 @@
1
1
  module MemHealth
2
- VERSION = "0.1.0"
2
+ VERSION = '0.1.2'
3
3
  end
data/lib/memhealth.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "mem_health/version"
2
2
  require "mem_health/configuration"
3
+ require "mem_health/tracking_concern"
3
4
  require "mem_health/middleware"
5
+ require "mem_health/job_tracking_middleware"
4
6
  require "mem_health/tracker"
5
7
  require "mem_health/engine"
6
8
 
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.2
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-10-01 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rails
@@ -77,12 +78,16 @@ files:
77
78
  - README.md
78
79
  - app/controllers/mem_health/dashboard_controller.rb
79
80
  - app/views/layouts/memhealth/application.html.erb
81
+ - app/views/mem_health/dashboard/_web_view.html.erb
82
+ - app/views/mem_health/dashboard/_worker_view.html.erb
80
83
  - app/views/mem_health/dashboard/index.html.erb
81
84
  - config/routes.rb
82
85
  - lib/mem_health/configuration.rb
83
86
  - lib/mem_health/engine.rb
87
+ - lib/mem_health/job_tracking_middleware.rb
84
88
  - lib/mem_health/middleware.rb
85
89
  - lib/mem_health/tracker.rb
90
+ - lib/mem_health/tracking_concern.rb
86
91
  - lib/mem_health/version.rb
87
92
  - lib/memhealth.rb
88
93
  homepage: https://github.com/topkeyhq/memhealth
@@ -91,6 +96,7 @@ licenses:
91
96
  metadata:
92
97
  homepage_uri: https://github.com/topkeyhq/memhealth
93
98
  source_code_uri: https://github.com/topkeyhq/memhealth
99
+ post_install_message:
94
100
  rdoc_options: []
95
101
  require_paths:
96
102
  - lib
@@ -105,7 +111,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
111
  - !ruby/object:Gem::Version
106
112
  version: '0'
107
113
  requirements: []
108
- rubygems_version: 3.6.7
114
+ rubygems_version: 3.5.16
115
+ signing_key:
109
116
  specification_version: 4
110
117
  summary: Rails memory health monitoring and tracking
111
118
  test_files: []