brainzlab 0.1.3 → 0.1.4
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/lib/brainzlab/configuration.rb +20 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +458 -25
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +700 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation.rb +91 -1
- data/lib/brainzlab/version.rb +1 -1
- metadata +11 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
class ActionView
|
|
6
|
+
# Thresholds for slow render detection (in milliseconds)
|
|
7
|
+
SLOW_RENDER_THRESHOLD = 50
|
|
8
|
+
VERY_SLOW_RENDER_THRESHOLD = 200
|
|
9
|
+
|
|
10
|
+
# Only track partials rendered more than this many times
|
|
11
|
+
COLLECTION_TRACKING_THRESHOLD = 10
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def install!
|
|
15
|
+
return unless defined?(::ActionView)
|
|
16
|
+
return if @installed
|
|
17
|
+
|
|
18
|
+
install_render_template_subscriber!
|
|
19
|
+
install_render_partial_subscriber!
|
|
20
|
+
install_render_collection_subscriber!
|
|
21
|
+
install_render_layout_subscriber!
|
|
22
|
+
|
|
23
|
+
@installed = true
|
|
24
|
+
BrainzLab.debug_log('ActionView instrumentation installed')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def installed?
|
|
28
|
+
@installed == true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# ============================================
|
|
34
|
+
# Render Template
|
|
35
|
+
# ============================================
|
|
36
|
+
def install_render_template_subscriber!
|
|
37
|
+
ActiveSupport::Notifications.subscribe('render_template.action_view') do |*args|
|
|
38
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
39
|
+
handle_render_template(event)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def handle_render_template(event)
|
|
44
|
+
payload = event.payload
|
|
45
|
+
duration = event.duration.round(2)
|
|
46
|
+
|
|
47
|
+
identifier = payload[:identifier]
|
|
48
|
+
layout = payload[:layout]
|
|
49
|
+
|
|
50
|
+
# Extract template name from full path
|
|
51
|
+
template_name = extract_template_name(identifier)
|
|
52
|
+
|
|
53
|
+
# Add breadcrumb for Reflex
|
|
54
|
+
record_render_breadcrumb('template', template_name, duration, layout: layout)
|
|
55
|
+
|
|
56
|
+
# Add span to Pulse
|
|
57
|
+
record_render_span(event, 'template', template_name, duration, layout: layout)
|
|
58
|
+
|
|
59
|
+
# Log slow renders to Recall
|
|
60
|
+
log_slow_render('template', template_name, duration) if duration >= SLOW_RENDER_THRESHOLD
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
BrainzLab.debug_log("ActionView render_template instrumentation failed: #{e.message}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# ============================================
|
|
66
|
+
# Render Partial
|
|
67
|
+
# ============================================
|
|
68
|
+
def install_render_partial_subscriber!
|
|
69
|
+
ActiveSupport::Notifications.subscribe('render_partial.action_view') do |*args|
|
|
70
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
71
|
+
handle_render_partial(event)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def handle_render_partial(event)
|
|
76
|
+
payload = event.payload
|
|
77
|
+
duration = event.duration.round(2)
|
|
78
|
+
|
|
79
|
+
identifier = payload[:identifier]
|
|
80
|
+
template_name = extract_template_name(identifier)
|
|
81
|
+
|
|
82
|
+
# Skip very fast partials to reduce noise
|
|
83
|
+
return if duration < 1 && !payload[:cache_hit]
|
|
84
|
+
|
|
85
|
+
# Add breadcrumb for Reflex
|
|
86
|
+
record_render_breadcrumb('partial', template_name, duration, cached: payload[:cache_hit])
|
|
87
|
+
|
|
88
|
+
# Add span to Pulse
|
|
89
|
+
record_render_span(event, 'partial', template_name, duration, cached: payload[:cache_hit])
|
|
90
|
+
|
|
91
|
+
# Log slow renders to Recall
|
|
92
|
+
log_slow_render('partial', template_name, duration) if duration >= SLOW_RENDER_THRESHOLD
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
BrainzLab.debug_log("ActionView render_partial instrumentation failed: #{e.message}")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ============================================
|
|
98
|
+
# Render Collection
|
|
99
|
+
# ============================================
|
|
100
|
+
def install_render_collection_subscriber!
|
|
101
|
+
ActiveSupport::Notifications.subscribe('render_collection.action_view') do |*args|
|
|
102
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
103
|
+
handle_render_collection(event)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def handle_render_collection(event)
|
|
108
|
+
payload = event.payload
|
|
109
|
+
duration = event.duration.round(2)
|
|
110
|
+
|
|
111
|
+
identifier = payload[:identifier]
|
|
112
|
+
count = payload[:count] || 0
|
|
113
|
+
cache_hits = payload[:cache_hits] || 0
|
|
114
|
+
|
|
115
|
+
template_name = extract_template_name(identifier)
|
|
116
|
+
|
|
117
|
+
# Add breadcrumb for significant collections
|
|
118
|
+
if count >= COLLECTION_TRACKING_THRESHOLD || duration >= SLOW_RENDER_THRESHOLD
|
|
119
|
+
record_collection_breadcrumb(template_name, duration, count, cache_hits)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Add span to Pulse
|
|
123
|
+
record_collection_span(event, template_name, duration, count, cache_hits)
|
|
124
|
+
|
|
125
|
+
# Log slow collection renders to Recall
|
|
126
|
+
if duration >= SLOW_RENDER_THRESHOLD
|
|
127
|
+
log_slow_collection_render(template_name, duration, count, cache_hits)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Track potential N+1 view pattern (many partials rendered)
|
|
131
|
+
track_collection_performance(template_name, count, duration) if count >= 50
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
BrainzLab.debug_log("ActionView render_collection instrumentation failed: #{e.message}")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# ============================================
|
|
137
|
+
# Render Layout
|
|
138
|
+
# ============================================
|
|
139
|
+
def install_render_layout_subscriber!
|
|
140
|
+
ActiveSupport::Notifications.subscribe('render_layout.action_view') do |*args|
|
|
141
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
142
|
+
handle_render_layout(event)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def handle_render_layout(event)
|
|
147
|
+
payload = event.payload
|
|
148
|
+
duration = event.duration.round(2)
|
|
149
|
+
|
|
150
|
+
identifier = payload[:identifier]
|
|
151
|
+
layout_name = extract_template_name(identifier)
|
|
152
|
+
|
|
153
|
+
# Only track if significant
|
|
154
|
+
return if duration < 5
|
|
155
|
+
|
|
156
|
+
# Add span to Pulse (layouts are important for understanding request timing)
|
|
157
|
+
if BrainzLab.configuration.pulse_effectively_enabled?
|
|
158
|
+
tracer = BrainzLab::Pulse.tracer
|
|
159
|
+
if tracer.current_trace
|
|
160
|
+
span_data = {
|
|
161
|
+
span_id: SecureRandom.uuid,
|
|
162
|
+
name: "view.layout.#{layout_name}",
|
|
163
|
+
kind: 'view',
|
|
164
|
+
started_at: event.time,
|
|
165
|
+
ended_at: event.end,
|
|
166
|
+
duration_ms: duration,
|
|
167
|
+
error: false,
|
|
168
|
+
data: {
|
|
169
|
+
'view.type' => 'layout',
|
|
170
|
+
'view.template' => layout_name
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
tracer.current_spans << span_data
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
BrainzLab.debug_log("ActionView render_layout instrumentation failed: #{e.message}")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# ============================================
|
|
182
|
+
# Recording Helpers
|
|
183
|
+
# ============================================
|
|
184
|
+
def record_render_breadcrumb(type, template_name, duration, layout: nil, cached: nil)
|
|
185
|
+
return unless BrainzLab.configuration.reflex_effectively_enabled?
|
|
186
|
+
|
|
187
|
+
level = case duration
|
|
188
|
+
when 0...SLOW_RENDER_THRESHOLD then :info
|
|
189
|
+
when SLOW_RENDER_THRESHOLD...VERY_SLOW_RENDER_THRESHOLD then :warning
|
|
190
|
+
else :error
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
message = if cached
|
|
194
|
+
"Render #{type}: #{template_name} (cached)"
|
|
195
|
+
else
|
|
196
|
+
"Render #{type}: #{template_name} (#{duration}ms)"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
200
|
+
message,
|
|
201
|
+
category: "view.#{type}",
|
|
202
|
+
level: level,
|
|
203
|
+
data: {
|
|
204
|
+
template: template_name,
|
|
205
|
+
type: type,
|
|
206
|
+
duration_ms: duration,
|
|
207
|
+
layout: layout,
|
|
208
|
+
cached: cached
|
|
209
|
+
}.compact
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def record_render_span(event, type, template_name, duration, layout: nil, cached: nil)
|
|
214
|
+
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
215
|
+
|
|
216
|
+
tracer = BrainzLab::Pulse.tracer
|
|
217
|
+
return unless tracer.current_trace
|
|
218
|
+
|
|
219
|
+
span_data = {
|
|
220
|
+
span_id: SecureRandom.uuid,
|
|
221
|
+
name: "view.#{type}.#{template_name}",
|
|
222
|
+
kind: 'view',
|
|
223
|
+
started_at: event.time,
|
|
224
|
+
ended_at: event.end,
|
|
225
|
+
duration_ms: duration,
|
|
226
|
+
error: false,
|
|
227
|
+
data: {
|
|
228
|
+
'view.type' => type,
|
|
229
|
+
'view.template' => template_name,
|
|
230
|
+
'view.layout' => layout,
|
|
231
|
+
'view.cached' => cached
|
|
232
|
+
}.compact
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
tracer.current_spans << span_data
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def record_collection_breadcrumb(template_name, duration, count, cache_hits)
|
|
239
|
+
return unless BrainzLab.configuration.reflex_effectively_enabled?
|
|
240
|
+
|
|
241
|
+
level = case duration
|
|
242
|
+
when 0...SLOW_RENDER_THRESHOLD then :info
|
|
243
|
+
when SLOW_RENDER_THRESHOLD...VERY_SLOW_RENDER_THRESHOLD then :warning
|
|
244
|
+
else :error
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
cache_info = cache_hits > 0 ? " (#{cache_hits}/#{count} cached)" : ""
|
|
248
|
+
|
|
249
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
250
|
+
"Render collection: #{template_name} x#{count}#{cache_info} (#{duration}ms)",
|
|
251
|
+
category: 'view.collection',
|
|
252
|
+
level: level,
|
|
253
|
+
data: {
|
|
254
|
+
template: template_name,
|
|
255
|
+
count: count,
|
|
256
|
+
cache_hits: cache_hits,
|
|
257
|
+
duration_ms: duration,
|
|
258
|
+
avg_per_item_ms: count > 0 ? (duration / count).round(2) : nil
|
|
259
|
+
}.compact
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def record_collection_span(event, template_name, duration, count, cache_hits)
|
|
264
|
+
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
265
|
+
|
|
266
|
+
tracer = BrainzLab::Pulse.tracer
|
|
267
|
+
return unless tracer.current_trace
|
|
268
|
+
|
|
269
|
+
span_data = {
|
|
270
|
+
span_id: SecureRandom.uuid,
|
|
271
|
+
name: "view.collection.#{template_name}",
|
|
272
|
+
kind: 'view',
|
|
273
|
+
started_at: event.time,
|
|
274
|
+
ended_at: event.end,
|
|
275
|
+
duration_ms: duration,
|
|
276
|
+
error: false,
|
|
277
|
+
data: {
|
|
278
|
+
'view.type' => 'collection',
|
|
279
|
+
'view.template' => template_name,
|
|
280
|
+
'view.count' => count,
|
|
281
|
+
'view.cache_hits' => cache_hits,
|
|
282
|
+
'view.avg_per_item_ms' => count > 0 ? (duration / count).round(2) : nil
|
|
283
|
+
}.compact
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
tracer.current_spans << span_data
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# ============================================
|
|
290
|
+
# Logging Helpers
|
|
291
|
+
# ============================================
|
|
292
|
+
def log_slow_render(type, template_name, duration)
|
|
293
|
+
return unless BrainzLab.configuration.recall_effectively_enabled?
|
|
294
|
+
|
|
295
|
+
level = duration >= VERY_SLOW_RENDER_THRESHOLD ? :error : :warn
|
|
296
|
+
|
|
297
|
+
BrainzLab::Recall.send(
|
|
298
|
+
level,
|
|
299
|
+
"Slow #{type} render: #{template_name} (#{duration}ms)",
|
|
300
|
+
template: template_name,
|
|
301
|
+
type: type,
|
|
302
|
+
duration_ms: duration,
|
|
303
|
+
threshold_exceeded: duration >= VERY_SLOW_RENDER_THRESHOLD ? 'critical' : 'warning'
|
|
304
|
+
)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def log_slow_collection_render(template_name, duration, count, cache_hits)
|
|
308
|
+
return unless BrainzLab.configuration.recall_effectively_enabled?
|
|
309
|
+
|
|
310
|
+
level = duration >= VERY_SLOW_RENDER_THRESHOLD ? :error : :warn
|
|
311
|
+
|
|
312
|
+
BrainzLab::Recall.send(
|
|
313
|
+
level,
|
|
314
|
+
"Slow collection render: #{template_name} x#{count} (#{duration}ms)",
|
|
315
|
+
template: template_name,
|
|
316
|
+
type: 'collection',
|
|
317
|
+
count: count,
|
|
318
|
+
cache_hits: cache_hits,
|
|
319
|
+
duration_ms: duration,
|
|
320
|
+
avg_per_item_ms: count > 0 ? (duration / count).round(2) : nil,
|
|
321
|
+
threshold_exceeded: duration >= VERY_SLOW_RENDER_THRESHOLD ? 'critical' : 'warning'
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def track_collection_performance(template_name, count, duration)
|
|
326
|
+
avg_per_item = count > 0 ? (duration / count).round(2) : 0
|
|
327
|
+
|
|
328
|
+
# If average time per item is high, this might indicate N+1 in the partial
|
|
329
|
+
if avg_per_item > 5 # More than 5ms per item is suspicious
|
|
330
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
331
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
332
|
+
"Slow collection items: #{template_name} (#{avg_per_item}ms/item)",
|
|
333
|
+
category: 'view.performance',
|
|
334
|
+
level: :warning,
|
|
335
|
+
data: {
|
|
336
|
+
template: template_name,
|
|
337
|
+
count: count,
|
|
338
|
+
duration_ms: duration,
|
|
339
|
+
avg_per_item_ms: avg_per_item,
|
|
340
|
+
suggestion: 'Consider caching, eager loading, or optimizing the partial'
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
if BrainzLab.configuration.recall_effectively_enabled?
|
|
346
|
+
BrainzLab::Recall.warn(
|
|
347
|
+
"Collection partial may have N+1 or performance issue",
|
|
348
|
+
template: template_name,
|
|
349
|
+
count: count,
|
|
350
|
+
duration_ms: duration,
|
|
351
|
+
avg_per_item_ms: avg_per_item
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# ============================================
|
|
358
|
+
# Helper Methods
|
|
359
|
+
# ============================================
|
|
360
|
+
def extract_template_name(identifier)
|
|
361
|
+
return 'unknown' unless identifier
|
|
362
|
+
|
|
363
|
+
# Remove the full path and extract just the template name
|
|
364
|
+
# e.g., "/app/views/users/show.html.erb" -> "users/show"
|
|
365
|
+
path = identifier.to_s
|
|
366
|
+
|
|
367
|
+
# Try to extract from app/views path
|
|
368
|
+
if path.include?('app/views/')
|
|
369
|
+
template = path.split('app/views/').last
|
|
370
|
+
# Remove extension
|
|
371
|
+
template.sub(/\.[^.]+\z/, '').sub(/\.[^.]+\z/, '')
|
|
372
|
+
else
|
|
373
|
+
# Fallback: just use the filename
|
|
374
|
+
File.basename(path).sub(/\.[^.]+\z/, '').sub(/\.[^.]+\z/, '')
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|