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.
@@ -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