rails_vitals 0.2.0 → 0.3.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 +4 -4
- data/README.md +6 -0
- data/app/assets/javascripts/rails_vitals/application.js +161 -0
- data/app/assets/stylesheets/rails_vitals/application.css +175 -0
- data/app/controllers/rails_vitals/explains_controller.rb +16 -0
- data/app/controllers/rails_vitals/n_plus_ones_controller.rb +0 -1
- data/app/controllers/rails_vitals/requests_controller.rb +1 -1
- data/app/helpers/rails_vitals/application_helper.rb +28 -0
- data/app/views/layouts/rails_vitals/application.html.erb +1 -0
- data/app/views/rails_vitals/associations/index.html.erb +41 -189
- data/app/views/rails_vitals/dashboard/index.html.erb +6 -6
- data/app/views/rails_vitals/explains/_plan_node.html.erb +137 -0
- data/app/views/rails_vitals/explains/show.html.erb +186 -0
- data/app/views/rails_vitals/heatmap/index.html.erb +7 -7
- data/app/views/rails_vitals/models/index.html.erb +19 -36
- data/app/views/rails_vitals/n_plus_ones/index.html.erb +9 -9
- data/app/views/rails_vitals/n_plus_ones/show.html.erb +30 -76
- data/app/views/rails_vitals/requests/index.html.erb +5 -5
- data/app/views/rails_vitals/requests/show.html.erb +82 -165
- data/config/routes.rb +1 -0
- data/lib/rails_vitals/analyzers/explain_analyzer.rb +347 -0
- data/lib/rails_vitals/collector.rb +2 -1
- data/lib/rails_vitals/notifications/subscriber.rb +2 -1
- data/lib/rails_vitals/version.rb +1 -1
- data/lib/rails_vitals.rb +1 -0
- metadata +8 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85f8d6772f3f10badb2a598143172c1e11375eb017affe72007fe893e2f6d241
|
|
4
|
+
data.tar.gz: 072320c0d3826677abacc3d36cbfd22843be687a955e41c9f924cb07bba640f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6648831e36ed7b41615af2636939d1f1dace58b270a311486820ee5d9e48581a5d617d590622855dbf6430dfdbc3ed1716721bb6d03f58e1af8dda8a2fcd969
|
|
7
|
+
data.tar.gz: 204fab1e081f960f358295b56f42666800867133c14eafdcc6074524694110a5de0cef994e65647f504ffea3d90aab42620b861ce1c00e3de31b32e929c40278
|
data/README.md
CHANGED
|
@@ -56,6 +56,11 @@ Each N+1 pattern has a detail page showing affected requests, estimated query sa
|
|
|
56
56
|
|
|
57
57
|

