fluyenta-ruby 0.1.14

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.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. metadata +251 -0
@@ -0,0 +1,559 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ class ActiveRecord
6
+ SCHEMA_QUERIES = %w[SCHEMA EXPLAIN].freeze
7
+ INTERNAL_TABLES = %w[pg_ information_schema sqlite_ mysql.].freeze
8
+
9
+ # Thresholds for slow query detection (in milliseconds)
10
+ SLOW_QUERY_THRESHOLD = 100
11
+ VERY_SLOW_QUERY_THRESHOLD = 1000
12
+
13
+ # N+1 detection settings
14
+ N_PLUS_ONE_THRESHOLD = 5 # queries to same table in single request
15
+ N_PLUS_ONE_WINDOW = 50 # max queries to track per request
16
+
17
+ class << self
18
+ def install!
19
+ return unless defined?(::ActiveRecord)
20
+ return if @installed
21
+
22
+ install_sql_subscriber!
23
+ install_instantiation_subscriber!
24
+ install_transaction_subscribers!
25
+ install_strict_loading_subscriber!
26
+ install_deprecated_association_subscriber!
27
+
28
+ @installed = true
29
+ BrainzLab.debug_log('ActiveRecord instrumentation installed (sql, instantiation, transactions, strict_loading)')
30
+ end
31
+
32
+ def installed?
33
+ @installed == true
34
+ end
35
+
36
+ private
37
+
38
+ # ============================================
39
+ # SQL Query Instrumentation
40
+ # ============================================
41
+ def install_sql_subscriber!
42
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
43
+ event = ActiveSupport::Notifications::Event.new(*args)
44
+ next if skip_query?(event.payload)
45
+
46
+ handle_sql_event(event)
47
+ end
48
+ end
49
+
50
+ def handle_sql_event(event)
51
+ payload = event.payload
52
+ duration = event.duration.round(2)
53
+
54
+ # Record breadcrumb for Reflex (enhanced)
55
+ record_sql_breadcrumb(event, duration)
56
+
57
+ # Add span to Pulse (APM)
58
+ record_sql_span(event, duration)
59
+
60
+ # Log slow queries to Recall
61
+ log_slow_query(event, duration) if duration >= SLOW_QUERY_THRESHOLD
62
+
63
+ # Track for N+1 detection
64
+ track_query_for_n_plus_one(event)
65
+ rescue StandardError => e
66
+ BrainzLab.debug_log("ActiveRecord SQL instrumentation failed: #{e.message}")
67
+ end
68
+
69
+ def record_sql_breadcrumb(event, duration)
70
+ payload = event.payload
71
+ sql = payload[:sql]
72
+ name = payload[:name] || 'SQL'
73
+ operation = extract_operation(sql)
74
+
75
+ message = if payload[:cached]
76
+ "#{name} (cached)"
77
+ else
78
+ "#{name} (#{duration}ms)"
79
+ end
80
+
81
+ # Determine level based on duration
82
+ level = case duration
83
+ when 0...SLOW_QUERY_THRESHOLD then :info
84
+ when SLOW_QUERY_THRESHOLD...VERY_SLOW_QUERY_THRESHOLD then :warning
85
+ else :error
86
+ end
87
+
88
+ BrainzLab::Reflex.add_breadcrumb(
89
+ message,
90
+ category: "db.#{operation}",
91
+ level: level,
92
+ data: {
93
+ sql: truncate_sql(sql),
94
+ duration_ms: duration,
95
+ cached: payload[:cached] || false,
96
+ async: payload[:async] || false,
97
+ row_count: payload[:row_count],
98
+ affected_rows: payload[:affected_rows],
99
+ connection_name: extract_connection_name(payload[:connection])
100
+ }.compact
101
+ )
102
+ end
103
+
104
+ def record_sql_span(event, duration)
105
+ # Only add spans if Pulse is enabled and there's an active trace
106
+ return unless BrainzLab.configuration.pulse_effectively_enabled?
107
+
108
+ tracer = BrainzLab::Pulse.tracer
109
+ return unless tracer.current_trace
110
+
111
+ payload = event.payload
112
+ sql = payload[:sql]
113
+ operation = extract_operation(sql)
114
+ name = payload[:name] || 'SQL'
115
+
116
+ # Build span data
117
+ span_data = {
118
+ span_id: SecureRandom.uuid,
119
+ name: "db.#{operation}",
120
+ kind: 'db',
121
+ started_at: event.time,
122
+ ended_at: event.end,
123
+ duration_ms: duration,
124
+ error: false,
125
+ data: {
126
+ 'db.system' => extract_adapter_name(payload[:connection]),
127
+ 'db.name' => extract_database_name(payload[:connection]),
128
+ 'db.statement' => truncate_sql(sql, 1000),
129
+ 'db.operation' => operation,
130
+ 'db.query_name' => name,
131
+ 'db.cached' => payload[:cached] || false,
132
+ 'db.async' => payload[:async] || false,
133
+ 'db.row_count' => payload[:row_count],
134
+ 'db.affected_rows' => payload[:affected_rows]
135
+ }.compact
136
+ }
137
+
138
+ tracer.current_spans << span_data
139
+ end
140
+
141
+ def log_slow_query(event, duration)
142
+ return unless BrainzLab.configuration.recall_effectively_enabled?
143
+
144
+ payload = event.payload
145
+ sql = payload[:sql]
146
+ operation = extract_operation(sql)
147
+ name = payload[:name] || 'SQL'
148
+
149
+ level = duration >= VERY_SLOW_QUERY_THRESHOLD ? :error : :warn
150
+
151
+ BrainzLab::Recall.send(
152
+ level,
153
+ "Slow SQL query: #{name} (#{duration}ms)",
154
+ sql: truncate_sql(sql, 2000),
155
+ duration_ms: duration,
156
+ operation: operation,
157
+ cached: payload[:cached] || false,
158
+ row_count: payload[:row_count],
159
+ affected_rows: payload[:affected_rows],
160
+ connection_name: extract_connection_name(payload[:connection]),
161
+ threshold_exceeded: duration >= VERY_SLOW_QUERY_THRESHOLD ? 'critical' : 'warning'
162
+ )
163
+ end
164
+
165
+ # ============================================
166
+ # N+1 Query Detection
167
+ # ============================================
168
+ def track_query_for_n_plus_one(event)
169
+ return unless BrainzLab.configuration.reflex_effectively_enabled?
170
+
171
+ payload = event.payload
172
+ sql = payload[:sql].to_s
173
+
174
+ # Extract table name from SELECT queries
175
+ table = extract_table_from_sql(sql)
176
+ return unless table
177
+
178
+ # Track queries per table in current request
179
+ query_tracker = Thread.current[:brainzlab_query_tracker] ||= {}
180
+ query_tracker[table] ||= { count: 0, queries: [] }
181
+
182
+ tracker = query_tracker[table]
183
+ tracker[:count] += 1
184
+
185
+ # Store sample queries (limited)
186
+ if tracker[:queries].size < 3
187
+ tracker[:queries] << truncate_sql(sql, 200)
188
+ end
189
+
190
+ # Detect N+1 pattern
191
+ if tracker[:count] == N_PLUS_ONE_THRESHOLD
192
+ report_n_plus_one(table, tracker)
193
+ end
194
+ end
195
+
196
+ def report_n_plus_one(table, tracker)
197
+ BrainzLab::Reflex.add_breadcrumb(
198
+ "Potential N+1 detected: #{tracker[:count]}+ queries to '#{table}'",
199
+ category: 'db.n_plus_one',
200
+ level: :warning,
201
+ data: {
202
+ table: table,
203
+ query_count: tracker[:count],
204
+ sample_queries: tracker[:queries]
205
+ }
206
+ )
207
+
208
+ if BrainzLab.configuration.recall_effectively_enabled?
209
+ BrainzLab::Recall.warn(
210
+ "Potential N+1 query detected",
211
+ table: table,
212
+ query_count: tracker[:count],
213
+ sample_queries: tracker[:queries]
214
+ )
215
+ end
216
+ end
217
+
218
+ def clear_n_plus_one_tracker!
219
+ Thread.current[:brainzlab_query_tracker] = nil
220
+ end
221
+
222
+ # ============================================
223
+ # Record Instantiation (for N+1 metrics)
224
+ # ============================================
225
+ def install_instantiation_subscriber!
226
+ ActiveSupport::Notifications.subscribe('instantiation.active_record') do |*args|
227
+ event = ActiveSupport::Notifications::Event.new(*args)
228
+ handle_instantiation_event(event)
229
+ end
230
+ end
231
+
232
+ def handle_instantiation_event(event)
233
+ payload = event.payload
234
+ record_count = payload[:record_count]
235
+ class_name = payload[:class_name]
236
+ duration = event.duration.round(2)
237
+
238
+ # Track instantiation metrics for Pulse
239
+ if BrainzLab.configuration.pulse_effectively_enabled?
240
+ tracer = BrainzLab::Pulse.tracer
241
+ if tracer.current_trace
242
+ span_data = {
243
+ span_id: SecureRandom.uuid,
244
+ name: "db.instantiate.#{class_name}",
245
+ kind: 'db',
246
+ started_at: event.time,
247
+ ended_at: event.end,
248
+ duration_ms: duration,
249
+ error: false,
250
+ data: {
251
+ 'db.operation' => 'instantiate',
252
+ 'db.model' => class_name,
253
+ 'db.record_count' => record_count
254
+ }
255
+ }
256
+
257
+ tracer.current_spans << span_data
258
+ end
259
+ end
260
+
261
+ # Add breadcrumb for large instantiations
262
+ if record_count >= 100
263
+ BrainzLab::Reflex.add_breadcrumb(
264
+ "Instantiated #{record_count} #{class_name} records",
265
+ category: 'db.instantiate',
266
+ level: record_count >= 1000 ? :warning : :info,
267
+ data: {
268
+ class_name: class_name,
269
+ record_count: record_count,
270
+ duration_ms: duration
271
+ }
272
+ )
273
+ end
274
+ rescue StandardError => e
275
+ BrainzLab.debug_log("ActiveRecord instantiation instrumentation failed: #{e.message}")
276
+ end
277
+
278
+ # ============================================
279
+ # Transaction Tracking
280
+ # ============================================
281
+ def install_transaction_subscribers!
282
+ # Track transaction start
283
+ ActiveSupport::Notifications.subscribe('start_transaction.active_record') do |*args|
284
+ event = ActiveSupport::Notifications::Event.new(*args)
285
+ handle_transaction_start(event)
286
+ end
287
+
288
+ # Track transaction completion
289
+ ActiveSupport::Notifications.subscribe('transaction.active_record') do |*args|
290
+ event = ActiveSupport::Notifications::Event.new(*args)
291
+ handle_transaction_complete(event)
292
+ end
293
+ end
294
+
295
+ def handle_transaction_start(event)
296
+ # Store transaction start time for duration calculation
297
+ transaction = event.payload[:transaction]
298
+ return unless transaction
299
+
300
+ Thread.current[:brainzlab_transaction_starts] ||= {}
301
+ Thread.current[:brainzlab_transaction_starts][transaction.object_id] = event.time
302
+ rescue StandardError => e
303
+ BrainzLab.debug_log("ActiveRecord transaction start instrumentation failed: #{e.message}")
304
+ end
305
+
306
+ def handle_transaction_complete(event)
307
+ payload = event.payload
308
+ transaction = payload[:transaction]
309
+ outcome = payload[:outcome] # :commit, :rollback, :restart, :incomplete
310
+
311
+ # Calculate duration from stored start time
312
+ starts = Thread.current[:brainzlab_transaction_starts] || {}
313
+ start_time = starts.delete(transaction&.object_id) || event.time
314
+ duration = ((event.end - start_time) * 1000).round(2)
315
+
316
+ connection_name = extract_connection_name(payload[:connection])
317
+
318
+ # Add breadcrumb
319
+ level = case outcome
320
+ when :commit then :info
321
+ when :rollback then :warning
322
+ when :restart, :incomplete then :error
323
+ else :info
324
+ end
325
+
326
+ BrainzLab::Reflex.add_breadcrumb(
327
+ "Transaction #{outcome} (#{duration}ms)",
328
+ category: 'db.transaction',
329
+ level: level,
330
+ data: {
331
+ outcome: outcome.to_s,
332
+ duration_ms: duration,
333
+ connection_name: connection_name
334
+ }.compact
335
+ )
336
+
337
+ # Add Pulse span
338
+ if BrainzLab.configuration.pulse_effectively_enabled?
339
+ tracer = BrainzLab::Pulse.tracer
340
+ if tracer.current_trace
341
+ span_data = {
342
+ span_id: SecureRandom.uuid,
343
+ name: 'db.transaction',
344
+ kind: 'db',
345
+ started_at: start_time,
346
+ ended_at: event.end,
347
+ duration_ms: duration,
348
+ error: outcome != :commit,
349
+ data: {
350
+ 'db.operation' => 'transaction',
351
+ 'db.transaction.outcome' => outcome.to_s,
352
+ 'db.name' => connection_name
353
+ }.compact
354
+ }
355
+
356
+ tracer.current_spans << span_data
357
+ end
358
+ end
359
+
360
+ # Log rollbacks and errors to Recall
361
+ if outcome != :commit && BrainzLab.configuration.recall_effectively_enabled?
362
+ log_level = outcome == :rollback ? :warn : :error
363
+ BrainzLab::Recall.send(
364
+ log_level,
365
+ "Database transaction #{outcome}",
366
+ outcome: outcome.to_s,
367
+ duration_ms: duration,
368
+ connection_name: connection_name
369
+ )
370
+ end
371
+ rescue StandardError => e
372
+ BrainzLab.debug_log("ActiveRecord transaction instrumentation failed: #{e.message}")
373
+ end
374
+
375
+ # ============================================
376
+ # Strict Loading Violation Tracking
377
+ # ============================================
378
+ def install_strict_loading_subscriber!
379
+ ActiveSupport::Notifications.subscribe('strict_loading_violation.active_record') do |*args|
380
+ event = ActiveSupport::Notifications::Event.new(*args)
381
+ handle_strict_loading_violation(event)
382
+ end
383
+ end
384
+
385
+ def handle_strict_loading_violation(event)
386
+ payload = event.payload
387
+ owner = payload[:owner]
388
+ reflection = payload[:reflection]
389
+
390
+ owner_class = owner.is_a?(Class) ? owner.name : owner.class.name
391
+ association_name = reflection.respond_to?(:name) ? reflection.name : reflection.to_s
392
+
393
+ # Add breadcrumb
394
+ BrainzLab::Reflex.add_breadcrumb(
395
+ "Strict loading violation: #{owner_class}##{association_name}",
396
+ category: 'db.strict_loading',
397
+ level: :warning,
398
+ data: {
399
+ owner_class: owner_class,
400
+ association: association_name.to_s,
401
+ reflection_type: reflection.class.name
402
+ }
403
+ )
404
+
405
+ # Log to Recall
406
+ if BrainzLab.configuration.recall_effectively_enabled?
407
+ BrainzLab::Recall.warn(
408
+ "Strict loading violation detected",
409
+ owner_class: owner_class,
410
+ association: association_name.to_s,
411
+ message: "Attempted to lazily load #{association_name} on #{owner_class} with strict_loading enabled"
412
+ )
413
+ end
414
+ rescue StandardError => e
415
+ BrainzLab.debug_log("ActiveRecord strict loading instrumentation failed: #{e.message}")
416
+ end
417
+
418
+ # ============================================
419
+ # Deprecated Association Tracking
420
+ # Fired when a deprecated association is accessed
421
+ # ============================================
422
+ def install_deprecated_association_subscriber!
423
+ ActiveSupport::Notifications.subscribe('deprecated_association.active_record') do |*args|
424
+ event = ActiveSupport::Notifications::Event.new(*args)
425
+ handle_deprecated_association(event)
426
+ end
427
+ end
428
+
429
+ def handle_deprecated_association(event)
430
+ payload = event.payload
431
+ owner = payload[:owner]
432
+ reflection = payload[:reflection]
433
+ message = payload[:message]
434
+
435
+ owner_class = owner.is_a?(Class) ? owner.name : owner.class.name
436
+ association_name = reflection.respond_to?(:name) ? reflection.name : reflection.to_s
437
+
438
+ # Add breadcrumb
439
+ BrainzLab::Reflex.add_breadcrumb(
440
+ "Deprecated association: #{owner_class}##{association_name}",
441
+ category: 'db.deprecated_association',
442
+ level: :warning,
443
+ data: {
444
+ owner_class: owner_class,
445
+ association: association_name.to_s,
446
+ message: message&.slice(0, 200)
447
+ }.compact
448
+ )
449
+
450
+ # Log to Recall
451
+ if BrainzLab.configuration.recall_effectively_enabled?
452
+ BrainzLab::Recall.warn(
453
+ "Deprecated association accessed",
454
+ owner_class: owner_class,
455
+ association: association_name.to_s,
456
+ message: message
457
+ )
458
+ end
459
+ rescue StandardError => e
460
+ BrainzLab.debug_log("ActiveRecord deprecated association instrumentation failed: #{e.message}")
461
+ end
462
+
463
+ # ============================================
464
+ # Helper Methods
465
+ # ============================================
466
+ def extract_operation(sql)
467
+ return 'query' unless sql
468
+
469
+ case sql.to_s.strip
470
+ when /\ASELECT/i then 'select'
471
+ when /\AINSERT/i then 'insert'
472
+ when /\AUPDATE/i then 'update'
473
+ when /\ADELETE/i then 'delete'
474
+ when /\ABEGIN/i, /\ASTART TRANSACTION/i then 'transaction.begin'
475
+ when /\ACOMMIT/i then 'transaction.commit'
476
+ when /\AROLLBACK TO SAVEPOINT/i then 'savepoint.rollback'
477
+ when /\AROLLBACK/i then 'transaction.rollback'
478
+ when /\ARELEASE SAVEPOINT/i then 'savepoint.release'
479
+ when /\ASAVEPOINT/i then 'savepoint'
480
+ else 'query'
481
+ end
482
+ end
483
+
484
+ def extract_table_from_sql(sql)
485
+ # Extract table name from SELECT queries for N+1 detection
486
+ # Handles: SELECT ... FROM table_name, SELECT ... FROM "table_name", etc.
487
+ return nil unless sql =~ /\ASELECT/i
488
+
489
+ if sql =~ /FROM\s+["'`]?(\w+)["'`]?/i
490
+ ::Regexp.last_match(1)
491
+ end
492
+ end
493
+
494
+ def skip_query?(payload)
495
+ # Skip schema queries
496
+ return true if SCHEMA_QUERIES.include?(payload[:name])
497
+
498
+ # Skip internal/system table queries
499
+ sql = payload[:sql].to_s.downcase
500
+ return true if INTERNAL_TABLES.any? { |t| sql.include?(t) }
501
+
502
+ # Skip if no SQL (shouldn't happen but be safe)
503
+ return true if payload[:sql].nil? || payload[:sql].empty?
504
+
505
+ false
506
+ end
507
+
508
+ def extract_connection_name(connection)
509
+ return nil unless connection
510
+
511
+ # Rails 8.1+ uses db_config.name on the pool
512
+ if connection.respond_to?(:pool)
513
+ pool = connection.pool
514
+ pool.db_config.name if pool.respond_to?(:db_config) && pool.db_config.respond_to?(:name)
515
+ elsif connection.respond_to?(:db_config) && connection.db_config.respond_to?(:name)
516
+ connection.db_config.name
517
+ end
518
+ rescue StandardError
519
+ nil
520
+ end
521
+
522
+ def extract_adapter_name(connection)
523
+ return nil unless connection
524
+
525
+ if connection.respond_to?(:adapter_name)
526
+ connection.adapter_name.downcase
527
+ elsif connection.respond_to?(:pool) && connection.pool.respond_to?(:db_config)
528
+ connection.pool.db_config.adapter
529
+ end
530
+ rescue StandardError
531
+ nil
532
+ end
533
+
534
+ def extract_database_name(connection)
535
+ return nil unless connection
536
+
537
+ if connection.respond_to?(:pool) && connection.pool.respond_to?(:db_config)
538
+ connection.pool.db_config.database
539
+ elsif connection.respond_to?(:current_database)
540
+ connection.current_database
541
+ end
542
+ rescue StandardError
543
+ nil
544
+ end
545
+
546
+ def truncate_sql(sql, max_length = 500)
547
+ return nil unless sql
548
+
549
+ truncated = sql.to_s.gsub(/\s+/, ' ').strip
550
+ if truncated.length > max_length
551
+ "#{truncated[0, max_length - 3]}..."
552
+ else
553
+ truncated
554
+ end
555
+ end
556
+ end
557
+ end
558
+ end
559
+ end