rails_vitals 0.2.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/MIT-LICENSE +20 -0
- data/README.md +340 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/rails_vitals/application.css +180 -0
- data/app/controllers/rails_vitals/application_controller.rb +30 -0
- data/app/controllers/rails_vitals/associations_controller.rb +8 -0
- data/app/controllers/rails_vitals/dashboard_controller.rb +59 -0
- data/app/controllers/rails_vitals/heatmap_controller.rb +39 -0
- data/app/controllers/rails_vitals/models_controller.rb +65 -0
- data/app/controllers/rails_vitals/n_plus_ones_controller.rb +43 -0
- data/app/controllers/rails_vitals/requests_controller.rb +44 -0
- data/app/helpers/rails_vitals/application_helper.rb +63 -0
- data/app/jobs/rails_vitals/application_job.rb +4 -0
- data/app/mailers/rails_vitals/application_mailer.rb +6 -0
- data/app/models/rails_vitals/application_record.rb +5 -0
- data/app/views/layouts/rails_vitals/application.html.erb +27 -0
- data/app/views/rails_vitals/associations/index.html.erb +370 -0
- data/app/views/rails_vitals/dashboard/index.html.erb +158 -0
- data/app/views/rails_vitals/heatmap/index.html.erb +66 -0
- data/app/views/rails_vitals/models/index.html.erb +117 -0
- data/app/views/rails_vitals/n_plus_ones/index.html.erb +49 -0
- data/app/views/rails_vitals/n_plus_ones/show.html.erb +139 -0
- data/app/views/rails_vitals/requests/index.html.erb +60 -0
- data/app/views/rails_vitals/requests/show.html.erb +396 -0
- data/config/routes.rb +9 -0
- data/lib/rails_vitals/analyzers/association_mapper.rb +121 -0
- data/lib/rails_vitals/analyzers/n_plus_one_aggregator.rb +116 -0
- data/lib/rails_vitals/analyzers/sql_tokenizer.rb +240 -0
- data/lib/rails_vitals/collector.rb +78 -0
- data/lib/rails_vitals/configuration.rb +27 -0
- data/lib/rails_vitals/engine.rb +25 -0
- data/lib/rails_vitals/instrumentation/callback_instrumentation.rb +30 -0
- data/lib/rails_vitals/middleware/panel_injector.rb +75 -0
- data/lib/rails_vitals/notifications/subscriber.rb +59 -0
- data/lib/rails_vitals/panel_renderer.rb +233 -0
- data/lib/rails_vitals/request_record.rb +51 -0
- data/lib/rails_vitals/scorers/base_scorer.rb +25 -0
- data/lib/rails_vitals/scorers/composite_scorer.rb +36 -0
- data/lib/rails_vitals/scorers/n_plus_one_scorer.rb +43 -0
- data/lib/rails_vitals/scorers/query_scorer.rb +42 -0
- data/lib/rails_vitals/store.rb +34 -0
- data/lib/rails_vitals/version.rb +3 -0
- data/lib/rails_vitals.rb +33 -0
- data/lib/tasks/rails_vitals_tasks.rake +4 -0
- metadata +113 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# lib/rails_vitals/panel_renderer.rb
|
|
2
|
+
module RailsVitals
|
|
3
|
+
class PanelRenderer
|
|
4
|
+
def self.render(collector, scorer)
|
|
5
|
+
new(collector, scorer).render
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(collector, scorer)
|
|
9
|
+
@collector = collector
|
|
10
|
+
@scorer = scorer
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render
|
|
14
|
+
<<~HTML
|
|
15
|
+
<div id="rails-vitals-panel" style="#{panel_styles}">
|
|
16
|
+
#{toggle_button}
|
|
17
|
+
#{collapsed_badge}
|
|
18
|
+
#{expanded_content}
|
|
19
|
+
</div>
|
|
20
|
+
#{inline_script}
|
|
21
|
+
HTML
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# ─── PANEL WRAPPER ───────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
def panel_styles
|
|
29
|
+
"position:fixed;bottom:20px;right:20px;z-index:999999;font-family:monospace;font-size:12px;"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ─── COLLAPSED STATE ─────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
def collapsed_badge
|
|
35
|
+
<<~HTML
|
|
36
|
+
<div id="rv-badge" style="
|
|
37
|
+
background:#{score_bg};
|
|
38
|
+
color:#fff;
|
|
39
|
+
padding:6px 12px;
|
|
40
|
+
border-radius:6px;
|
|
41
|
+
cursor:pointer;
|
|
42
|
+
display:flex;
|
|
43
|
+
align-items:center;
|
|
44
|
+
gap:8px;
|
|
45
|
+
box-shadow:0 2px 8px rgba(0,0,0,0.3);
|
|
46
|
+
" onclick="rvToggle()">
|
|
47
|
+
<span style="font-weight:bold;font-size:14px;">#{@scorer.score}</span>
|
|
48
|
+
<span style="opacity:0.85;">#{@scorer.label}</span>
|
|
49
|
+
<span style="opacity:0.6;font-size:10px;">▲</span>
|
|
50
|
+
</div>
|
|
51
|
+
HTML
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def toggle_button; ""; end
|
|
55
|
+
|
|
56
|
+
# ─── EXPANDED STATE ───────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
def expanded_content
|
|
59
|
+
<<~HTML
|
|
60
|
+
<div id="rv-expanded" style="
|
|
61
|
+
display:none;
|
|
62
|
+
background:#1a1a2e;
|
|
63
|
+
color:#e2e8f0;
|
|
64
|
+
border-radius:8px;
|
|
65
|
+
padding:16px;
|
|
66
|
+
margin-bottom:8px;
|
|
67
|
+
width:380px;
|
|
68
|
+
box-shadow:0 4px 20px rgba(0,0,0,0.5);
|
|
69
|
+
border:1px solid #2d3748;
|
|
70
|
+
">
|
|
71
|
+
#{header_section}
|
|
72
|
+
#{divider}
|
|
73
|
+
#{request_info_section}
|
|
74
|
+
#{divider}
|
|
75
|
+
#{query_summary_section}
|
|
76
|
+
#{divider}
|
|
77
|
+
#{slowest_queries_section}
|
|
78
|
+
#{divider}
|
|
79
|
+
#{admin_link_section}
|
|
80
|
+
</div>
|
|
81
|
+
HTML
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# ─── SECTIONS ────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
def header_section
|
|
87
|
+
<<~HTML
|
|
88
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
|
89
|
+
<div>
|
|
90
|
+
<span style="font-size:28px;font-weight:bold;color:#{score_bg};">#{@scorer.score}</span>
|
|
91
|
+
<span style="font-size:13px;color:#a0aec0;margin-left:6px;">/ 100</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div style="text-align:right;">
|
|
94
|
+
<div style="
|
|
95
|
+
background:#{score_bg};
|
|
96
|
+
color:#fff;
|
|
97
|
+
padding:3px 10px;
|
|
98
|
+
border-radius:4px;
|
|
99
|
+
font-size:11px;
|
|
100
|
+
font-weight:bold;
|
|
101
|
+
">#{@scorer.label}</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
HTML
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def request_info_section
|
|
108
|
+
<<~HTML
|
|
109
|
+
<div style="margin-bottom:4px;">
|
|
110
|
+
#{label_row("Endpoint", "#{@collector.controller}##{@collector.action}")}
|
|
111
|
+
#{label_row("Method", @collector.http_method.to_s.upcase)}
|
|
112
|
+
#{label_row("Status", @collector.response_status.to_s)}
|
|
113
|
+
#{label_row("Duration", "#{@collector.duration_ms&.round(1)}ms")}
|
|
114
|
+
</div>
|
|
115
|
+
HTML
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def query_summary_section
|
|
119
|
+
n_plus_one_count = n_plus_one_scorer.n_plus_one_patterns.size
|
|
120
|
+
|
|
121
|
+
<<~HTML
|
|
122
|
+
<div style="margin-bottom:4px;">
|
|
123
|
+
#{label_row("Queries", @collector.total_query_count.to_s)}
|
|
124
|
+
#{label_row("DB Time", "#{@collector.total_db_time_ms.round(1)}ms")}
|
|
125
|
+
#{label_row("N+1", n_plus_one_badge(n_plus_one_count))}
|
|
126
|
+
</div>
|
|
127
|
+
HTML
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def slowest_queries_section
|
|
131
|
+
queries = @collector.slowest_queries(5)
|
|
132
|
+
return "" if queries.empty?
|
|
133
|
+
|
|
134
|
+
rows = queries.map do |q|
|
|
135
|
+
sql = truncate(q[:sql], 45)
|
|
136
|
+
time_ms = q[:duration_ms].round(1)
|
|
137
|
+
<<~HTML
|
|
138
|
+
<div style="margin-bottom:6px;">
|
|
139
|
+
<div style="color:#90cdf4;font-size:11px;">#{escape(sql)}</div>
|
|
140
|
+
<div style="color:#68d391;font-size:10px;">#{time_ms}ms</div>
|
|
141
|
+
</div>
|
|
142
|
+
HTML
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
<<~HTML
|
|
146
|
+
<div>
|
|
147
|
+
<div style="color:#a0aec0;font-size:10px;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.05em;">
|
|
148
|
+
Slowest Queries
|
|
149
|
+
</div>
|
|
150
|
+
#{rows.join}
|
|
151
|
+
</div>
|
|
152
|
+
HTML
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def admin_link_section
|
|
156
|
+
record = RailsVitals.store.all.last
|
|
157
|
+
return "" unless record
|
|
158
|
+
|
|
159
|
+
<<~HTML
|
|
160
|
+
<div style="text-align:right;margin-top:4px;">
|
|
161
|
+
<a href="/rails_vitals/requests/#{record.id}"
|
|
162
|
+
style="color:#90cdf4;font-size:11px;text-decoration:none;"
|
|
163
|
+
target="_blank">
|
|
164
|
+
View full report →
|
|
165
|
+
</a>
|
|
166
|
+
</div>
|
|
167
|
+
HTML
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# ─── HELPERS ─────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
def divider
|
|
173
|
+
"<div style='border-top:1px solid #2d3748;margin:10px 0;'></div>"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def label_row(label, value)
|
|
177
|
+
<<~HTML
|
|
178
|
+
<div style="display:flex;justify-content:space-between;padding:2px 0;">
|
|
179
|
+
<span style="color:#a0aec0;">#{label}</span>
|
|
180
|
+
<span style="color:#e2e8f0;">#{value}</span>
|
|
181
|
+
</div>
|
|
182
|
+
HTML
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def n_plus_one_badge(count)
|
|
186
|
+
return "<span style='color:#68d391;'>None</span>" if count.zero?
|
|
187
|
+
|
|
188
|
+
"<span style='background:#e53e3e;color:#fff;padding:1px 6px;border-radius:3px;font-size:10px;'>#{count} detected</span>"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def score_bg
|
|
192
|
+
case @scorer.color
|
|
193
|
+
when "green" then "#276749"
|
|
194
|
+
when "blue" then "#2b6cb0"
|
|
195
|
+
when "amber" then "#b7791f"
|
|
196
|
+
else "#c53030"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def n_plus_one_scorer
|
|
201
|
+
@n_plus_one_scorer ||= Scorers::NPlusOneScorer.new(@collector)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def truncate(str, length)
|
|
205
|
+
str.length > length ? "#{str[0, length]}…" : str
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def escape(str)
|
|
209
|
+
str.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# ─── JAVASCRIPT ──────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
def inline_script
|
|
215
|
+
<<~HTML
|
|
216
|
+
<script>
|
|
217
|
+
function rvToggle() {
|
|
218
|
+
var expanded = document.getElementById('rv-expanded');
|
|
219
|
+
var badge = document.getElementById('rv-badge');
|
|
220
|
+
var arrow = badge.querySelector('span:last-child');
|
|
221
|
+
if (expanded.style.display === 'none') {
|
|
222
|
+
expanded.style.display = 'block';
|
|
223
|
+
arrow.textContent = '▼';
|
|
224
|
+
} else {
|
|
225
|
+
expanded.style.display = 'none';
|
|
226
|
+
arrow.textContent = '▲';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
</script>
|
|
230
|
+
HTML
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module RailsVitals
|
|
2
|
+
class RequestRecord
|
|
3
|
+
attr_reader :id, :controller, :action, :http_method,
|
|
4
|
+
:response_status, :duration_ms, :score,
|
|
5
|
+
:label, :color, :queries, :n_plus_one_patterns,
|
|
6
|
+
:callbacks, :total_callback_time_ms, :recorded_at
|
|
7
|
+
|
|
8
|
+
def initialize(collector:, scorer:)
|
|
9
|
+
@id = SecureRandom.hex(8)
|
|
10
|
+
@controller = collector.controller
|
|
11
|
+
@action = collector.action
|
|
12
|
+
@http_method = collector.http_method
|
|
13
|
+
@response_status = collector.response_status
|
|
14
|
+
@duration_ms = collector.duration_ms
|
|
15
|
+
@queries = collector.queries
|
|
16
|
+
@callbacks = collector.callbacks
|
|
17
|
+
@total_callback_time_ms = collector.total_callback_time_ms
|
|
18
|
+
@score = scorer.score
|
|
19
|
+
@label = scorer.label
|
|
20
|
+
@color = scorer.color
|
|
21
|
+
@n_plus_one_patterns = build_n_plus_one_patterns(scorer)
|
|
22
|
+
@recorded_at = Time.now
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def endpoint
|
|
26
|
+
"#{@controller}##{@action}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def total_query_count
|
|
30
|
+
@queries.size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def total_db_time_ms
|
|
34
|
+
@queries.sum { |q| q[:duration_ms] }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def slowest_queries(limit = 3)
|
|
38
|
+
@queries.sort_by { |q| -q[:duration_ms] }.first(limit)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_n_plus_one_patterns(scorer)
|
|
44
|
+
scorer_instance = scorer.is_a?(Scorers::CompositeScorer) ?
|
|
45
|
+
Scorers::NPlusOneScorer.new(scorer.instance_variable_get(:@collector)) :
|
|
46
|
+
nil
|
|
47
|
+
|
|
48
|
+
scorer_instance&.n_plus_one_patterns || {}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module RailsVitals
|
|
2
|
+
module Scorers
|
|
3
|
+
class BaseScorer
|
|
4
|
+
HEALTHY = (90..100)
|
|
5
|
+
ACCEPTABLE = (70..89)
|
|
6
|
+
WARNING = (50..69)
|
|
7
|
+
CRITICAL = (0..49)
|
|
8
|
+
|
|
9
|
+
def initialize(collector)
|
|
10
|
+
@collector = collector
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns a score between 0 and 100
|
|
14
|
+
def score
|
|
15
|
+
raise NotImplementedError, "#{self.class} must implement #score"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def clamp(value)
|
|
21
|
+
value.clamp(0, 100)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module RailsVitals
|
|
2
|
+
module Scorers
|
|
3
|
+
class CompositeScorer < BaseScorer
|
|
4
|
+
# Weights will grow as we add more scorers
|
|
5
|
+
WEIGHTS = {
|
|
6
|
+
query: 0.40,
|
|
7
|
+
n_plus_one: 0.60
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def score
|
|
11
|
+
clamp(
|
|
12
|
+
(QueryScorer.new(@collector).score * WEIGHTS[:query]).round +
|
|
13
|
+
(NPlusOneScorer.new(@collector).score * WEIGHTS[:n_plus_one]).round
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def label
|
|
18
|
+
case score
|
|
19
|
+
when BaseScorer::HEALTHY then "Healthy"
|
|
20
|
+
when BaseScorer::ACCEPTABLE then "Acceptable"
|
|
21
|
+
when BaseScorer::WARNING then "Warning"
|
|
22
|
+
else "Critical"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def color
|
|
27
|
+
case score
|
|
28
|
+
when BaseScorer::HEALTHY then "green"
|
|
29
|
+
when BaseScorer::ACCEPTABLE then "blue"
|
|
30
|
+
when BaseScorer::WARNING then "amber"
|
|
31
|
+
else "red"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module RailsVitals
|
|
2
|
+
module Scorers
|
|
3
|
+
class NPlusOneScorer < BaseScorer
|
|
4
|
+
# Minimum times the same query must repeat to be flagged
|
|
5
|
+
REPEAT_THRESHOLD = 3
|
|
6
|
+
|
|
7
|
+
def score
|
|
8
|
+
return 100 if n_plus_one_count.zero?
|
|
9
|
+
|
|
10
|
+
# Each N+1 pattern costs 25 points, floored at 0
|
|
11
|
+
clamp(100 - (n_plus_one_count * 25))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def n_plus_one_patterns
|
|
15
|
+
query_fingerprints
|
|
16
|
+
.select { |_fingerprint, count| count >= REPEAT_THRESHOLD }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def n_plus_one_count
|
|
22
|
+
n_plus_one_patterns.size
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def query_fingerprints
|
|
26
|
+
@collector.queries
|
|
27
|
+
.select { |q| q.is_a?(Hash) && q[:sql].is_a?(String) }
|
|
28
|
+
.map { |q| normalize(q[:sql]) }
|
|
29
|
+
.tally
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Strip values to compare query structure, not data
|
|
33
|
+
def normalize(sql)
|
|
34
|
+
sql
|
|
35
|
+
.gsub(/\d+/, "?")
|
|
36
|
+
.gsub(/'[^']*'/, "?")
|
|
37
|
+
.gsub(/\s+/, " ")
|
|
38
|
+
.strip
|
|
39
|
+
.downcase
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module RailsVitals
|
|
2
|
+
module Scorers
|
|
3
|
+
class QueryScorer < BaseScorer
|
|
4
|
+
def score
|
|
5
|
+
clamp(count_score + time_score)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def count_score
|
|
11
|
+
count = @collector.total_query_count
|
|
12
|
+
warn_threshold = RailsVitals.config.query_warn_threshold
|
|
13
|
+
critical_threshold = RailsVitals.config.query_critical_threshold
|
|
14
|
+
|
|
15
|
+
if count <= warn_threshold
|
|
16
|
+
50 # full points
|
|
17
|
+
elsif count <= critical_threshold
|
|
18
|
+
# linear decay between warn and critical
|
|
19
|
+
ratio = (count - warn_threshold).to_f / (critical_threshold - warn_threshold)
|
|
20
|
+
(50 * (1 - ratio)).round
|
|
21
|
+
else
|
|
22
|
+
0
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def time_score
|
|
27
|
+
time_ms = @collector.total_db_time_ms
|
|
28
|
+
warn_ms = RailsVitals.config.db_time_warn_ms
|
|
29
|
+
critical_ms = RailsVitals.config.db_time_critical_ms
|
|
30
|
+
|
|
31
|
+
if time_ms <= warn_ms
|
|
32
|
+
50
|
|
33
|
+
elsif time_ms <= critical_ms
|
|
34
|
+
ratio = (time_ms - warn_ms).to_f / (critical_ms - warn_ms)
|
|
35
|
+
(50 * (1 - ratio)).round
|
|
36
|
+
else
|
|
37
|
+
0
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module RailsVitals
|
|
2
|
+
class Store
|
|
3
|
+
def initialize(size)
|
|
4
|
+
@size = size
|
|
5
|
+
@records = []
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def push(record)
|
|
10
|
+
@mutex.synchronize do
|
|
11
|
+
@records.push(record)
|
|
12
|
+
@records.shift if @records.size > @size
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def all
|
|
17
|
+
@mutex.synchronize { @records.dup }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find(id)
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
@records.find { |r| r.id == id }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clear
|
|
27
|
+
@mutex.synchronize { @records.clear }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def size
|
|
31
|
+
@mutex.synchronize { @records.size }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/rails_vitals.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "rails_vitals/version"
|
|
2
|
+
require "rails_vitals/configuration"
|
|
3
|
+
require "rails_vitals/store"
|
|
4
|
+
require "rails_vitals/collector"
|
|
5
|
+
require "rails_vitals/request_record"
|
|
6
|
+
require "rails_vitals/notifications/subscriber"
|
|
7
|
+
require "rails_vitals/instrumentation/callback_instrumentation"
|
|
8
|
+
require "rails_vitals/analyzers/n_plus_one_aggregator"
|
|
9
|
+
require "rails_vitals/analyzers/sql_tokenizer"
|
|
10
|
+
require "rails_vitals/analyzers/association_mapper"
|
|
11
|
+
require "rails_vitals/scorers/base_scorer"
|
|
12
|
+
require "rails_vitals/scorers/query_scorer"
|
|
13
|
+
require "rails_vitals/scorers/n_plus_one_scorer"
|
|
14
|
+
require "rails_vitals/scorers/composite_scorer"
|
|
15
|
+
require "rails_vitals/panel_renderer"
|
|
16
|
+
require "rails_vitals/middleware/panel_injector"
|
|
17
|
+
require "rails_vitals/engine"
|
|
18
|
+
|
|
19
|
+
module RailsVitals
|
|
20
|
+
class << self
|
|
21
|
+
def configure
|
|
22
|
+
yield config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def config
|
|
26
|
+
@config ||= Configuration.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def store
|
|
30
|
+
@store ||= Store.new(config.store_size)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_vitals
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- David Sanchez
|
|
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: '8.1'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 8.1.2
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '8.1'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 8.1.2
|
|
32
|
+
description: RailsVitals is a lightweight Rails engine gem that makes the hidden runtime
|
|
33
|
+
behavior of a Rails application visible, measurable, and teachable. It provides
|
|
34
|
+
insights into the inner workings of a Rails app, helping developers understand and
|
|
35
|
+
optimize their code. With RailsVitals, you can easily identify performance bottlenecks,
|
|
36
|
+
track database queries, and gain a deeper understanding of how your application
|
|
37
|
+
operates under the hood.
|
|
38
|
+
email:
|
|
39
|
+
- sanchez.dav90@gmail.com
|
|
40
|
+
executables: []
|
|
41
|
+
extensions: []
|
|
42
|
+
extra_rdoc_files: []
|
|
43
|
+
files:
|
|
44
|
+
- MIT-LICENSE
|
|
45
|
+
- README.md
|
|
46
|
+
- Rakefile
|
|
47
|
+
- app/assets/stylesheets/rails_vitals/application.css
|
|
48
|
+
- app/controllers/rails_vitals/application_controller.rb
|
|
49
|
+
- app/controllers/rails_vitals/associations_controller.rb
|
|
50
|
+
- app/controllers/rails_vitals/dashboard_controller.rb
|
|
51
|
+
- app/controllers/rails_vitals/heatmap_controller.rb
|
|
52
|
+
- app/controllers/rails_vitals/models_controller.rb
|
|
53
|
+
- app/controllers/rails_vitals/n_plus_ones_controller.rb
|
|
54
|
+
- app/controllers/rails_vitals/requests_controller.rb
|
|
55
|
+
- app/helpers/rails_vitals/application_helper.rb
|
|
56
|
+
- app/jobs/rails_vitals/application_job.rb
|
|
57
|
+
- app/mailers/rails_vitals/application_mailer.rb
|
|
58
|
+
- app/models/rails_vitals/application_record.rb
|
|
59
|
+
- app/views/layouts/rails_vitals/application.html.erb
|
|
60
|
+
- app/views/rails_vitals/associations/index.html.erb
|
|
61
|
+
- app/views/rails_vitals/dashboard/index.html.erb
|
|
62
|
+
- app/views/rails_vitals/heatmap/index.html.erb
|
|
63
|
+
- app/views/rails_vitals/models/index.html.erb
|
|
64
|
+
- app/views/rails_vitals/n_plus_ones/index.html.erb
|
|
65
|
+
- app/views/rails_vitals/n_plus_ones/show.html.erb
|
|
66
|
+
- app/views/rails_vitals/requests/index.html.erb
|
|
67
|
+
- app/views/rails_vitals/requests/show.html.erb
|
|
68
|
+
- config/routes.rb
|
|
69
|
+
- lib/rails_vitals.rb
|
|
70
|
+
- lib/rails_vitals/analyzers/association_mapper.rb
|
|
71
|
+
- lib/rails_vitals/analyzers/n_plus_one_aggregator.rb
|
|
72
|
+
- lib/rails_vitals/analyzers/sql_tokenizer.rb
|
|
73
|
+
- lib/rails_vitals/collector.rb
|
|
74
|
+
- lib/rails_vitals/configuration.rb
|
|
75
|
+
- lib/rails_vitals/engine.rb
|
|
76
|
+
- lib/rails_vitals/instrumentation/callback_instrumentation.rb
|
|
77
|
+
- lib/rails_vitals/middleware/panel_injector.rb
|
|
78
|
+
- lib/rails_vitals/notifications/subscriber.rb
|
|
79
|
+
- lib/rails_vitals/panel_renderer.rb
|
|
80
|
+
- lib/rails_vitals/request_record.rb
|
|
81
|
+
- lib/rails_vitals/scorers/base_scorer.rb
|
|
82
|
+
- lib/rails_vitals/scorers/composite_scorer.rb
|
|
83
|
+
- lib/rails_vitals/scorers/n_plus_one_scorer.rb
|
|
84
|
+
- lib/rails_vitals/scorers/query_scorer.rb
|
|
85
|
+
- lib/rails_vitals/store.rb
|
|
86
|
+
- lib/rails_vitals/version.rb
|
|
87
|
+
- lib/tasks/rails_vitals_tasks.rake
|
|
88
|
+
homepage: https://github.com/Sanchezdav/rails_vitals
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
homepage_uri: https://github.com/Sanchezdav/rails_vitals
|
|
93
|
+
source_code_uri: https://github.com/Sanchezdav/rails_vitals/tree/main
|
|
94
|
+
changelog_uri: https://github.com/Sanchezdav/rails_vitals/CHANGELOG.md
|
|
95
|
+
rdoc_options: []
|
|
96
|
+
require_paths:
|
|
97
|
+
- lib
|
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 3.0.0
|
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '0'
|
|
108
|
+
requirements: []
|
|
109
|
+
rubygems_version: 3.7.2
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: RailsVitals is a lightweight Rails engine gem that makes the hidden runtime
|
|
112
|
+
behavior of a Rails application visible, measurable, and teachable.
|
|
113
|
+
test_files: []
|