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
|
@@ -6,20 +6,27 @@ module BrainzLab
|
|
|
6
6
|
SCHEMA_QUERIES = %w[SCHEMA EXPLAIN].freeze
|
|
7
7
|
INTERNAL_TABLES = %w[pg_ information_schema sqlite_ mysql.].freeze
|
|
8
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
|
+
|
|
9
17
|
class << self
|
|
10
18
|
def install!
|
|
11
19
|
return unless defined?(::ActiveRecord)
|
|
12
20
|
return if @installed
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
22
|
+
install_sql_subscriber!
|
|
23
|
+
install_instantiation_subscriber!
|
|
24
|
+
install_transaction_subscribers!
|
|
25
|
+
install_strict_loading_subscriber!
|
|
26
|
+
install_deprecated_association_subscriber!
|
|
20
27
|
|
|
21
28
|
@installed = true
|
|
22
|
-
BrainzLab.debug_log('ActiveRecord
|
|
29
|
+
BrainzLab.debug_log('ActiveRecord instrumentation installed (sql, instantiation, transactions, strict_loading)')
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
def installed?
|
|
@@ -28,16 +35,43 @@ module BrainzLab
|
|
|
28
35
|
|
|
29
36
|
private
|
|
30
37
|
|
|
31
|
-
|
|
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)
|
|
32
51
|
payload = event.payload
|
|
33
|
-
sql = payload[:sql]
|
|
34
|
-
name = payload[:name] || 'SQL'
|
|
35
52
|
duration = event.duration.round(2)
|
|
36
53
|
|
|
37
|
-
#
|
|
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'
|
|
38
73
|
operation = extract_operation(sql)
|
|
39
74
|
|
|
40
|
-
# Build breadcrumb message
|
|
41
75
|
message = if payload[:cached]
|
|
42
76
|
"#{name} (cached)"
|
|
43
77
|
else
|
|
@@ -45,12 +79,10 @@ module BrainzLab
|
|
|
45
79
|
end
|
|
46
80
|
|
|
47
81
|
# Determine level based on duration
|
|
48
|
-
level =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
else
|
|
53
|
-
:info
|
|
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
|
|
54
86
|
end
|
|
55
87
|
|
|
56
88
|
BrainzLab::Reflex.add_breadcrumb(
|
|
@@ -61,26 +93,404 @@ module BrainzLab
|
|
|
61
93
|
sql: truncate_sql(sql),
|
|
62
94
|
duration_ms: duration,
|
|
63
95
|
cached: payload[:cached] || false,
|
|
96
|
+
async: payload[:async] || false,
|
|
97
|
+
row_count: payload[:row_count],
|
|
98
|
+
affected_rows: payload[:affected_rows],
|
|
64
99
|
connection_name: extract_connection_name(payload[:connection])
|
|
65
100
|
}.compact
|
|
66
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
|
|
67
274
|
rescue StandardError => e
|
|
68
|
-
BrainzLab.debug_log("ActiveRecord
|
|
275
|
+
BrainzLab.debug_log("ActiveRecord instantiation instrumentation failed: #{e.message}")
|
|
69
276
|
end
|
|
70
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
|
+
# ============================================
|
|
71
466
|
def extract_operation(sql)
|
|
72
467
|
return 'query' unless sql
|
|
73
468
|
|
|
74
|
-
case sql.to_s.strip
|
|
469
|
+
case sql.to_s.strip
|
|
75
470
|
when /\ASELECT/i then 'select'
|
|
76
471
|
when /\AINSERT/i then 'insert'
|
|
77
472
|
when /\AUPDATE/i then 'update'
|
|
78
473
|
when /\ADELETE/i then 'delete'
|
|
79
|
-
when /\ABEGIN/i, /\
|
|
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'
|
|
80
480
|
else 'query'
|
|
81
481
|
end
|
|
82
482
|
end
|
|
83
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
|
+
|
|
84
494
|
def skip_query?(payload)
|
|
85
495
|
# Skip schema queries
|
|
86
496
|
return true if SCHEMA_QUERIES.include?(payload[:name])
|
|
@@ -99,7 +509,6 @@ module BrainzLab
|
|
|
99
509
|
return nil unless connection
|
|
100
510
|
|
|
101
511
|
# Rails 8.1+ uses db_config.name on the pool
|
|
102
|
-
# Older versions used connection_class but that's removed in Rails 8.1
|
|
103
512
|
if connection.respond_to?(:pool)
|
|
104
513
|
pool = connection.pool
|
|
105
514
|
pool.db_config.name if pool.respond_to?(:db_config) && pool.db_config.respond_to?(:name)
|
|
@@ -110,12 +519,36 @@ module BrainzLab
|
|
|
110
519
|
nil
|
|
111
520
|
end
|
|
112
521
|
|
|
113
|
-
def
|
|
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)
|
|
114
547
|
return nil unless sql
|
|
115
548
|
|
|
116
549
|
truncated = sql.to_s.gsub(/\s+/, ' ').strip
|
|
117
|
-
if truncated.length >
|
|
118
|
-
"#{truncated[0,
|
|
550
|
+
if truncated.length > max_length
|
|
551
|
+
"#{truncated[0, max_length - 3]}..."
|
|
119
552
|
else
|
|
120
553
|
truncated
|
|
121
554
|
end
|