brainzlab-rails 0.1.1
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/CLAUDE.md +144 -0
- data/IMPLEMENTATION_PLAN.md +370 -0
- data/Rakefile +8 -0
- data/brainzlab-rails.gemspec +42 -0
- data/lib/brainzlab/rails/analyzers/cache_efficiency.rb +123 -0
- data/lib/brainzlab/rails/analyzers/n_plus_one_detector.rb +90 -0
- data/lib/brainzlab/rails/analyzers/slow_query_analyzer.rb +118 -0
- data/lib/brainzlab/rails/collectors/action_cable.rb +212 -0
- data/lib/brainzlab/rails/collectors/action_controller.rb +299 -0
- data/lib/brainzlab/rails/collectors/action_mailer.rb +187 -0
- data/lib/brainzlab/rails/collectors/action_view.rb +176 -0
- data/lib/brainzlab/rails/collectors/active_job.rb +374 -0
- data/lib/brainzlab/rails/collectors/active_record.rb +250 -0
- data/lib/brainzlab/rails/collectors/active_storage.rb +306 -0
- data/lib/brainzlab/rails/collectors/base.rb +129 -0
- data/lib/brainzlab/rails/collectors/cache.rb +384 -0
- data/lib/brainzlab/rails/configuration.rb +121 -0
- data/lib/brainzlab/rails/event_router.rb +67 -0
- data/lib/brainzlab/rails/railtie.rb +98 -0
- data/lib/brainzlab/rails/subscriber.rb +164 -0
- data/lib/brainzlab/rails/version.rb +7 -0
- data/lib/brainzlab-rails.rb +72 -0
- metadata +178 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Rails
|
|
5
|
+
module Collectors
|
|
6
|
+
# Collects Active Support Cache events
|
|
7
|
+
# Tracks cache efficiency and performance
|
|
8
|
+
class Cache < Base
|
|
9
|
+
def initialize(configuration)
|
|
10
|
+
super
|
|
11
|
+
@cache_analyzer = Analyzers::CacheEfficiency.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def process(event_data)
|
|
15
|
+
case event_data[:name]
|
|
16
|
+
when 'cache_read.active_support'
|
|
17
|
+
handle_read(event_data)
|
|
18
|
+
when 'cache_read_multi.active_support'
|
|
19
|
+
handle_read_multi(event_data)
|
|
20
|
+
when 'cache_generate.active_support'
|
|
21
|
+
handle_generate(event_data)
|
|
22
|
+
when 'cache_fetch_hit.active_support'
|
|
23
|
+
handle_fetch_hit(event_data)
|
|
24
|
+
when 'cache_write.active_support'
|
|
25
|
+
handle_write(event_data)
|
|
26
|
+
when 'cache_write_multi.active_support'
|
|
27
|
+
handle_write_multi(event_data)
|
|
28
|
+
when 'cache_increment.active_support'
|
|
29
|
+
handle_increment(event_data)
|
|
30
|
+
when 'cache_decrement.active_support'
|
|
31
|
+
handle_decrement(event_data)
|
|
32
|
+
when 'cache_delete.active_support'
|
|
33
|
+
handle_delete(event_data)
|
|
34
|
+
when 'cache_delete_multi.active_support'
|
|
35
|
+
handle_delete_multi(event_data)
|
|
36
|
+
when 'cache_delete_matched.active_support'
|
|
37
|
+
handle_delete_matched(event_data)
|
|
38
|
+
when 'cache_cleanup.active_support'
|
|
39
|
+
handle_cleanup(event_data)
|
|
40
|
+
when 'cache_prune.active_support'
|
|
41
|
+
handle_prune(event_data)
|
|
42
|
+
when 'cache_exist?.active_support'
|
|
43
|
+
handle_exist(event_data)
|
|
44
|
+
when 'message_serializer_fallback.active_support'
|
|
45
|
+
handle_serializer_fallback(event_data)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Track cache efficiency if enabled
|
|
49
|
+
if @configuration.cache_efficiency_tracking
|
|
50
|
+
@cache_analyzer.track(event_data)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def handle_read(event_data)
|
|
57
|
+
payload = event_data[:payload]
|
|
58
|
+
key = payload[:key]
|
|
59
|
+
store = payload[:store]
|
|
60
|
+
hit = payload[:hit]
|
|
61
|
+
duration_ms = event_data[:duration_ms]
|
|
62
|
+
|
|
63
|
+
# === PULSE: Cache read span ===
|
|
64
|
+
send_to_pulse(event_data, {
|
|
65
|
+
name: "cache.read",
|
|
66
|
+
category: 'cache.read',
|
|
67
|
+
attributes: {
|
|
68
|
+
key: truncate_key(key),
|
|
69
|
+
store: store,
|
|
70
|
+
hit: hit,
|
|
71
|
+
super_operation: payload[:super_operation]
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# === FLUX: Metrics ===
|
|
76
|
+
tags = { store: store }
|
|
77
|
+
send_to_flux(:increment, 'rails.cache.reads', 1, tags)
|
|
78
|
+
send_to_flux(:increment, hit ? 'rails.cache.hits' : 'rails.cache.misses', 1, tags)
|
|
79
|
+
send_to_flux(:timing, 'rails.cache.read_ms', duration_ms, tags)
|
|
80
|
+
|
|
81
|
+
# === REFLEX: Breadcrumb ===
|
|
82
|
+
add_breadcrumb(
|
|
83
|
+
"Cache #{hit ? 'hit' : 'miss'}: #{truncate_key(key)}",
|
|
84
|
+
category: 'cache.read',
|
|
85
|
+
level: :debug,
|
|
86
|
+
data: {
|
|
87
|
+
key: truncate_key(key),
|
|
88
|
+
hit: hit,
|
|
89
|
+
duration_ms: duration_ms
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_read_multi(event_data)
|
|
95
|
+
payload = event_data[:payload]
|
|
96
|
+
keys = payload[:key] || []
|
|
97
|
+
store = payload[:store]
|
|
98
|
+
hits = payload[:hits] || []
|
|
99
|
+
duration_ms = event_data[:duration_ms]
|
|
100
|
+
|
|
101
|
+
hit_count = hits.size
|
|
102
|
+
miss_count = keys.size - hit_count
|
|
103
|
+
hit_rate = keys.size > 0 ? (hit_count.to_f / keys.size * 100).round(1) : 0
|
|
104
|
+
|
|
105
|
+
# === PULSE: Multi-read span ===
|
|
106
|
+
send_to_pulse(event_data, {
|
|
107
|
+
name: "cache.read_multi",
|
|
108
|
+
category: 'cache.read',
|
|
109
|
+
attributes: {
|
|
110
|
+
key_count: keys.size,
|
|
111
|
+
hit_count: hit_count,
|
|
112
|
+
miss_count: miss_count,
|
|
113
|
+
hit_rate: hit_rate,
|
|
114
|
+
store: store
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# === FLUX: Metrics ===
|
|
119
|
+
tags = { store: store }
|
|
120
|
+
send_to_flux(:increment, 'rails.cache.multi_reads', 1, tags)
|
|
121
|
+
send_to_flux(:increment, 'rails.cache.hits', hit_count, tags)
|
|
122
|
+
send_to_flux(:increment, 'rails.cache.misses', miss_count, tags)
|
|
123
|
+
send_to_flux(:histogram, 'rails.cache.multi_read_keys', keys.size, tags)
|
|
124
|
+
send_to_flux(:timing, 'rails.cache.read_multi_ms', duration_ms, tags)
|
|
125
|
+
|
|
126
|
+
# === REFLEX: Breadcrumb ===
|
|
127
|
+
add_breadcrumb(
|
|
128
|
+
"Cache multi-read: #{keys.size} keys (#{hit_rate}% hit rate)",
|
|
129
|
+
category: 'cache.read',
|
|
130
|
+
level: :debug,
|
|
131
|
+
data: {
|
|
132
|
+
key_count: keys.size,
|
|
133
|
+
hit_count: hit_count,
|
|
134
|
+
hit_rate: hit_rate
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def handle_generate(event_data)
|
|
140
|
+
payload = event_data[:payload]
|
|
141
|
+
key = payload[:key]
|
|
142
|
+
store = payload[:store]
|
|
143
|
+
duration_ms = event_data[:duration_ms]
|
|
144
|
+
|
|
145
|
+
# This fires on cache miss + block execution
|
|
146
|
+
# === PULSE: Cache generate span ===
|
|
147
|
+
send_to_pulse(event_data, {
|
|
148
|
+
name: "cache.generate",
|
|
149
|
+
category: 'cache.generate',
|
|
150
|
+
attributes: {
|
|
151
|
+
key: truncate_key(key),
|
|
152
|
+
store: store
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
# === FLUX: Metrics ===
|
|
157
|
+
send_to_flux(:increment, 'rails.cache.generates', 1, { store: store })
|
|
158
|
+
send_to_flux(:timing, 'rails.cache.generate_ms', duration_ms, { store: store })
|
|
159
|
+
|
|
160
|
+
# Flag slow cache generations
|
|
161
|
+
if duration_ms > 100
|
|
162
|
+
send_to_recall(:warn, "Slow cache generation", {
|
|
163
|
+
key: truncate_key(key),
|
|
164
|
+
duration_ms: duration_ms
|
|
165
|
+
})
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def handle_fetch_hit(event_data)
|
|
170
|
+
payload = event_data[:payload]
|
|
171
|
+
key = payload[:key]
|
|
172
|
+
store = payload[:store]
|
|
173
|
+
|
|
174
|
+
# === FLUX: Fetch hit metrics ===
|
|
175
|
+
send_to_flux(:increment, 'rails.cache.fetch_hits', 1, { store: store })
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def handle_write(event_data)
|
|
179
|
+
payload = event_data[:payload]
|
|
180
|
+
key = payload[:key]
|
|
181
|
+
store = payload[:store]
|
|
182
|
+
duration_ms = event_data[:duration_ms]
|
|
183
|
+
|
|
184
|
+
# === PULSE: Cache write span ===
|
|
185
|
+
send_to_pulse(event_data, {
|
|
186
|
+
name: "cache.write",
|
|
187
|
+
category: 'cache.write',
|
|
188
|
+
attributes: {
|
|
189
|
+
key: truncate_key(key),
|
|
190
|
+
store: store
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
# === FLUX: Metrics ===
|
|
195
|
+
send_to_flux(:increment, 'rails.cache.writes', 1, { store: store })
|
|
196
|
+
send_to_flux(:timing, 'rails.cache.write_ms', duration_ms, { store: store })
|
|
197
|
+
|
|
198
|
+
# === REFLEX: Breadcrumb ===
|
|
199
|
+
add_breadcrumb(
|
|
200
|
+
"Cache write: #{truncate_key(key)}",
|
|
201
|
+
category: 'cache.write',
|
|
202
|
+
level: :debug,
|
|
203
|
+
data: {
|
|
204
|
+
key: truncate_key(key),
|
|
205
|
+
duration_ms: duration_ms
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def handle_write_multi(event_data)
|
|
211
|
+
payload = event_data[:payload]
|
|
212
|
+
keys = payload[:key]&.keys || []
|
|
213
|
+
store = payload[:store]
|
|
214
|
+
duration_ms = event_data[:duration_ms]
|
|
215
|
+
|
|
216
|
+
# === FLUX: Metrics ===
|
|
217
|
+
send_to_flux(:increment, 'rails.cache.multi_writes', 1, { store: store })
|
|
218
|
+
send_to_flux(:histogram, 'rails.cache.multi_write_keys', keys.size, { store: store })
|
|
219
|
+
send_to_flux(:timing, 'rails.cache.write_multi_ms', duration_ms, { store: store })
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def handle_increment(event_data)
|
|
223
|
+
payload = event_data[:payload]
|
|
224
|
+
key = payload[:key]
|
|
225
|
+
amount = payload[:amount]
|
|
226
|
+
store = payload[:store]
|
|
227
|
+
|
|
228
|
+
# === FLUX: Metrics ===
|
|
229
|
+
send_to_flux(:increment, 'rails.cache.increments', 1, { store: store })
|
|
230
|
+
|
|
231
|
+
# === REFLEX: Breadcrumb ===
|
|
232
|
+
add_breadcrumb(
|
|
233
|
+
"Cache increment: #{truncate_key(key)} by #{amount}",
|
|
234
|
+
category: 'cache.counter',
|
|
235
|
+
level: :debug,
|
|
236
|
+
data: { key: truncate_key(key), amount: amount }
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def handle_decrement(event_data)
|
|
241
|
+
payload = event_data[:payload]
|
|
242
|
+
key = payload[:key]
|
|
243
|
+
amount = payload[:amount]
|
|
244
|
+
store = payload[:store]
|
|
245
|
+
|
|
246
|
+
# === FLUX: Metrics ===
|
|
247
|
+
send_to_flux(:increment, 'rails.cache.decrements', 1, { store: store })
|
|
248
|
+
|
|
249
|
+
# === REFLEX: Breadcrumb ===
|
|
250
|
+
add_breadcrumb(
|
|
251
|
+
"Cache decrement: #{truncate_key(key)} by #{amount}",
|
|
252
|
+
category: 'cache.counter',
|
|
253
|
+
level: :debug,
|
|
254
|
+
data: { key: truncate_key(key), amount: amount }
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def handle_delete(event_data)
|
|
259
|
+
payload = event_data[:payload]
|
|
260
|
+
key = payload[:key]
|
|
261
|
+
store = payload[:store]
|
|
262
|
+
|
|
263
|
+
# === FLUX: Metrics ===
|
|
264
|
+
send_to_flux(:increment, 'rails.cache.deletes', 1, { store: store })
|
|
265
|
+
|
|
266
|
+
# === REFLEX: Breadcrumb ===
|
|
267
|
+
add_breadcrumb(
|
|
268
|
+
"Cache delete: #{truncate_key(key)}",
|
|
269
|
+
category: 'cache.delete',
|
|
270
|
+
level: :debug,
|
|
271
|
+
data: { key: truncate_key(key) }
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def handle_delete_multi(event_data)
|
|
276
|
+
payload = event_data[:payload]
|
|
277
|
+
keys = payload[:key] || []
|
|
278
|
+
store = payload[:store]
|
|
279
|
+
|
|
280
|
+
# === FLUX: Metrics ===
|
|
281
|
+
send_to_flux(:increment, 'rails.cache.multi_deletes', 1, { store: store })
|
|
282
|
+
send_to_flux(:histogram, 'rails.cache.multi_delete_keys', keys.size, { store: store })
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def handle_delete_matched(event_data)
|
|
286
|
+
payload = event_data[:payload]
|
|
287
|
+
pattern = payload[:key]
|
|
288
|
+
store = payload[:store]
|
|
289
|
+
|
|
290
|
+
# === FLUX: Metrics ===
|
|
291
|
+
send_to_flux(:increment, 'rails.cache.pattern_deletes', 1, { store: store })
|
|
292
|
+
|
|
293
|
+
# === RECALL: Log pattern delete ===
|
|
294
|
+
send_to_recall(:info, "Cache pattern delete", {
|
|
295
|
+
pattern: pattern.to_s,
|
|
296
|
+
store: store
|
|
297
|
+
})
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def handle_cleanup(event_data)
|
|
301
|
+
payload = event_data[:payload]
|
|
302
|
+
store = payload[:store]
|
|
303
|
+
size = payload[:size]
|
|
304
|
+
|
|
305
|
+
# === FLUX: Metrics ===
|
|
306
|
+
send_to_flux(:increment, 'rails.cache.cleanups', 1, { store: store })
|
|
307
|
+
send_to_flux(:gauge, 'rails.cache.size_before_cleanup', size, { store: store })
|
|
308
|
+
|
|
309
|
+
# === RECALL: Log cleanup ===
|
|
310
|
+
send_to_recall(:info, "Cache cleanup", {
|
|
311
|
+
store: store,
|
|
312
|
+
size_before: size
|
|
313
|
+
})
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def handle_prune(event_data)
|
|
317
|
+
payload = event_data[:payload]
|
|
318
|
+
store = payload[:store]
|
|
319
|
+
target_size = payload[:key]
|
|
320
|
+
from_size = payload[:from]
|
|
321
|
+
|
|
322
|
+
# === FLUX: Metrics ===
|
|
323
|
+
send_to_flux(:increment, 'rails.cache.prunes', 1, { store: store })
|
|
324
|
+
send_to_flux(:gauge, 'rails.cache.prune_from', from_size, { store: store })
|
|
325
|
+
send_to_flux(:gauge, 'rails.cache.prune_target', target_size, { store: store })
|
|
326
|
+
|
|
327
|
+
# === RECALL: Log prune ===
|
|
328
|
+
send_to_recall(:info, "Cache prune", {
|
|
329
|
+
store: store,
|
|
330
|
+
from_bytes: from_size,
|
|
331
|
+
target_bytes: target_size
|
|
332
|
+
})
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def handle_exist(event_data)
|
|
336
|
+
payload = event_data[:payload]
|
|
337
|
+
key = payload[:key]
|
|
338
|
+
store = payload[:store]
|
|
339
|
+
|
|
340
|
+
# === FLUX: Metrics ===
|
|
341
|
+
send_to_flux(:increment, 'rails.cache.exist_checks', 1, { store: store })
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def handle_serializer_fallback(event_data)
|
|
345
|
+
payload = event_data[:payload]
|
|
346
|
+
serializer = payload[:serializer]
|
|
347
|
+
fallback = payload[:fallback]
|
|
348
|
+
duration_ms = event_data[:duration_ms]
|
|
349
|
+
|
|
350
|
+
# === RECALL: Warn about serializer fallback ===
|
|
351
|
+
send_to_recall(:warn, "Message serializer fallback", {
|
|
352
|
+
serializer: serializer.to_s,
|
|
353
|
+
fallback: fallback.to_s,
|
|
354
|
+
duration_ms: duration_ms
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
# === REFLEX: Breadcrumb ===
|
|
358
|
+
add_breadcrumb(
|
|
359
|
+
"Serializer fallback: #{serializer} -> #{fallback}",
|
|
360
|
+
category: 'cache.serializer',
|
|
361
|
+
level: :warning,
|
|
362
|
+
data: {
|
|
363
|
+
serializer: serializer.to_s,
|
|
364
|
+
fallback: fallback.to_s
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# === FLUX: Metrics ===
|
|
369
|
+
send_to_flux(:increment, 'rails.cache.serializer_fallbacks', 1, {
|
|
370
|
+
serializer: serializer.to_s,
|
|
371
|
+
fallback: fallback.to_s
|
|
372
|
+
})
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def truncate_key(key, max_length = 100)
|
|
376
|
+
return '' if key.nil?
|
|
377
|
+
|
|
378
|
+
key_str = key.to_s
|
|
379
|
+
key_str.length > max_length ? "#{key_str[0, max_length]}..." : key_str
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Rails
|
|
5
|
+
class Configuration
|
|
6
|
+
# Product routing - which products receive which events
|
|
7
|
+
attr_accessor :pulse_enabled # APM tracing
|
|
8
|
+
attr_accessor :recall_enabled # Structured logging
|
|
9
|
+
attr_accessor :reflex_enabled # Error tracking
|
|
10
|
+
attr_accessor :flux_enabled # Metrics
|
|
11
|
+
attr_accessor :nerve_enabled # Job monitoring
|
|
12
|
+
|
|
13
|
+
# Collector settings
|
|
14
|
+
attr_accessor :action_controller_enabled
|
|
15
|
+
attr_accessor :action_view_enabled
|
|
16
|
+
attr_accessor :active_record_enabled
|
|
17
|
+
attr_accessor :active_job_enabled
|
|
18
|
+
attr_accessor :action_cable_enabled
|
|
19
|
+
attr_accessor :action_mailer_enabled
|
|
20
|
+
attr_accessor :active_storage_enabled
|
|
21
|
+
attr_accessor :cache_enabled
|
|
22
|
+
|
|
23
|
+
# Analyzer settings
|
|
24
|
+
attr_accessor :n_plus_one_detection
|
|
25
|
+
attr_accessor :slow_query_threshold_ms
|
|
26
|
+
attr_accessor :cache_efficiency_tracking
|
|
27
|
+
|
|
28
|
+
# Filtering
|
|
29
|
+
attr_accessor :ignored_actions # Controller actions to ignore
|
|
30
|
+
attr_accessor :ignored_sql_patterns # SQL patterns to ignore (e.g., SCHEMA queries)
|
|
31
|
+
attr_accessor :ignored_job_classes # Job classes to ignore
|
|
32
|
+
|
|
33
|
+
# Sampling
|
|
34
|
+
attr_accessor :sample_rate # 0.0 to 1.0, percentage of events to capture
|
|
35
|
+
|
|
36
|
+
# Performance
|
|
37
|
+
attr_accessor :async_processing # Process events asynchronously
|
|
38
|
+
attr_accessor :batch_size # Batch events before sending
|
|
39
|
+
attr_accessor :flush_interval_ms # Flush interval for batched events
|
|
40
|
+
|
|
41
|
+
def initialize
|
|
42
|
+
# Default: all products enabled (respects main SDK settings)
|
|
43
|
+
@pulse_enabled = true
|
|
44
|
+
@recall_enabled = true
|
|
45
|
+
@reflex_enabled = true
|
|
46
|
+
@flux_enabled = true
|
|
47
|
+
@nerve_enabled = true
|
|
48
|
+
|
|
49
|
+
# Default: all collectors enabled
|
|
50
|
+
@action_controller_enabled = true
|
|
51
|
+
@action_view_enabled = true
|
|
52
|
+
@active_record_enabled = true
|
|
53
|
+
@active_job_enabled = true
|
|
54
|
+
@action_cable_enabled = true
|
|
55
|
+
@action_mailer_enabled = true
|
|
56
|
+
@active_storage_enabled = true
|
|
57
|
+
@cache_enabled = true
|
|
58
|
+
|
|
59
|
+
# Default: analyzers enabled with sensible thresholds
|
|
60
|
+
@n_plus_one_detection = true
|
|
61
|
+
@slow_query_threshold_ms = 100
|
|
62
|
+
@cache_efficiency_tracking = true
|
|
63
|
+
|
|
64
|
+
# Default: ignore common noise
|
|
65
|
+
@ignored_actions = []
|
|
66
|
+
@ignored_sql_patterns = [
|
|
67
|
+
/\ASELECT.*FROM.*schema_migrations/i,
|
|
68
|
+
/\ASELECT.*FROM.*ar_internal_metadata/i
|
|
69
|
+
]
|
|
70
|
+
@ignored_job_classes = []
|
|
71
|
+
|
|
72
|
+
# Default: capture everything
|
|
73
|
+
@sample_rate = 1.0
|
|
74
|
+
|
|
75
|
+
# Default: async with batching
|
|
76
|
+
@async_processing = true
|
|
77
|
+
@batch_size = 100
|
|
78
|
+
@flush_interval_ms = 1000
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def pulse_effectively_enabled?
|
|
82
|
+
@pulse_enabled && BrainzLab.configuration&.pulse_effectively_enabled?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def recall_effectively_enabled?
|
|
86
|
+
@recall_enabled && BrainzLab.configuration&.recall_effectively_enabled?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def reflex_effectively_enabled?
|
|
90
|
+
@reflex_enabled && BrainzLab.configuration&.reflex_effectively_enabled?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def flux_effectively_enabled?
|
|
94
|
+
@flux_enabled && BrainzLab.configuration&.flux_enabled
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def nerve_effectively_enabled?
|
|
98
|
+
@nerve_enabled
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def should_sample?
|
|
102
|
+
return true if @sample_rate >= 1.0
|
|
103
|
+
return false if @sample_rate <= 0.0
|
|
104
|
+
|
|
105
|
+
rand < @sample_rate
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ignored_action?(controller, action)
|
|
109
|
+
@ignored_actions.include?("#{controller}##{action}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def ignored_sql?(sql)
|
|
113
|
+
@ignored_sql_patterns.any? { |pattern| sql.match?(pattern) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def ignored_job?(job_class)
|
|
117
|
+
@ignored_job_classes.include?(job_class.to_s)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Rails
|
|
5
|
+
# Routes Rails instrumentation events to appropriate BrainzLab products
|
|
6
|
+
# Each event can be sent to multiple products based on its type
|
|
7
|
+
class EventRouter
|
|
8
|
+
attr_reader :configuration, :collectors
|
|
9
|
+
|
|
10
|
+
def initialize(configuration)
|
|
11
|
+
@configuration = configuration
|
|
12
|
+
@collectors = initialize_collectors
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def route(event_data)
|
|
16
|
+
event_name = event_data[:name]
|
|
17
|
+
collector = collector_for(event_name)
|
|
18
|
+
|
|
19
|
+
return unless collector
|
|
20
|
+
|
|
21
|
+
# Collector processes and routes to products
|
|
22
|
+
collector.process(event_data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def initialize_collectors
|
|
28
|
+
{
|
|
29
|
+
action_controller: Collectors::ActionController.new(@configuration),
|
|
30
|
+
action_view: Collectors::ActionView.new(@configuration),
|
|
31
|
+
active_record: Collectors::ActiveRecord.new(@configuration),
|
|
32
|
+
active_job: Collectors::ActiveJob.new(@configuration),
|
|
33
|
+
action_cable: Collectors::ActionCable.new(@configuration),
|
|
34
|
+
action_mailer: Collectors::ActionMailer.new(@configuration),
|
|
35
|
+
active_storage: Collectors::ActiveStorage.new(@configuration),
|
|
36
|
+
cache: Collectors::Cache.new(@configuration)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def collector_for(event_name)
|
|
41
|
+
case event_name
|
|
42
|
+
when /\.action_controller$/
|
|
43
|
+
@collectors[:action_controller] if @configuration.action_controller_enabled
|
|
44
|
+
when /\.action_view$/
|
|
45
|
+
@collectors[:action_view] if @configuration.action_view_enabled
|
|
46
|
+
when /\.active_record$/
|
|
47
|
+
@collectors[:active_record] if @configuration.active_record_enabled
|
|
48
|
+
when /\.active_job$/
|
|
49
|
+
@collectors[:active_job] if @configuration.active_job_enabled
|
|
50
|
+
when /\.action_cable$/
|
|
51
|
+
@collectors[:action_cable] if @configuration.action_cable_enabled
|
|
52
|
+
when /\.action_mailer$/, /\.action_mailbox$/
|
|
53
|
+
@collectors[:action_mailer] if @configuration.action_mailer_enabled
|
|
54
|
+
when /\.active_storage$/
|
|
55
|
+
@collectors[:active_storage] if @configuration.active_storage_enabled
|
|
56
|
+
when /cache.*\.active_support$/, /message_serializer_fallback\.active_support$/
|
|
57
|
+
@collectors[:cache] if @configuration.cache_enabled
|
|
58
|
+
when /\.action_dispatch$/
|
|
59
|
+
@collectors[:action_controller] if @configuration.action_controller_enabled
|
|
60
|
+
when /deprecation\.rails$/, /\.railties$/
|
|
61
|
+
# Route deprecations to Recall for logging
|
|
62
|
+
@collectors[:action_controller] if @configuration.action_controller_enabled
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Rails
|
|
5
|
+
# Railtie for automatic Rails integration
|
|
6
|
+
# Automatically starts instrumentation when Rails boots
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
config.brainzlab_rails = ActiveSupport::OrderedOptions.new
|
|
9
|
+
|
|
10
|
+
# Initialize after Rails and BrainzLab SDK are configured
|
|
11
|
+
initializer 'brainzlab_rails.setup', after: :load_config_initializers do |app|
|
|
12
|
+
# Configure from Rails config if provided
|
|
13
|
+
BrainzLab::Rails.configure do |config|
|
|
14
|
+
rails_config = app.config.brainzlab_rails
|
|
15
|
+
|
|
16
|
+
# Copy any Rails-level configuration
|
|
17
|
+
config.pulse_enabled = rails_config.pulse_enabled if rails_config.key?(:pulse_enabled)
|
|
18
|
+
config.recall_enabled = rails_config.recall_enabled if rails_config.key?(:recall_enabled)
|
|
19
|
+
config.reflex_enabled = rails_config.reflex_enabled if rails_config.key?(:reflex_enabled)
|
|
20
|
+
config.flux_enabled = rails_config.flux_enabled if rails_config.key?(:flux_enabled)
|
|
21
|
+
config.nerve_enabled = rails_config.nerve_enabled if rails_config.key?(:nerve_enabled)
|
|
22
|
+
|
|
23
|
+
# Collector settings
|
|
24
|
+
config.action_controller_enabled = rails_config.action_controller_enabled if rails_config.key?(:action_controller_enabled)
|
|
25
|
+
config.action_view_enabled = rails_config.action_view_enabled if rails_config.key?(:action_view_enabled)
|
|
26
|
+
config.active_record_enabled = rails_config.active_record_enabled if rails_config.key?(:active_record_enabled)
|
|
27
|
+
config.active_job_enabled = rails_config.active_job_enabled if rails_config.key?(:active_job_enabled)
|
|
28
|
+
config.action_cable_enabled = rails_config.action_cable_enabled if rails_config.key?(:action_cable_enabled)
|
|
29
|
+
config.action_mailer_enabled = rails_config.action_mailer_enabled if rails_config.key?(:action_mailer_enabled)
|
|
30
|
+
config.active_storage_enabled = rails_config.active_storage_enabled if rails_config.key?(:active_storage_enabled)
|
|
31
|
+
config.cache_enabled = rails_config.cache_enabled if rails_config.key?(:cache_enabled)
|
|
32
|
+
|
|
33
|
+
# Analyzer settings
|
|
34
|
+
config.n_plus_one_detection = rails_config.n_plus_one_detection if rails_config.key?(:n_plus_one_detection)
|
|
35
|
+
config.slow_query_threshold_ms = rails_config.slow_query_threshold_ms if rails_config.key?(:slow_query_threshold_ms)
|
|
36
|
+
config.cache_efficiency_tracking = rails_config.cache_efficiency_tracking if rails_config.key?(:cache_efficiency_tracking)
|
|
37
|
+
|
|
38
|
+
# Filtering
|
|
39
|
+
config.ignored_actions = rails_config.ignored_actions if rails_config.key?(:ignored_actions)
|
|
40
|
+
config.ignored_sql_patterns = rails_config.ignored_sql_patterns if rails_config.key?(:ignored_sql_patterns)
|
|
41
|
+
config.ignored_job_classes = rails_config.ignored_job_classes if rails_config.key?(:ignored_job_classes)
|
|
42
|
+
|
|
43
|
+
# Sampling
|
|
44
|
+
config.sample_rate = rails_config.sample_rate if rails_config.key?(:sample_rate)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Start instrumentation when Rails is ready
|
|
49
|
+
config.after_initialize do
|
|
50
|
+
# Only start if BrainzLab SDK is configured
|
|
51
|
+
# Check for either secret_key (legacy) or any product enabled with auto-provisioning
|
|
52
|
+
if sdk_configured?
|
|
53
|
+
BrainzLab::Rails.start!
|
|
54
|
+
::Rails.logger.info '[BrainzLab::Rails] Instrumentation started (SDK Rails events delegated)'
|
|
55
|
+
else
|
|
56
|
+
::Rails.logger.warn '[BrainzLab::Rails] BrainzLab SDK not configured, skipping instrumentation'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.sdk_configured?
|
|
61
|
+
config = BrainzLab.configuration
|
|
62
|
+
return false unless config
|
|
63
|
+
|
|
64
|
+
# Check for secret_key (set directly or by auto-provisioning)
|
|
65
|
+
return true if config.secret_key.to_s.strip.length.positive?
|
|
66
|
+
|
|
67
|
+
# Check if any product can auto-provision
|
|
68
|
+
# Products with auto_provision + master_key will provision on first use
|
|
69
|
+
products_with_auto = %i[recall reflex pulse flux]
|
|
70
|
+
has_auto_provision = products_with_auto.any? do |product|
|
|
71
|
+
enabled = config.send("#{product}_enabled")
|
|
72
|
+
can_provision = config.send("#{product}_auto_provision") &&
|
|
73
|
+
config.send("#{product}_master_key").to_s.strip.length.positive? &&
|
|
74
|
+
config.app_name.to_s.strip.length.positive?
|
|
75
|
+
enabled && can_provision
|
|
76
|
+
end
|
|
77
|
+
return true if has_auto_provision
|
|
78
|
+
|
|
79
|
+
# Check for direct API keys
|
|
80
|
+
direct_keys = {
|
|
81
|
+
reflex: :reflex_api_key,
|
|
82
|
+
pulse: :pulse_api_key,
|
|
83
|
+
flux: :flux_api_key
|
|
84
|
+
}
|
|
85
|
+
direct_keys.any? do |product, key_method|
|
|
86
|
+
config.send("#{product}_enabled") &&
|
|
87
|
+
config.send(key_method).to_s.strip.length.positive?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Expose configuration in Rails console
|
|
92
|
+
console do
|
|
93
|
+
puts '[BrainzLab::Rails] Rails instrumentation active'
|
|
94
|
+
puts " Hit rate: #{BrainzLab::Rails.subscriber&.event_router&.collectors&.dig(:cache)&.instance_variable_get(:@cache_analyzer)&.hit_rate}%" rescue nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|