memhealth 0.1.0
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 +7 -0
- data/LICENSE +1 -0
- data/README.md +114 -0
- data/app/controllers/mem_health/dashboard_controller.rb +36 -0
- data/app/views/layouts/memhealth/application.html.erb +213 -0
- data/app/views/mem_health/dashboard/index.html.erb +181 -0
- data/config/routes.rb +4 -0
- data/lib/mem_health/configuration.rb +32 -0
- data/lib/mem_health/engine.rb +40 -0
- data/lib/mem_health/middleware.rb +154 -0
- data/lib/mem_health/tracker.rb +104 -0
- data/lib/mem_health/version.rb +3 -0
- data/lib/memhealth.rb +20 -0
- metadata +111 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8c0895575f5adc6767b228212ace922f4eb66eb565771c05bde602b4c4b48971
|
|
4
|
+
data.tar.gz: b44315dc375c9fa4108a5fb889f76dd1a0520883f9c8952a675c8fd840bc981a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f5e263596880a31d83003ebe56700a6ed94d8bb6eafa60222b7424fbb1f8632b9a534b9952d6c892d41637a43f66df0a4d673d6e7f62c4f35c0279382a8486cc
|
|
7
|
+
data.tar.gz: 80ad19cf0d82d6a4f543701a82736c6837d265b0daa57ef9a709df99c550452b2fb8e77f131add2e86e2871c0978729d06e84035539b42cc1eb5c40ff6e5f2f5
|
data/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT License
|
data/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# MemHealth
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
## 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
|
+
```
|
|
45
|
+
|
|
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` |
|
|
56
|
+
|
|
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.
|
|
58
|
+
|
|
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
|
+
### ActiveAdmin Integration
|
|
82
|
+
|
|
83
|
+
To add Memhealth to your ActiveAdmin Operations menu, add this to your ActiveAdmin initializer:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# config/initializers/active_admin.rb
|
|
87
|
+
ActiveAdmin.setup do |config|
|
|
88
|
+
# ... other config ...
|
|
89
|
+
|
|
90
|
+
config.namespace :admin do |admin|
|
|
91
|
+
admin.build_menu do |menu|
|
|
92
|
+
# ... other menu items ...
|
|
93
|
+
menu.add label: "MemHealth", url: "/admin/memhealth", parent: "Operations"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Console Usage
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# View statistics
|
|
103
|
+
MemHealth::Tracker.print_stats
|
|
104
|
+
|
|
105
|
+
# Get top memory consuming URLs
|
|
106
|
+
MemHealth::Tracker.top_memory_urls
|
|
107
|
+
|
|
108
|
+
# Clear all data
|
|
109
|
+
MemHealth::Tracker.clear_all_data
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
The gem is available as open source under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module MemHealth
|
|
2
|
+
class DashboardController < ActionController::Base
|
|
3
|
+
protect_from_forgery with: :exception
|
|
4
|
+
layout "memhealth/application"
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@memory_hunter_enabled = MemHealth.configuration.enabled?
|
|
8
|
+
|
|
9
|
+
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
|
|
13
|
+
else
|
|
14
|
+
@stats = nil
|
|
15
|
+
@top_urls = []
|
|
16
|
+
@max_memory_url = nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def clear
|
|
21
|
+
before_stats = MemHealth::Tracker.stats
|
|
22
|
+
MemHealth::Tracker.clear_all_data
|
|
23
|
+
|
|
24
|
+
redirect_to root_path, notice: "✅ All memory usage statistics have been reset successfully! Cleared #{before_stats[:stored_urls_count]} URLs and reset max memory diff from #{before_stats[:max_memory_diff]} MB to 0.0 MB."
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
redirect_to root_path, alert: "❌ Error clearing memory statistics: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def authenticate_user!
|
|
32
|
+
# Override this method in your host app if you need authentication
|
|
33
|
+
# For example: authenticate_admin_user! or redirect_to login_path unless current_user&.admin?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Memhealth</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
/* Base styles */
|
|
11
|
+
* {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Arial, sans-serif;
|
|
17
|
+
line-height: 1.6;
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Padding utilities with better spacing */
|
|
23
|
+
.px-4 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
|
24
|
+
.py-6 { padding-top: 2rem; padding-bottom: 2rem; }
|
|
25
|
+
.px-6 { padding-left: 2rem; padding-right: 2rem; }
|
|
26
|
+
.py-4 { padding-top: 1.25rem; padding-bottom: 1.25rem; }
|
|
27
|
+
.px-2 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
|
28
|
+
.py-1 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
|
29
|
+
.px-3 { padding-left: 1rem; padding-right: 1rem; }
|
|
30
|
+
.py-2 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
|
31
|
+
.py-3 { padding-top: 1rem; padding-bottom: 1rem; }
|
|
32
|
+
.pb-6 { padding-bottom: 2rem; }
|
|
33
|
+
.p-6 { padding: 2rem; }
|
|
34
|
+
.p-4 { padding: 1.25rem; }
|
|
35
|
+
.p-3 { padding: 1rem; }
|
|
36
|
+
|
|
37
|
+
.mb-6 { margin-bottom: 2rem; }
|
|
38
|
+
.mb-4 { margin-bottom: 1.5rem; }
|
|
39
|
+
.mb-2 { margin-bottom: 0.75rem; }
|
|
40
|
+
.mb-1 { margin-bottom: 0.5rem; }
|
|
41
|
+
.mt-4 { margin-top: 1.5rem; }
|
|
42
|
+
.mt-3 { margin-top: 1rem; }
|
|
43
|
+
.mt-1 { margin-top: 0.5rem; }
|
|
44
|
+
.mt-6 { margin-top: 2rem; }
|
|
45
|
+
.ml-4 { margin-left: 1.5rem; }
|
|
46
|
+
.ml-0 { margin-left: 0; }
|
|
47
|
+
|
|
48
|
+
.bg-green-50 { background-color: rgb(240 253 244); }
|
|
49
|
+
.bg-red-50 { background-color: rgb(254 242 242); }
|
|
50
|
+
.bg-yellow-50 { background-color: rgb(254 252 232); }
|
|
51
|
+
.bg-white { background-color: rgb(255 255 255); }
|
|
52
|
+
.bg-blue-50 { background-color: rgb(239 246 255); }
|
|
53
|
+
.bg-purple-50 { background-color: rgb(250 245 255); }
|
|
54
|
+
.bg-gray-50 { background-color: rgb(249 250 251); }
|
|
55
|
+
.bg-gray-100 { background-color: rgb(243 244 246); }
|
|
56
|
+
.bg-gray-900 { background-color: rgb(17 24 39); }
|
|
57
|
+
.bg-red-100 { background-color: rgb(254 226 226); }
|
|
58
|
+
.bg-yellow-100 { background-color: rgb(254 249 195); }
|
|
59
|
+
.bg-green-100 { background-color: rgb(220 252 231); }
|
|
60
|
+
|
|
61
|
+
.border { border-width: 1px; }
|
|
62
|
+
.border-2 { border-width: 2px; }
|
|
63
|
+
.border-green-200 { border-color: rgb(187 247 208); }
|
|
64
|
+
.border-red-200 { border-color: rgb(254 202 202); }
|
|
65
|
+
.border-yellow-200 { border-color: rgb(254 240 138); }
|
|
66
|
+
.border-red-100 { border-color: rgb(254 226 226); }
|
|
67
|
+
.border-yellow-100 { border-color: rgb(254 249 195); }
|
|
68
|
+
.border-red-400 { border-color: rgb(248 113 113); }
|
|
69
|
+
.border-b { border-bottom-width: 1px; }
|
|
70
|
+
.border-gray-200 { border-color: rgb(229 231 235); }
|
|
71
|
+
.divide-y > * + * { border-top-width: 1px; }
|
|
72
|
+
.divide-gray-200 > * + * { border-color: rgb(229 231 235); }
|
|
73
|
+
|
|
74
|
+
.rounded-lg { border-radius: 0.75rem; }
|
|
75
|
+
.rounded-md { border-radius: 0.5rem; }
|
|
76
|
+
.rounded { border-radius: 0.375rem; }
|
|
77
|
+
.rounded-full { border-radius: 9999px; }
|
|
78
|
+
|
|
79
|
+
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
|
80
|
+
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
|
81
|
+
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
|
82
|
+
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
|
83
|
+
|
|
84
|
+
.font-semibold { font-weight: 600; }
|
|
85
|
+
.font-medium { font-weight: 500; }
|
|
86
|
+
.font-bold { font-weight: 700; }
|
|
87
|
+
.font-mono { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; }
|
|
88
|
+
|
|
89
|
+
.text-green-800 { color: rgb(22 101 52); }
|
|
90
|
+
.text-green-700 { color: rgb(21 128 61); }
|
|
91
|
+
.text-red-800 { color: rgb(153 27 27); }
|
|
92
|
+
.text-gray-900 { color: rgb(17 24 39); }
|
|
93
|
+
.text-gray-600 { color: rgb(75 85 99); }
|
|
94
|
+
.text-gray-500 { color: rgb(107 114 128); }
|
|
95
|
+
.text-blue-600 { color: rgb(37 99 235); }
|
|
96
|
+
.text-blue-900 { color: rgb(30 58 138); }
|
|
97
|
+
.text-green-600 { color: rgb(22 163 74); }
|
|
98
|
+
.text-green-900 { color: rgb(20 83 45); }
|
|
99
|
+
.text-yellow-600 { color: rgb(202 138 4); }
|
|
100
|
+
.text-yellow-900 { color: rgb(113 63 18); }
|
|
101
|
+
.text-purple-600 { color: rgb(147 51 234); }
|
|
102
|
+
.text-purple-900 { color: rgb(88 28 135); }
|
|
103
|
+
.text-purple-500 { color: rgb(168 85 247); }
|
|
104
|
+
.text-yellow-800 { color: rgb(146 64 14); }
|
|
105
|
+
.text-gray-700 { color: rgb(55 65 81); }
|
|
106
|
+
.text-green-400 { color: rgb(74 222 128); }
|
|
107
|
+
.text-red-900 { color: rgb(127 29 29); }
|
|
108
|
+
.text-red-800 { color: rgb(153 27 27); }
|
|
109
|
+
|
|
110
|
+
.shadow { box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); }
|
|
111
|
+
.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); }
|
|
112
|
+
|
|
113
|
+
.flex { display: flex; }
|
|
114
|
+
.inline-flex { display: inline-flex; }
|
|
115
|
+
.grid { display: grid; }
|
|
116
|
+
.items-start { align-items: flex-start; }
|
|
117
|
+
.items-center { align-items: center; }
|
|
118
|
+
.justify-between { justify-content: space-between; }
|
|
119
|
+
.flex-1 { flex: 1 1 0%; }
|
|
120
|
+
.flex-shrink-0 { flex-shrink: 0; }
|
|
121
|
+
.gap-4 { gap: 1.5rem; }
|
|
122
|
+
.space-y-3 > * + * { margin-top: 1rem; }
|
|
123
|
+
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
124
|
+
|
|
125
|
+
.min-w-0 { min-width: 0px; }
|
|
126
|
+
.min-w-full { min-width: 100%; }
|
|
127
|
+
.max-w-xs { max-width: 20rem; }
|
|
128
|
+
.max-w-md { max-width: 28rem; }
|
|
129
|
+
|
|
130
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
131
|
+
.whitespace-nowrap { white-space: nowrap; }
|
|
132
|
+
.text-center { text-align: center; }
|
|
133
|
+
.text-left { text-align: left; }
|
|
134
|
+
.uppercase { text-transform: uppercase; }
|
|
135
|
+
.tracking-wider { letter-spacing: 0.05em; }
|
|
136
|
+
|
|
137
|
+
.overflow-hidden { overflow: hidden; }
|
|
138
|
+
.cursor-pointer { cursor: pointer; }
|
|
139
|
+
|
|
140
|
+
.hover\\:bg-red-100:hover { background-color: rgb(254 226 226); }
|
|
141
|
+
.focus\\:ring-2:focus { box-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); }
|
|
142
|
+
.focus\\:ring-red-500:focus { --tw-ring-color: rgb(239 68 68); }
|
|
143
|
+
|
|
144
|
+
/* Table styles */
|
|
145
|
+
table { border-collapse: collapse; width: 100%; }
|
|
146
|
+
th, td { text-align: left; }
|
|
147
|
+
th { font-weight: 500; }
|
|
148
|
+
|
|
149
|
+
/* Compact table cell padding */
|
|
150
|
+
table th.px-6 { padding-left: 1rem !important; padding-right: 1rem !important; }
|
|
151
|
+
table td.px-6 { padding-left: 1rem !important; padding-right: 1rem !important; }
|
|
152
|
+
table th.py-3 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; }
|
|
153
|
+
table td.py-4 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; }
|
|
154
|
+
|
|
155
|
+
/* Responsive grid */
|
|
156
|
+
@media (min-width: 768px) {
|
|
157
|
+
.md\\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Container styles */
|
|
161
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
162
|
+
|
|
163
|
+
/* Header styles */
|
|
164
|
+
.header {
|
|
165
|
+
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
166
|
+
color: white;
|
|
167
|
+
padding: 1.5rem 0;
|
|
168
|
+
margin-bottom: 2rem;
|
|
169
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.header h1 {
|
|
173
|
+
font-size: 2.5rem;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
margin: 0;
|
|
176
|
+
letter-spacing: -0.025em;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.header p {
|
|
180
|
+
margin: 1rem 0 0 0;
|
|
181
|
+
opacity: 0.95;
|
|
182
|
+
font-size: 1.125rem;
|
|
183
|
+
font-weight: 400;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
</style>
|
|
187
|
+
</head>
|
|
188
|
+
|
|
189
|
+
<body class="bg-gray-50">
|
|
190
|
+
<div class="header">
|
|
191
|
+
<div class="container px-6">
|
|
192
|
+
<h1>MemHealth</h1>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
<div class="container">
|
|
198
|
+
<% if notice %>
|
|
199
|
+
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
|
200
|
+
<div class="text-sm text-green-800"><%= notice %></div>
|
|
201
|
+
</div>
|
|
202
|
+
<% end %>
|
|
203
|
+
|
|
204
|
+
<% if alert %>
|
|
205
|
+
<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
206
|
+
<div class="text-sm text-red-800"><%= alert %></div>
|
|
207
|
+
</div>
|
|
208
|
+
<% end %>
|
|
209
|
+
|
|
210
|
+
<%= yield %>
|
|
211
|
+
</div>
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<div class="px-6 pb-6">
|
|
2
|
+
<% if @memory_hunter_enabled %>
|
|
3
|
+
<!-- Memory Hunter Enabled Status -->
|
|
4
|
+
<div class="bg-green-50 border border-green-200 rounded-lg px-6 py-4 mb-6">
|
|
5
|
+
<h3 class="text-lg font-semibold text-green-800 mb-2">✅ MemHealth Enabled</h3>
|
|
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
|
+
</div>
|
|
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>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
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>
|
|
148
|
+
<% 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>
|
|
154
|
+
<% end %>
|
|
155
|
+
|
|
156
|
+
<!-- Action buttons -->
|
|
157
|
+
<div class="mt-6 space-y-3">
|
|
158
|
+
<div class="flex gap-4">
|
|
159
|
+
<%= form_with url: clear_path, method: :post, local: true, class: "inline" do |form| %>
|
|
160
|
+
<%= form.button "🗑️ Reset Stats",
|
|
161
|
+
type: "submit",
|
|
162
|
+
onclick: "return confirm('Are you sure you want to reset all memory usage statistics? This will clear all data including max memory difference and URLs.')",
|
|
163
|
+
class: "inline-flex items-center px-6 py-3 border-2 border-red-400 text-sm font-semibold rounded-md text-red-800 bg-red-50 hover:bg-red-100 focus:ring-2 focus:ring-red-500 shadow-sm cursor-pointer" %>
|
|
164
|
+
<% end %>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<% else %>
|
|
168
|
+
<!-- Memory Hunter Disabled Status -->
|
|
169
|
+
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6 mb-6">
|
|
170
|
+
<h3 class="text-lg font-semibold text-yellow-800 mb-4">⚠️ MemHealth Disabled</h3>
|
|
171
|
+
|
|
172
|
+
<div class="bg-white rounded-md p-4 border border-yellow-100">
|
|
173
|
+
<p class="text-sm text-gray-700 mb-3">Memory profiling is currently disabled. To enable memory growth tracking:</p>
|
|
174
|
+
|
|
175
|
+
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm mb-3">
|
|
176
|
+
Set ENV['MEM_HEALTH_ENABLED'] = true
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<% end %>
|
|
181
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "redis"
|
|
2
|
+
|
|
3
|
+
module MemHealth
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :enabled,
|
|
6
|
+
:skip_requests,
|
|
7
|
+
:memory_threshold_mb,
|
|
8
|
+
:ram_before_threshold_mb,
|
|
9
|
+
:max_stored_urls,
|
|
10
|
+
:redis_url,
|
|
11
|
+
:redis_key_prefix
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@enabled = ENV.fetch("MEM_HEALTH_ENABLED", "false") == "true"
|
|
15
|
+
@skip_requests = ENV.fetch("MEM_HEALTH_SKIP_REQUESTS", "10").to_i
|
|
16
|
+
@memory_threshold_mb = ENV.fetch("MEM_HEALTH_MEMORY_THRESHOLD_MB", "1").to_i
|
|
17
|
+
@ram_before_threshold_mb = ENV.fetch("MEM_HEALTH_RAM_BEFORE_THRESHOLD", "0").to_i
|
|
18
|
+
@max_stored_urls = ENV.fetch("MEM_HEALTH_MAX_STORED_URLS", "20").to_i
|
|
19
|
+
redis_env_key = ENV.fetch("MEM_HEALTH_REDIS_KEY", "REDIS_URL")
|
|
20
|
+
@redis_url = ENV.fetch(redis_env_key, "redis://localhost:6379/0")
|
|
21
|
+
@redis_key_prefix = "memhealth"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enabled?
|
|
25
|
+
@enabled
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def redis
|
|
29
|
+
@redis ||= Redis.new(url: redis_url)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module MemHealth
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace MemHealth
|
|
4
|
+
|
|
5
|
+
# Set specific autoload paths and exclude others
|
|
6
|
+
config.autoload_paths += %W(#{root}/app/controllers #{root}/app/views)
|
|
7
|
+
config.eager_load_paths += %W(#{root}/app/controllers #{root}/app/views)
|
|
8
|
+
|
|
9
|
+
# Configure Zeitwerk to ignore config and lib directories
|
|
10
|
+
initializer "mem_health.zeitwerk_ignore", before: :set_autoload_paths do
|
|
11
|
+
config.autoload_paths.delete("#{root}/config")
|
|
12
|
+
config.autoload_paths.delete("#{root}/lib")
|
|
13
|
+
|
|
14
|
+
if defined?(Rails.autoloaders)
|
|
15
|
+
Rails.autoloaders.main.ignore("#{root}/config")
|
|
16
|
+
Rails.autoloaders.main.ignore("#{root}/lib")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
config.generators do |g|
|
|
21
|
+
g.test_framework :rspec
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
initializer "mem_health.load_controllers", after: :load_config_initializers do
|
|
25
|
+
# Manually require the controller to ensure it loads
|
|
26
|
+
begin
|
|
27
|
+
require File.join(root, "app", "controllers", "mem_health", "dashboard_controller")
|
|
28
|
+
rescue => e
|
|
29
|
+
Rails.logger.warn "Could not load MemHealth controller: #{e.message}" if defined?(Rails.logger)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
initializer "mem_health.middleware", before: :build_middleware_stack do |app|
|
|
34
|
+
if MemHealth.configuration.enabled?
|
|
35
|
+
require "get_process_mem"
|
|
36
|
+
app.config.middleware.use MemHealth::Middleware
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require "get_process_mem"
|
|
2
|
+
|
|
3
|
+
module MemHealth
|
|
4
|
+
class Middleware
|
|
5
|
+
@@request_count = 0
|
|
6
|
+
@@skipped_requests_count = 0
|
|
7
|
+
|
|
8
|
+
def self.redis_tracked_requests_key
|
|
9
|
+
"#{MemHealth.configuration.redis_key_prefix}:tracked_requests"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(app)
|
|
13
|
+
@app = app
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
@@request_count += 1
|
|
18
|
+
|
|
19
|
+
before = GetProcessMem.new.mb
|
|
20
|
+
status, headers, response = @app.call(env)
|
|
21
|
+
after = GetProcessMem.new.mb
|
|
22
|
+
|
|
23
|
+
memory_diff = (after - before).round(2)
|
|
24
|
+
request_url = build_request_url(env)
|
|
25
|
+
account_info = extract_account_info(env)
|
|
26
|
+
|
|
27
|
+
# Skip the first few requests as they have large memory jumps due to class loading
|
|
28
|
+
if @@request_count > config.skip_requests
|
|
29
|
+
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
|
|
32
|
+
else
|
|
33
|
+
@@skipped_requests_count += 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
[status, headers, response]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.reset_data
|
|
40
|
+
@@request_count = 0
|
|
41
|
+
@@skipped_requests_count = 0
|
|
42
|
+
MemHealth.configuration.redis.del(redis_tracked_requests_key)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.tracked_requests_count
|
|
46
|
+
MemHealth.configuration.redis.get(redis_tracked_requests_key)&.to_i || 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.total_requests_count
|
|
50
|
+
@@request_count
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.skipped_requests_count
|
|
54
|
+
@@skipped_requests_count
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
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
|
+
def redis_tracked_requests_key
|
|
80
|
+
self.class.redis_tracked_requests_key
|
|
81
|
+
end
|
|
82
|
+
|
|
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
|
+
def extract_account_info(env)
|
|
132
|
+
account_info = {}
|
|
133
|
+
|
|
134
|
+
begin
|
|
135
|
+
# Try to get account info from various sources
|
|
136
|
+
if defined?(ActsAsTenant) && ActsAsTenant.current_tenant
|
|
137
|
+
account = ActsAsTenant.current_tenant
|
|
138
|
+
account_info[:account_id] = account.id
|
|
139
|
+
elsif env["warden"]&.user&.respond_to?(:account)
|
|
140
|
+
# Try to get from authenticated user
|
|
141
|
+
account = env["warden"].user.account
|
|
142
|
+
account_info[:account_id] = account.id
|
|
143
|
+
elsif env["HTTP_X_ACCOUNT_ID"]
|
|
144
|
+
# Fallback to header if available
|
|
145
|
+
account_info[:account_id] = env["HTTP_X_ACCOUNT_ID"]
|
|
146
|
+
end
|
|
147
|
+
rescue StandardError => _e
|
|
148
|
+
# Silently fail if account extraction fails
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
account_info
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module MemHealth
|
|
2
|
+
class Tracker
|
|
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
|
|
7
|
+
end
|
|
8
|
+
|
|
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|
|
|
12
|
+
data = JSON.parse(json_data)
|
|
13
|
+
data.merge("memory_diff" => score)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
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|
|
|
23
|
+
data = JSON.parse(json_data)
|
|
24
|
+
data.merge("memory_diff" => score)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
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)
|
|
31
|
+
return nil if json_data.nil?
|
|
32
|
+
|
|
33
|
+
JSON.parse(json_data)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Clear all memory tracking data
|
|
37
|
+
def clear_all_data
|
|
38
|
+
redis.del(redis_max_diff_key, redis_max_diff_url_key, redis_high_usage_urls_key)
|
|
39
|
+
MemHealth::Middleware.reset_data
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get stats summary
|
|
43
|
+
def stats
|
|
44
|
+
max_diff = max_memory_diff
|
|
45
|
+
stored_urls_count = redis.zcard(redis_high_usage_urls_key)
|
|
46
|
+
tracked_count = MemHealth::Middleware.tracked_requests_count
|
|
47
|
+
total_count = MemHealth::Middleware.total_requests_count
|
|
48
|
+
skipped_count = MemHealth::Middleware.skipped_requests_count
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
max_memory_diff: max_diff,
|
|
52
|
+
stored_urls_count: stored_urls_count,
|
|
53
|
+
max_stored_urls: config.max_stored_urls,
|
|
54
|
+
tracked_requests_count: tracked_count,
|
|
55
|
+
total_requests_count: total_count,
|
|
56
|
+
skipped_requests_count: skipped_count,
|
|
57
|
+
requests_tracked: "#{tracked_count} requests tracked (#{skipped_count} skipped) out of #{total_count} total requests"
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Pretty print stats for console use
|
|
62
|
+
def print_stats
|
|
63
|
+
stats_data = stats
|
|
64
|
+
puts "\n=== Memory Usage Stats ==="
|
|
65
|
+
puts "Max memory difference: #{stats_data[:max_memory_diff]} MB"
|
|
66
|
+
puts "Stored URLs: #{stats_data[:stored_urls_count]}/#{stats_data[:max_stored_urls]}"
|
|
67
|
+
puts "Tracking: #{stats_data[:requests_tracked]}"
|
|
68
|
+
|
|
69
|
+
if stats_data[:stored_urls_count] > 0
|
|
70
|
+
puts "\nTop 10 memory usage URLs:"
|
|
71
|
+
top_memory_urls(limit: 10).each_with_index do |url_data, index|
|
|
72
|
+
if url_data["ram_before"] && url_data["ram_after"]
|
|
73
|
+
puts "#{index + 1}. #{url_data["memory_diff"]} MB (#{url_data["ram_before"]} → #{url_data["ram_after"]} MB) - #{url_data["url"]} (#{url_data["recorded_at"]})"
|
|
74
|
+
else
|
|
75
|
+
puts "#{index + 1}. #{url_data["memory_diff"]} MB - #{url_data["url"]} (#{url_data["recorded_at"]})"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def config
|
|
84
|
+
MemHealth.configuration
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def redis
|
|
88
|
+
config.redis
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def redis_max_diff_key
|
|
92
|
+
"#{config.redis_key_prefix}:max_diff"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def redis_max_diff_url_key
|
|
96
|
+
"#{config.redis_key_prefix}:max_diff_url"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def redis_high_usage_urls_key
|
|
100
|
+
"#{config.redis_key_prefix}:high_usage_urls"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/memhealth.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "mem_health/version"
|
|
2
|
+
require "mem_health/configuration"
|
|
3
|
+
require "mem_health/middleware"
|
|
4
|
+
require "mem_health/tracker"
|
|
5
|
+
require "mem_health/engine"
|
|
6
|
+
|
|
7
|
+
module MemHealth
|
|
8
|
+
class << self
|
|
9
|
+
attr_writer :configuration
|
|
10
|
+
|
|
11
|
+
def configuration
|
|
12
|
+
@configuration ||= Configuration.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configure
|
|
16
|
+
yield(configuration)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
metadata
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: memhealth
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Klemen Nagode
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: redis
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: get_process_mem
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec-rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
description: A Rails engine for monitoring memory health and detecting growth patterns
|
|
69
|
+
in production applications
|
|
70
|
+
email:
|
|
71
|
+
- klemen@topkey.io
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- LICENSE
|
|
77
|
+
- README.md
|
|
78
|
+
- app/controllers/mem_health/dashboard_controller.rb
|
|
79
|
+
- app/views/layouts/memhealth/application.html.erb
|
|
80
|
+
- app/views/mem_health/dashboard/index.html.erb
|
|
81
|
+
- config/routes.rb
|
|
82
|
+
- lib/mem_health/configuration.rb
|
|
83
|
+
- lib/mem_health/engine.rb
|
|
84
|
+
- lib/mem_health/middleware.rb
|
|
85
|
+
- lib/mem_health/tracker.rb
|
|
86
|
+
- lib/mem_health/version.rb
|
|
87
|
+
- lib/memhealth.rb
|
|
88
|
+
homepage: https://github.com/topkeyhq/memhealth
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
homepage_uri: https://github.com/topkeyhq/memhealth
|
|
93
|
+
source_code_uri: https://github.com/topkeyhq/memhealth
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 3.6.7
|
|
109
|
+
specification_version: 4
|
|
110
|
+
summary: Rails memory health monitoring and tracking
|
|
111
|
+
test_files: []
|