|
|
58
58
|
|
|
59
|
+
### 🔬 EXPLAIN Visualizer
|
|
60
|
+
Any `SELECT` query in Request Detail can be sent directly to PostgreSQL's `EXPLAIN ANALYZE`. The result is rendered as an interactive tree, each node shows its operation type, estimated cost, actual time, row estimates vs. reality, and loop count. Nodes are color-coded by cost. Click any node to expand an education card explaining what the operation does and when it becomes a problem.
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
59
64
|
### 🎭 Callback Map
|
|
60
65
|
Every ActiveRecord callback (`before_save`, `after_create`, `before_validation`, etc.) is timed and grouped by model in the Request Detail view. Expensive callbacks surface immediately — including hidden side effects like callbacks that trigger additional queries.
|
|
61
66
|
|
|
@@ -143,6 +148,7 @@ Navigate to `/rails_vitals` to access the full admin interface.
|
|
|
143
148
|
| Models | `/rails_vitals/models` | Per-model query breakdown |
|
|
144
149
|
| N+1 Patterns | `/rails_vitals/n_plus_ones` | Cross-request N+1 aggregation with fix suggestions |
|
|
145
150
|
| Association Map | `/rails_vitals/associations` | Live SVG model graph with N+1 and index annotations |
|
|
151
|
+
| EXPLAIN Visualizer | `/rails_vitals/requests/:request_id/explain/:query_index` | Interactive PostgreSQL EXPLAIN ANALYZE tree with warnings and fix suggestions |
|
|
146
152
|
|
|
147
153
|
---
|
|
148
154
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// ─── Palette ────────────────────────────────────────────────────────────────
|
|
2
|
+
var COLOR_N1 = '#fc8181'; // red — N+1 warnings
|
|
3
|
+
var COLOR_HEALTHY = '#68d391'; // green — healthy / no issues
|
|
4
|
+
var COLOR_BELONGS = '#9f7aea'; // purple — belongs_to macro
|
|
5
|
+
var COLOR_HAS_MANY = '#f6ad55'; // orange — has_many / has_one macro
|
|
6
|
+
var COLOR_MUTED = '#718096'; // grey — secondary text
|
|
7
|
+
var COLOR_TEXT = '#e2e8f0'; // white-ish — primary text
|
|
8
|
+
var COLOR_SURFACE = '#1a202c'; // dark — card surface
|
|
9
|
+
var COLOR_N1_BG = '#2d1515'; // dark red — N+1 card background
|
|
10
|
+
|
|
11
|
+
// ─── Panel element IDs (shared by selectNode / closePanel) ──────────────────
|
|
12
|
+
var ID_PANEL = 'assoc-panel';
|
|
13
|
+
var ID_PANEL_INNER = 'assoc-panel-inner';
|
|
14
|
+
var ID_MODEL_NAME = 'panel-model-name';
|
|
15
|
+
var ID_QUERY_COUNT = 'panel-query-count';
|
|
16
|
+
var ID_AVG_TIME = 'panel-avg-time';
|
|
17
|
+
var ID_N1_COUNT = 'panel-n1-count';
|
|
18
|
+
var ID_ASSOCIATIONS = 'panel-associations';
|
|
19
|
+
var ID_N1_SECTION = 'panel-n1-section';
|
|
20
|
+
var ID_N1_LIST = 'panel-n1-list';
|
|
21
|
+
var ID_LINKS = 'panel-links';
|
|
22
|
+
|
|
23
|
+
// ─── requests/show ──────────────────────────────────────────────────────────
|
|
24
|
+
function toggleDna(id) {
|
|
25
|
+
var row = document.getElementById(id);
|
|
26
|
+
if (row) {
|
|
27
|
+
var isHidden = window.getComputedStyle(row).display === 'none';
|
|
28
|
+
row.classList.remove('d-none');
|
|
29
|
+
row.style.display = isHidden ? 'table-row' : 'none';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toggleCard(id, chevronId) {
|
|
34
|
+
var card = document.getElementById(id);
|
|
35
|
+
if (card) {
|
|
36
|
+
var isHidden = window.getComputedStyle(card).display === 'none';
|
|
37
|
+
card.classList.remove('d-none');
|
|
38
|
+
card.style.display = isHidden ? (card.tagName === 'TABLE' ? 'table' : 'block') : 'none';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (chevronId) {
|
|
42
|
+
var chevron = document.getElementById(chevronId);
|
|
43
|
+
if (chevron) {
|
|
44
|
+
chevron.textContent = chevron.textContent === '▼' ? '▶' : '▼';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── explains/show ──────────────────────────────────────────────────────────
|
|
50
|
+
function toggleExplanation(id) {
|
|
51
|
+
var el = document.getElementById(id);
|
|
52
|
+
if (el) {
|
|
53
|
+
var isHidden = window.getComputedStyle(el).display === 'none';
|
|
54
|
+
el.classList.remove('d-none');
|
|
55
|
+
el.style.display = isHidden ? 'block' : 'none';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── associations/index ─────────────────────────────────────────────────────
|
|
60
|
+
// NODE_DATA, N1_PATH, and REQUEST_PATH are injected inline by the view as a data bridge.
|
|
61
|
+
|
|
62
|
+
function selectNode(nameJson) {
|
|
63
|
+
var name = JSON.parse(nameJson);
|
|
64
|
+
var node = NODE_DATA[name];
|
|
65
|
+
if (!node) return;
|
|
66
|
+
|
|
67
|
+
// Highlight selected node
|
|
68
|
+
document.querySelectorAll('[id^="node-"]').forEach(function(el) {
|
|
69
|
+
el.style.opacity = '0.4';
|
|
70
|
+
});
|
|
71
|
+
var el = document.getElementById('node-' + name);
|
|
72
|
+
if (el) el.style.opacity = '1';
|
|
73
|
+
|
|
74
|
+
// Populate panel header
|
|
75
|
+
document.getElementById(ID_MODEL_NAME).textContent = node.name;
|
|
76
|
+
document.getElementById(ID_QUERY_COUNT).textContent = node.query_count;
|
|
77
|
+
document.getElementById(ID_AVG_TIME).textContent = node.avg_query_time_ms;
|
|
78
|
+
|
|
79
|
+
var n1Count = node.n1_patterns.length;
|
|
80
|
+
var n1El = document.getElementById(ID_N1_COUNT);
|
|
81
|
+
n1El.textContent = n1Count;
|
|
82
|
+
n1El.style.color = n1Count > 0 ? COLOR_N1 : COLOR_HEALTHY;
|
|
83
|
+
|
|
84
|
+
// Associations list
|
|
85
|
+
var assocHtml = '';
|
|
86
|
+
node.associations.forEach(function(a) {
|
|
87
|
+
var macroColor = a.macro === 'belongs_to' ? COLOR_BELONGS : COLOR_HAS_MANY;
|
|
88
|
+
var n1Badge = a.has_n1
|
|
89
|
+
? badge(COLOR_N1, 'N+1')
|
|
90
|
+
: '';
|
|
91
|
+
var indexBadge = a.indexed
|
|
92
|
+
? badge(COLOR_HEALTHY, 'indexed')
|
|
93
|
+
: badge(COLOR_HAS_MANY, '⚠ no index');
|
|
94
|
+
|
|
95
|
+
assocHtml +=
|
|
96
|
+
'<div style="padding:8px;background:' + COLOR_SURFACE + ';border-radius:4px;margin-bottom:6px;font-size:12px;">' +
|
|
97
|
+
'<span style="color:' + macroColor + ';font-family:monospace;">' + a.macro + '</span>' +
|
|
98
|
+
' <span style="color:' + COLOR_TEXT + ';font-family:monospace;">:' + a.to_model.toLowerCase() + '</span>' +
|
|
99
|
+
n1Badge +
|
|
100
|
+
'<div style="color:' + COLOR_MUTED + ';font-size:10px;margin-top:4px;font-family:monospace;">' +
|
|
101
|
+
'fk: ' + a.foreign_key + indexBadge +
|
|
102
|
+
'</div>' +
|
|
103
|
+
'</div>';
|
|
104
|
+
});
|
|
105
|
+
document.getElementById(ID_ASSOCIATIONS).innerHTML =
|
|
106
|
+
assocHtml || '<div style="color:' + COLOR_MUTED + ';font-size:12px;">No associations</div>';
|
|
107
|
+
|
|
108
|
+
// N+1 section
|
|
109
|
+
var n1Section = document.getElementById(ID_N1_SECTION);
|
|
110
|
+
if (n1Count > 0) {
|
|
111
|
+
n1Section.style.display = 'block';
|
|
112
|
+
var n1Html = '';
|
|
113
|
+
node.n1_patterns.forEach(function(p) {
|
|
114
|
+
n1Html +=
|
|
115
|
+
'<div style="padding:8px;background:' + COLOR_N1_BG + ';border:1px solid ' + COLOR_N1 + '44;' +
|
|
116
|
+
'border-radius:4px;margin-bottom:6px;font-size:11px;">' +
|
|
117
|
+
'<div style="color:' + COLOR_N1 + ';font-family:monospace;margin-bottom:4px;">' +
|
|
118
|
+
p.occurrences + 'x detected' +
|
|
119
|
+
'</div>' +
|
|
120
|
+
(p.fix_suggestion
|
|
121
|
+
? '<div style="color:' + COLOR_HEALTHY + ';font-family:monospace;">Fix: ' + p.fix_suggestion + '</div>'
|
|
122
|
+
: '') +
|
|
123
|
+
'</div>';
|
|
124
|
+
});
|
|
125
|
+
document.getElementById(ID_N1_LIST).innerHTML = n1Html;
|
|
126
|
+
} else {
|
|
127
|
+
n1Section.style.display = 'none';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Action links
|
|
131
|
+
var linksHtml = '';
|
|
132
|
+
if (n1Count > 0) {
|
|
133
|
+
linksHtml +=
|
|
134
|
+
'<a href="' + N1_PATH + '" ' +
|
|
135
|
+
'style="display:block;background:' + COLOR_N1_BG + ';border:1px solid ' + COLOR_N1 + '66;' +
|
|
136
|
+
'color:' + COLOR_N1 + ';padding:8px 12px;border-radius:4px;font-size:12px;' +
|
|
137
|
+
'text-decoration:none;text-align:center;margin-top:8px;">' +
|
|
138
|
+
'View N+1 patterns →' +
|
|
139
|
+
'</a>';
|
|
140
|
+
}
|
|
141
|
+
document.getElementById(ID_LINKS).innerHTML = linksHtml;
|
|
142
|
+
|
|
143
|
+
// Open panel
|
|
144
|
+
document.getElementById(ID_PANEL).style.width = '320px';
|
|
145
|
+
document.getElementById(ID_PANEL_INNER).style.display = 'block';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function closePanel() {
|
|
149
|
+
document.getElementById(ID_PANEL).style.width = '0';
|
|
150
|
+
document.getElementById(ID_PANEL_INNER).style.display = 'none';
|
|
151
|
+
document.querySelectorAll('[id^="node-"]').forEach(function(el) {
|
|
152
|
+
el.style.opacity = '1';
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
157
|
+
function badge(color, text) {
|
|
158
|
+
return '<span style="background:' + color + '33;color:' + color + ';' +
|
|
159
|
+
'font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;">' +
|
|
160
|
+
text + '</span>';
|
|
161
|
+
}
|
|
@@ -178,3 +178,178 @@ tr:hover td { background: #1e2535; }
|
|
|
178
178
|
border-radius: 3px;
|
|
179
179
|
font-size: 10px;
|
|
180
180
|
}
|
|
181
|
+
|
|
182
|
+
/* ─── Spacing — margin ─────────────────────────────────────────────────────── */
|
|
183
|
+
.mb-0 { margin-bottom: 0; }
|
|
184
|
+
.mb-4 { margin-bottom: 4px; }
|
|
185
|
+
.mb-6 { margin-bottom: 6px; }
|
|
186
|
+
.mb-8 { margin-bottom: 8px; }
|
|
187
|
+
.mb-12 { margin-bottom: 12px; }
|
|
188
|
+
.mb-16 { margin-bottom: 16px; }
|
|
189
|
+
.mb-20 { margin-bottom: 20px; }
|
|
190
|
+
.mb-24 { margin-bottom: 24px; }
|
|
191
|
+
.mt-2 { margin-top: 2px; }
|
|
192
|
+
.mt-4 { margin-top: 4px; }
|
|
193
|
+
.mt-6 { margin-top: 6px; }
|
|
194
|
+
.ml-6 { margin-left: 6px; }
|
|
195
|
+
.ml-8 { margin-left: 8px; }
|
|
196
|
+
|
|
197
|
+
/* ─── Font size ────────────────────────────────────────────────────────────── */
|
|
198
|
+
.text-10 { font-size: 10px; }
|
|
199
|
+
.text-12 { font-size: 12px; }
|
|
200
|
+
.text-16 { font-size: 16px; }
|
|
201
|
+
.text-24 { font-size: 24px; }
|
|
202
|
+
|
|
203
|
+
/* ─── Misc ─────────────────────────────────────────────────────────────────── */
|
|
204
|
+
.text-right { text-align: right; }
|
|
205
|
+
.line-relaxed { line-height: 1.6; }
|
|
206
|
+
.word-break { white-space: pre-wrap; word-break: break-all; }
|
|
207
|
+
|
|
208
|
+
/* ─── Structural components ────────────────────────────────────────────────── */
|
|
209
|
+
.section-divider {
|
|
210
|
+
padding-bottom: 12px;
|
|
211
|
+
border-bottom: 1px solid #2d3748;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.info-block {
|
|
215
|
+
background: #1a202c;
|
|
216
|
+
border-radius: 6px;
|
|
217
|
+
padding: 16px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.stat-card-dark {
|
|
221
|
+
background: #2d3748;
|
|
222
|
+
border-radius: 6px;
|
|
223
|
+
padding: 16px;
|
|
224
|
+
text-align: center;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.dna-panel {
|
|
228
|
+
background: #1a202c;
|
|
229
|
+
border-left: 3px solid #4299e1;
|
|
230
|
+
padding: 16px;
|
|
231
|
+
margin: 4px 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.explain-btn {
|
|
235
|
+
font-size: 10px;
|
|
236
|
+
background: #1a2d3a;
|
|
237
|
+
border: 1px solid #4299e144;
|
|
238
|
+
padding: 2px 8px;
|
|
239
|
+
border-radius: 3px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ─── Color utilities ──────────────────────────────────────────────────────── */
|
|
243
|
+
.text-muted { color: #a0aec0; }
|
|
244
|
+
.text-primary { color: #e2e8f0; }
|
|
245
|
+
.text-grey { color: #718096; }
|
|
246
|
+
.text-healthy { color: #68d391; }
|
|
247
|
+
.text-danger { color: #fc8181; }
|
|
248
|
+
.text-accent { color: #90cdf4; }
|
|
249
|
+
.text-blue { color: #4299e1; }
|
|
250
|
+
.text-orange { color: #f6ad55; }
|
|
251
|
+
.text-purple { color: #9f7aea; }
|
|
252
|
+
|
|
253
|
+
/* ─── Typography utilities ─────────────────────────────────────────────────── */
|
|
254
|
+
.mono { font-family: ui-monospace, monospace; }
|
|
255
|
+
.bold { font-weight: bold; }
|
|
256
|
+
.text-upper { text-transform: uppercase; letter-spacing: 0.05em; }
|
|
257
|
+
.text-nowrap { white-space: nowrap; }
|
|
258
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
259
|
+
.text-sm { font-size: 11px; }
|
|
260
|
+
.text-center { text-align: center; }
|
|
261
|
+
|
|
262
|
+
/* ─── Layout utilities ─────────────────────────────────────────────────────── */
|
|
263
|
+
.flex { display: flex; }
|
|
264
|
+
.flex-center { display: flex; align-items: center; }
|
|
265
|
+
.flex-between { display: flex; justify-content: space-between; align-items: center; }
|
|
266
|
+
.flex-wrap { flex-wrap: wrap; }
|
|
267
|
+
.flex-1 { flex: 1; }
|
|
268
|
+
.d-none { display: none; }
|
|
269
|
+
.cursor-pointer { cursor: pointer; }
|
|
270
|
+
|
|
271
|
+
/* ─── Page structure ───────────────────────────────────────────────────────── */
|
|
272
|
+
.page-header { margin-bottom: 24px; }
|
|
273
|
+
.page-heading { font-size: 20px; font-weight: bold; color: #e2e8f0; }
|
|
274
|
+
.page-subtitle { color: #a0aec0; font-size: 13px; margin-top: 4px; }
|
|
275
|
+
.back-link { color: #a0aec0; font-size: 13px; }
|
|
276
|
+
|
|
277
|
+
/* ─── Component utilities ──────────────────────────────────────────────────── */
|
|
278
|
+
.section-label {
|
|
279
|
+
color: #a0aec0;
|
|
280
|
+
font-size: 11px;
|
|
281
|
+
font-weight: bold;
|
|
282
|
+
text-transform: uppercase;
|
|
283
|
+
letter-spacing: 0.05em;
|
|
284
|
+
margin-bottom: 8px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.mini-label {
|
|
288
|
+
color: #718096;
|
|
289
|
+
font-size: 10px;
|
|
290
|
+
text-transform: uppercase;
|
|
291
|
+
letter-spacing: 0.05em;
|
|
292
|
+
margin-bottom: 4px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.empty-state { color: #a0aec0; text-align: center; padding: 48px; }
|
|
296
|
+
|
|
297
|
+
.code-block {
|
|
298
|
+
background: #1a202c;
|
|
299
|
+
padding: 12px;
|
|
300
|
+
border-radius: 6px;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
font-family: ui-monospace, monospace;
|
|
303
|
+
white-space: pre-wrap;
|
|
304
|
+
word-break: break-all;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.code-block-dark {
|
|
308
|
+
background: #2d3748;
|
|
309
|
+
border-radius: 4px;
|
|
310
|
+
padding: 8px 12px;
|
|
311
|
+
font-family: ui-monospace, monospace;
|
|
312
|
+
font-size: 12px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.stat-value-lg {
|
|
316
|
+
font-size: 20px;
|
|
317
|
+
font-weight: bold;
|
|
318
|
+
font-family: ui-monospace, monospace;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.stat-value-xl {
|
|
322
|
+
font-size: 28px;
|
|
323
|
+
font-weight: bold;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.stat-box {
|
|
327
|
+
flex: 1;
|
|
328
|
+
background: #1a202c;
|
|
329
|
+
border-radius: 4px;
|
|
330
|
+
padding: 10px;
|
|
331
|
+
text-align: center;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.impact-box {
|
|
335
|
+
background: #1a202c;
|
|
336
|
+
border-radius: 8px;
|
|
337
|
+
padding: 16px;
|
|
338
|
+
text-align: center;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.callback-badge {
|
|
342
|
+
color: #fff;
|
|
343
|
+
padding: 1px 6px;
|
|
344
|
+
border-radius: 3px;
|
|
345
|
+
font-size: 11px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.legend-dot {
|
|
349
|
+
display: inline-block;
|
|
350
|
+
width: 12px;
|
|
351
|
+
height: 12px;
|
|
352
|
+
border-radius: 2px;
|
|
353
|
+
margin-right: 4px;
|
|
354
|
+
vertical-align: middle;
|
|
355
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module RailsVitals
|
|
2
|
+
class ExplainsController < ApplicationController
|
|
3
|
+
def show
|
|
4
|
+
@record = RailsVitals.store.find(params[:request_id])
|
|
5
|
+
return render plain: "Request not found", status: :not_found unless @record
|
|
6
|
+
|
|
7
|
+
@query_index = params[:query_index].to_i
|
|
8
|
+
query = @record.queries[@query_index]
|
|
9
|
+
return render plain: "Query not found", status: :not_found unless query
|
|
10
|
+
|
|
11
|
+
@sql = query[:sql]
|
|
12
|
+
@binds = query[:binds] || []
|
|
13
|
+
@result = Analyzers::ExplainAnalyzer.analyze(@sql, binds: @binds)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -7,7 +7,7 @@ module RailsVitals
|
|
|
7
7
|
|
|
8
8
|
def show
|
|
9
9
|
@record = RailsVitals.store.find(params[:id])
|
|
10
|
-
render plain: "Request not found", status: :not_found unless @record
|
|
10
|
+
return render plain: "Request not found", status: :not_found unless @record
|
|
11
11
|
|
|
12
12
|
@query_dna = @record.queries.map do |q|
|
|
13
13
|
{
|
|
@@ -6,6 +6,7 @@ module RailsVitals
|
|
|
6
6
|
COLOR_RED = "#c53030"
|
|
7
7
|
COLOR_DARK_RED = "#742a2a"
|
|
8
8
|
COLOR_GRAY = "#4a5568"
|
|
9
|
+
COLOR_NEUTRAL = "#a0aec0"
|
|
9
10
|
COLOR_LIGHT_RED = "#fc8181"
|
|
10
11
|
COLOR_ORANGE = "#f6ad55"
|
|
11
12
|
COLOR_LIGHT_GREEN = "#68d391"
|
|
@@ -59,5 +60,32 @@ module RailsVitals
|
|
|
59
60
|
else COLOR_LIGHT_GREEN
|
|
60
61
|
end
|
|
61
62
|
end
|
|
63
|
+
|
|
64
|
+
def cost_color(cost)
|
|
65
|
+
return COLOR_NEUTRAL unless cost
|
|
66
|
+
cost = cost.to_f
|
|
67
|
+
if cost < 100 then COLOR_LIGHT_GREEN
|
|
68
|
+
elsif cost < 1000 then COLOR_ORANGE
|
|
69
|
+
else COLOR_LIGHT_RED
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def time_color(ms)
|
|
74
|
+
return COLOR_NEUTRAL unless ms
|
|
75
|
+
ms = ms.to_f
|
|
76
|
+
if ms < 10 then COLOR_LIGHT_GREEN
|
|
77
|
+
elsif ms < 100 then COLOR_ORANGE
|
|
78
|
+
else COLOR_LIGHT_RED
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rows_color(rows)
|
|
83
|
+
return COLOR_NEUTRAL unless rows
|
|
84
|
+
rows = rows.to_i
|
|
85
|
+
if rows < 1_000 then COLOR_LIGHT_GREEN
|
|
86
|
+
elsif rows < 10_000 then COLOR_ORANGE
|
|
87
|
+
else COLOR_LIGHT_RED
|
|
88
|
+
end
|
|
89
|
+
end
|
|
62
90
|
end
|
|
63
91
|
end
|