brainzlab 0.1.34 → 0.1.35
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/beacon/client.rb +4 -6
- data/lib/brainzlab/configuration.rb +301 -272
- data/lib/brainzlab/cortex/client.rb +4 -6
- data/lib/brainzlab/debug.rb +6 -6
- data/lib/brainzlab/dendrite/client.rb +4 -6
- data/lib/brainzlab/development/logger.rb +2 -2
- data/lib/brainzlab/development/store.rb +2 -4
- data/lib/brainzlab/devtools/middleware/error_page.rb +1 -1
- data/lib/brainzlab/errors.rb +5 -12
- data/lib/brainzlab/instrumentation/action_cable.rb +4 -4
- data/lib/brainzlab/instrumentation/action_controller.rb +12 -12
- data/lib/brainzlab/instrumentation/action_dispatch.rb +1 -1
- data/lib/brainzlab/instrumentation/action_mailbox.rb +7 -13
- data/lib/brainzlab/instrumentation/action_view.rb +29 -33
- data/lib/brainzlab/instrumentation/active_job.rb +6 -10
- data/lib/brainzlab/instrumentation/active_record.rb +21 -24
- data/lib/brainzlab/instrumentation/active_storage.rb +4 -4
- data/lib/brainzlab/instrumentation/active_support_cache.rb +6 -8
- data/lib/brainzlab/instrumentation/aws.rb +10 -7
- data/lib/brainzlab/instrumentation/grape.rb +3 -1
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +1 -1
- data/lib/brainzlab/instrumentation/railties.rb +2 -2
- data/lib/brainzlab/nerve/client.rb +4 -6
- data/lib/brainzlab/pulse/client.rb +1 -1
- data/lib/brainzlab/pulse.rb +5 -1
- data/lib/brainzlab/rails/railtie.rb +1 -3
- data/lib/brainzlab/reflex/client.rb +1 -1
- data/lib/brainzlab/sentinel/client.rb +4 -6
- data/lib/brainzlab/synapse/client.rb +4 -6
- data/lib/brainzlab/testing/event_store.rb +3 -3
- data/lib/brainzlab/testing/matchers.rb +4 -4
- data/lib/brainzlab/testing.rb +5 -3
- data/lib/brainzlab/vault/client.rb +4 -6
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +3 -3
- data/lib/fluyenta-ruby.rb +1 -1
- metadata +1 -1
|
@@ -184,9 +184,9 @@ module BrainzLab
|
|
|
184
184
|
BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{structured_error.message}")
|
|
185
185
|
|
|
186
186
|
# Call on_error callback if configured
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
return unless @config.on_error
|
|
188
|
+
|
|
189
|
+
@config.on_error.call(structured_error, { service: 'Cortex', operation: operation })
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
def handle_response_error(response, operation)
|
|
@@ -196,9 +196,7 @@ module BrainzLab
|
|
|
196
196
|
BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{structured_error.message}")
|
|
197
197
|
|
|
198
198
|
# Call on_error callback if configured
|
|
199
|
-
|
|
200
|
-
@config.on_error.call(structured_error, { service: 'Cortex', operation: operation })
|
|
201
|
-
end
|
|
199
|
+
@config.on_error&.call(structured_error, { service: 'Cortex', operation: operation })
|
|
202
200
|
|
|
203
201
|
structured_error
|
|
204
202
|
end
|
data/lib/brainzlab/debug.rb
CHANGED
|
@@ -172,7 +172,7 @@ module BrainzLab
|
|
|
172
172
|
|
|
173
173
|
def format_message(message, level:, **data)
|
|
174
174
|
timestamp = format_timestamp
|
|
175
|
-
prefix = colorize(
|
|
175
|
+
prefix = colorize('[BrainzLab]', :bold, :blue)
|
|
176
176
|
level_badge = format_level(level)
|
|
177
177
|
data_str = data.empty? ? '' : " #{format_data(data)}"
|
|
178
178
|
|
|
@@ -181,8 +181,8 @@ module BrainzLab
|
|
|
181
181
|
|
|
182
182
|
def format_arrow_message(direction, service, message, level: :info)
|
|
183
183
|
timestamp = format_timestamp
|
|
184
|
-
prefix = colorize(
|
|
185
|
-
arrow = direction == :out ? colorize(
|
|
184
|
+
prefix = colorize('[BrainzLab]', :bold, :blue)
|
|
185
|
+
arrow = direction == :out ? colorize('->', :cyan) : colorize('<-', :green)
|
|
186
186
|
service_name = colorize(service.to_s.capitalize, :magenta)
|
|
187
187
|
|
|
188
188
|
"#{prefix} #{timestamp} #{arrow} #{service_name} #{message}"
|
|
@@ -242,7 +242,7 @@ module BrainzLab
|
|
|
242
242
|
other_keys = data.keys.reject { |k| %i[message timestamp level].include?(k.to_sym) }
|
|
243
243
|
if other_keys.any?
|
|
244
244
|
key_summary = other_keys.take(3).map { |k| "#{k}: #{format_value(data[k])}" }.join(', ')
|
|
245
|
-
key_summary +=
|
|
245
|
+
key_summary += ', ...' if other_keys.length > 3
|
|
246
246
|
summary_parts << "(#{key_summary})"
|
|
247
247
|
end
|
|
248
248
|
|
|
@@ -253,7 +253,7 @@ module BrainzLab
|
|
|
253
253
|
return '' unless str
|
|
254
254
|
|
|
255
255
|
str = str.to_s
|
|
256
|
-
str.length > length ? "#{str[0..length - 4]}..." : str
|
|
256
|
+
str.length > length ? "#{str[0..(length - 4)]}..." : str
|
|
257
257
|
end
|
|
258
258
|
|
|
259
259
|
def status_message(status)
|
|
@@ -293,7 +293,7 @@ module BrainzLab
|
|
|
293
293
|
else config.logger.info(strip_colors(output))
|
|
294
294
|
end
|
|
295
295
|
else
|
|
296
|
-
|
|
296
|
+
warn output
|
|
297
297
|
end
|
|
298
298
|
end
|
|
299
299
|
|
|
@@ -227,9 +227,9 @@ module BrainzLab
|
|
|
227
227
|
BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{structured_error.message}")
|
|
228
228
|
|
|
229
229
|
# Call on_error callback if configured
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
return unless @config.on_error
|
|
231
|
+
|
|
232
|
+
@config.on_error.call(structured_error, { service: 'Dendrite', operation: operation })
|
|
233
233
|
end
|
|
234
234
|
|
|
235
235
|
def handle_response_error(response, operation)
|
|
@@ -239,9 +239,7 @@ module BrainzLab
|
|
|
239
239
|
BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{structured_error.message}")
|
|
240
240
|
|
|
241
241
|
# Call on_error callback if configured
|
|
242
|
-
|
|
243
|
-
@config.on_error.call(structured_error, { service: 'Dendrite', operation: operation })
|
|
244
|
-
end
|
|
242
|
+
@config.on_error&.call(structured_error, { service: 'Dendrite', operation: operation })
|
|
245
243
|
|
|
246
244
|
structured_error
|
|
247
245
|
end
|
|
@@ -82,8 +82,8 @@ module BrainzLab
|
|
|
82
82
|
when 'log'
|
|
83
83
|
level = payload[:level]&.to_sym
|
|
84
84
|
level_color = COLORS[level] || COLORS[:info]
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
"#{colorize("[#{level&.upcase}]", level_color)} #{payload[:message]}"
|
|
86
|
+
|
|
87
87
|
when 'error'
|
|
88
88
|
"#{payload[:error_class]}: #{payload[:message]}"
|
|
89
89
|
when 'trace'
|
|
@@ -89,10 +89,8 @@ module BrainzLab
|
|
|
89
89
|
private
|
|
90
90
|
|
|
91
91
|
def db
|
|
92
|
-
@db ||=
|
|
93
|
-
|
|
94
|
-
database.results_as_hash = false
|
|
95
|
-
end
|
|
92
|
+
@db ||= SQLite3::Database.new(@db_path).tap do |database|
|
|
93
|
+
database.results_as_hash = false
|
|
96
94
|
end
|
|
97
95
|
end
|
|
98
96
|
|
|
@@ -93,7 +93,7 @@ module BrainzLab
|
|
|
93
93
|
.gsub(' ', ' ')
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
def collect_debug_data_from_info(env, info,
|
|
96
|
+
def collect_debug_data_from_info(env, info, _status = 500)
|
|
97
97
|
context = defined?(BrainzLab::Context) ? BrainzLab::Context.current : nil
|
|
98
98
|
collector_data = Data::Collector.get_request_data
|
|
99
99
|
|
data/lib/brainzlab/errors.rb
CHANGED
|
@@ -53,13 +53,6 @@ module BrainzLab
|
|
|
53
53
|
super(message)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# Format the error as a detailed string with hints and documentation links.
|
|
57
|
-
#
|
|
58
|
-
# @return [String] Formatted error message
|
|
59
|
-
def to_s
|
|
60
|
-
super
|
|
61
|
-
end
|
|
62
|
-
|
|
63
56
|
# Return a detailed formatted version of the error with hints and documentation links.
|
|
64
57
|
# Use this method when you want the full structured output.
|
|
65
58
|
#
|
|
@@ -70,12 +63,12 @@ module BrainzLab
|
|
|
70
63
|
|
|
71
64
|
parts = ["#{self.class.name}: #{base_msg}"]
|
|
72
65
|
|
|
73
|
-
parts <<
|
|
66
|
+
parts << '' << "Hint: #{hint}" if hint
|
|
74
67
|
parts << "Docs: #{docs_url}" if docs_url
|
|
75
68
|
parts << "Code: #{code}" if code
|
|
76
69
|
|
|
77
70
|
if context && !context.empty?
|
|
78
|
-
parts <<
|
|
71
|
+
parts << '' << 'Context:'
|
|
79
72
|
context.each do |key, value|
|
|
80
73
|
parts << " #{key}: #{value}"
|
|
81
74
|
end
|
|
@@ -124,7 +117,7 @@ module BrainzLab
|
|
|
124
117
|
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
125
118
|
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/configuration"
|
|
126
119
|
code ||= 'configuration_error'
|
|
127
|
-
super
|
|
120
|
+
super
|
|
128
121
|
end
|
|
129
122
|
end
|
|
130
123
|
|
|
@@ -141,7 +134,7 @@ module BrainzLab
|
|
|
141
134
|
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
142
135
|
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/authentication"
|
|
143
136
|
code ||= 'authentication_error'
|
|
144
|
-
super
|
|
137
|
+
super
|
|
145
138
|
end
|
|
146
139
|
end
|
|
147
140
|
|
|
@@ -158,7 +151,7 @@ module BrainzLab
|
|
|
158
151
|
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
159
152
|
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/troubleshooting#connection-issues"
|
|
160
153
|
code ||= 'connection_error'
|
|
161
|
-
super
|
|
154
|
+
super
|
|
162
155
|
end
|
|
163
156
|
end
|
|
164
157
|
|
|
@@ -91,7 +91,7 @@ module BrainzLab
|
|
|
91
91
|
duration = event.duration.round(2)
|
|
92
92
|
|
|
93
93
|
channel_class = payload[:channel_class]
|
|
94
|
-
|
|
94
|
+
payload[:data]
|
|
95
95
|
via = payload[:via]
|
|
96
96
|
|
|
97
97
|
# Record breadcrumb
|
|
@@ -187,7 +187,7 @@ module BrainzLab
|
|
|
187
187
|
# Log rejection to Recall
|
|
188
188
|
if BrainzLab.configuration.recall_effectively_enabled?
|
|
189
189
|
BrainzLab::Recall.warn(
|
|
190
|
-
|
|
190
|
+
'ActionCable subscription rejected',
|
|
191
191
|
channel: channel_class
|
|
192
192
|
)
|
|
193
193
|
end
|
|
@@ -210,7 +210,7 @@ module BrainzLab
|
|
|
210
210
|
duration = event.duration.round(2)
|
|
211
211
|
|
|
212
212
|
broadcasting = payload[:broadcasting]
|
|
213
|
-
|
|
213
|
+
payload[:message]
|
|
214
214
|
coder = payload[:coder]
|
|
215
215
|
|
|
216
216
|
# Record breadcrumb
|
|
@@ -236,7 +236,7 @@ module BrainzLab
|
|
|
236
236
|
# ============================================
|
|
237
237
|
# Span Recording Helpers
|
|
238
238
|
# ============================================
|
|
239
|
-
def record_action_span(event, channel_class, action, duration,
|
|
239
|
+
def record_action_span(event, channel_class, action, duration, _data)
|
|
240
240
|
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
241
241
|
|
|
242
242
|
tracer = BrainzLab::Pulse.tracer
|
|
@@ -58,7 +58,7 @@ module BrainzLab
|
|
|
58
58
|
method = payload[:method]
|
|
59
59
|
path = payload[:path]
|
|
60
60
|
format = payload[:format]
|
|
61
|
-
|
|
61
|
+
payload[:params]
|
|
62
62
|
|
|
63
63
|
# Skip health check endpoints
|
|
64
64
|
return if excluded_path?(path)
|
|
@@ -99,12 +99,12 @@ module BrainzLab
|
|
|
99
99
|
payload = event.payload
|
|
100
100
|
duration = event.duration.round(2)
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
payload[:controller]
|
|
103
|
+
payload[:action]
|
|
104
104
|
status = payload[:status]
|
|
105
|
-
|
|
105
|
+
payload[:method]
|
|
106
106
|
path = payload[:path]
|
|
107
|
-
|
|
107
|
+
payload[:format]
|
|
108
108
|
|
|
109
109
|
# Skip health check endpoints
|
|
110
110
|
return if excluded_path?(path)
|
|
@@ -157,7 +157,7 @@ module BrainzLab
|
|
|
157
157
|
)
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
def record_request_trace(event, payload,
|
|
160
|
+
def record_request_trace(event, payload, _duration)
|
|
161
161
|
# Only record if Pulse is enabled and no trace is already active
|
|
162
162
|
# (middleware should handle this, but this is a fallback)
|
|
163
163
|
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
@@ -265,7 +265,7 @@ module BrainzLab
|
|
|
265
265
|
# Also log to Recall - halted callbacks can indicate auth issues
|
|
266
266
|
if BrainzLab.configuration.recall_effectively_enabled?
|
|
267
267
|
BrainzLab::Recall.info(
|
|
268
|
-
|
|
268
|
+
'Request halted by before_action filter',
|
|
269
269
|
filter: filter.to_s
|
|
270
270
|
)
|
|
271
271
|
end
|
|
@@ -307,7 +307,7 @@ module BrainzLab
|
|
|
307
307
|
# Log to Recall - this is a security-relevant event
|
|
308
308
|
if BrainzLab.configuration.recall_effectively_enabled?
|
|
309
309
|
BrainzLab::Recall.warn(
|
|
310
|
-
|
|
310
|
+
'Unpermitted parameters rejected',
|
|
311
311
|
unpermitted_keys: keys,
|
|
312
312
|
controller: context[:controller],
|
|
313
313
|
action: context[:action]
|
|
@@ -362,7 +362,7 @@ module BrainzLab
|
|
|
362
362
|
filename = payload[:filename]
|
|
363
363
|
|
|
364
364
|
BrainzLab::Reflex.add_breadcrumb(
|
|
365
|
-
"Sending data#{
|
|
365
|
+
"Sending data#{": #{filename}" if filename}",
|
|
366
366
|
category: 'http.data',
|
|
367
367
|
level: :info,
|
|
368
368
|
data: {
|
|
@@ -391,7 +391,7 @@ module BrainzLab
|
|
|
391
391
|
type = payload[:type]
|
|
392
392
|
|
|
393
393
|
BrainzLab::Reflex.add_breadcrumb(
|
|
394
|
-
"Streaming#{
|
|
394
|
+
"Streaming#{": #{filename}" if filename}",
|
|
395
395
|
category: 'http.stream',
|
|
396
396
|
level: :info,
|
|
397
397
|
data: {
|
|
@@ -597,7 +597,7 @@ module BrainzLab
|
|
|
597
597
|
# Add breadcrumb
|
|
598
598
|
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
599
599
|
BrainzLab::Reflex.add_breadcrumb(
|
|
600
|
-
|
|
600
|
+
'Rate limit triggered',
|
|
601
601
|
category: 'security.rate_limit',
|
|
602
602
|
level: :warning,
|
|
603
603
|
data: {
|
|
@@ -609,7 +609,7 @@ module BrainzLab
|
|
|
609
609
|
# Log to Recall - rate limiting is security-relevant
|
|
610
610
|
if BrainzLab.configuration.recall_effectively_enabled?
|
|
611
611
|
BrainzLab::Recall.warn(
|
|
612
|
-
|
|
612
|
+
'Rate limit triggered',
|
|
613
613
|
path: payload[:request]&.path,
|
|
614
614
|
ip: payload[:request]&.remote_ip
|
|
615
615
|
)
|
|
@@ -4,8 +4,8 @@ module BrainzLab
|
|
|
4
4
|
module Instrumentation
|
|
5
5
|
class ActionMailbox
|
|
6
6
|
# Thresholds for slow processing (in milliseconds)
|
|
7
|
-
SLOW_PROCESSING_THRESHOLD = 1000
|
|
8
|
-
VERY_SLOW_PROCESSING_THRESHOLD = 5000
|
|
7
|
+
SLOW_PROCESSING_THRESHOLD = 1000 # 1 second
|
|
8
|
+
VERY_SLOW_PROCESSING_THRESHOLD = 5000 # 5 seconds
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
11
|
def install!
|
|
@@ -102,7 +102,7 @@ module BrainzLab
|
|
|
102
102
|
started_at: event.time,
|
|
103
103
|
ended_at: event.end,
|
|
104
104
|
duration_ms: duration,
|
|
105
|
-
error:
|
|
105
|
+
error: %w[bounced failed].include?(status),
|
|
106
106
|
data: {
|
|
107
107
|
'mailbox.class' => mailbox_class,
|
|
108
108
|
'mailbox.email_id' => email_id,
|
|
@@ -123,7 +123,7 @@ module BrainzLab
|
|
|
123
123
|
return unless BrainzLab.configuration.recall_effectively_enabled?
|
|
124
124
|
|
|
125
125
|
# Determine log level based on status and duration
|
|
126
|
-
if
|
|
126
|
+
if %w[bounced failed].include?(status)
|
|
127
127
|
BrainzLab::Recall.error(
|
|
128
128
|
"Mailbox processing failed: #{mailbox_class}",
|
|
129
129
|
mailbox: mailbox_class,
|
|
@@ -154,9 +154,7 @@ module BrainzLab
|
|
|
154
154
|
def extract_from(inbound_email)
|
|
155
155
|
return nil unless inbound_email
|
|
156
156
|
|
|
157
|
-
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:from)
|
|
158
|
-
Array(inbound_email.mail.from).first
|
|
159
|
-
end
|
|
157
|
+
Array(inbound_email.mail.from).first if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:from)
|
|
160
158
|
rescue StandardError
|
|
161
159
|
nil
|
|
162
160
|
end
|
|
@@ -164,9 +162,7 @@ module BrainzLab
|
|
|
164
162
|
def extract_to(inbound_email)
|
|
165
163
|
return nil unless inbound_email
|
|
166
164
|
|
|
167
|
-
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:to)
|
|
168
|
-
Array(inbound_email.mail.to).first
|
|
169
|
-
end
|
|
165
|
+
Array(inbound_email.mail.to).first if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:to)
|
|
170
166
|
rescue StandardError
|
|
171
167
|
nil
|
|
172
168
|
end
|
|
@@ -174,9 +170,7 @@ module BrainzLab
|
|
|
174
170
|
def extract_subject(inbound_email)
|
|
175
171
|
return nil unless inbound_email
|
|
176
172
|
|
|
177
|
-
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:subject)
|
|
178
|
-
inbound_email.mail.subject
|
|
179
|
-
end
|
|
173
|
+
inbound_email.mail.subject if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:subject)
|
|
180
174
|
rescue StandardError
|
|
181
175
|
nil
|
|
182
176
|
end
|
|
@@ -115,17 +115,13 @@ module BrainzLab
|
|
|
115
115
|
template_name = extract_template_name(identifier)
|
|
116
116
|
|
|
117
117
|
# Add breadcrumb for significant collections
|
|
118
|
-
if count >= COLLECTION_TRACKING_THRESHOLD || duration >= BrainzLab.configuration.slow_render_threshold
|
|
119
|
-
record_collection_breadcrumb(template_name, duration, count, cache_hits)
|
|
120
|
-
end
|
|
118
|
+
record_collection_breadcrumb(template_name, duration, count, cache_hits) if count >= COLLECTION_TRACKING_THRESHOLD || duration >= BrainzLab.configuration.slow_render_threshold
|
|
121
119
|
|
|
122
120
|
# Add span to Pulse
|
|
123
121
|
record_collection_span(event, template_name, duration, count, cache_hits)
|
|
124
122
|
|
|
125
123
|
# Log slow collection renders to Recall
|
|
126
|
-
if duration >= BrainzLab.configuration.slow_render_threshold
|
|
127
|
-
log_slow_collection_render(template_name, duration, count, cache_hits)
|
|
128
|
-
end
|
|
124
|
+
log_slow_collection_render(template_name, duration, count, cache_hits) if duration >= BrainzLab.configuration.slow_render_threshold
|
|
129
125
|
|
|
130
126
|
# Track potential N+1 view pattern (many partials rendered)
|
|
131
127
|
track_collection_performance(template_name, count, duration) if count >= 50
|
|
@@ -244,7 +240,7 @@ module BrainzLab
|
|
|
244
240
|
else :error
|
|
245
241
|
end
|
|
246
242
|
|
|
247
|
-
cache_info = cache_hits
|
|
243
|
+
cache_info = cache_hits.positive? ? " (#{cache_hits}/#{count} cached)" : ''
|
|
248
244
|
|
|
249
245
|
BrainzLab::Reflex.add_breadcrumb(
|
|
250
246
|
"Render collection: #{template_name} x#{count}#{cache_info} (#{duration}ms)",
|
|
@@ -255,7 +251,7 @@ module BrainzLab
|
|
|
255
251
|
count: count,
|
|
256
252
|
cache_hits: cache_hits,
|
|
257
253
|
duration_ms: duration,
|
|
258
|
-
avg_per_item_ms: count
|
|
254
|
+
avg_per_item_ms: count.positive? ? (duration / count).round(2) : nil
|
|
259
255
|
}.compact
|
|
260
256
|
)
|
|
261
257
|
end
|
|
@@ -279,7 +275,7 @@ module BrainzLab
|
|
|
279
275
|
'view.template' => template_name,
|
|
280
276
|
'view.count' => count,
|
|
281
277
|
'view.cache_hits' => cache_hits,
|
|
282
|
-
'view.avg_per_item_ms' => count
|
|
278
|
+
'view.avg_per_item_ms' => count.positive? ? (duration / count).round(2) : nil
|
|
283
279
|
}.compact
|
|
284
280
|
}
|
|
285
281
|
|
|
@@ -317,41 +313,41 @@ module BrainzLab
|
|
|
317
313
|
count: count,
|
|
318
314
|
cache_hits: cache_hits,
|
|
319
315
|
duration_ms: duration,
|
|
320
|
-
avg_per_item_ms: count
|
|
316
|
+
avg_per_item_ms: count.positive? ? (duration / count).round(2) : nil,
|
|
321
317
|
threshold_exceeded: duration >= BrainzLab.configuration.very_slow_render_threshold ? 'critical' : 'warning'
|
|
322
318
|
)
|
|
323
319
|
end
|
|
324
320
|
|
|
325
321
|
def track_collection_performance(template_name, count, duration)
|
|
326
|
-
avg_per_item = count
|
|
322
|
+
avg_per_item = count.positive? ? (duration / count).round(2) : 0
|
|
327
323
|
|
|
328
324
|
# If average time per item is high, this might indicate N+1 in the partial
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
count: count,
|
|
338
|
-
duration_ms: duration,
|
|
339
|
-
avg_per_item_ms: avg_per_item,
|
|
340
|
-
suggestion: 'Consider caching, eager loading, or optimizing the partial'
|
|
341
|
-
}
|
|
342
|
-
)
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
if BrainzLab.configuration.recall_effectively_enabled?
|
|
346
|
-
BrainzLab::Recall.warn(
|
|
347
|
-
"Collection partial may have N+1 or performance issue",
|
|
325
|
+
return unless avg_per_item > 5 # More than 5ms per item is suspicious
|
|
326
|
+
|
|
327
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
328
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
329
|
+
"Slow collection items: #{template_name} (#{avg_per_item}ms/item)",
|
|
330
|
+
category: 'view.performance',
|
|
331
|
+
level: :warning,
|
|
332
|
+
data: {
|
|
348
333
|
template: template_name,
|
|
349
334
|
count: count,
|
|
350
335
|
duration_ms: duration,
|
|
351
|
-
avg_per_item_ms: avg_per_item
|
|
352
|
-
|
|
353
|
-
|
|
336
|
+
avg_per_item_ms: avg_per_item,
|
|
337
|
+
suggestion: 'Consider caching, eager loading, or optimizing the partial'
|
|
338
|
+
}
|
|
339
|
+
)
|
|
354
340
|
end
|
|
341
|
+
|
|
342
|
+
return unless BrainzLab.configuration.recall_effectively_enabled?
|
|
343
|
+
|
|
344
|
+
BrainzLab::Recall.warn(
|
|
345
|
+
'Collection partial may have N+1 or performance issue',
|
|
346
|
+
template: template_name,
|
|
347
|
+
count: count,
|
|
348
|
+
duration_ms: duration,
|
|
349
|
+
avg_per_item_ms: avg_per_item
|
|
350
|
+
)
|
|
355
351
|
end
|
|
356
352
|
|
|
357
353
|
# ============================================
|
|
@@ -4,8 +4,8 @@ module BrainzLab
|
|
|
4
4
|
module Instrumentation
|
|
5
5
|
class ActiveJob
|
|
6
6
|
# Thresholds for slow job detection (in milliseconds)
|
|
7
|
-
SLOW_JOB_THRESHOLD = 5000
|
|
8
|
-
VERY_SLOW_JOB_THRESHOLD = 30_000
|
|
7
|
+
SLOW_JOB_THRESHOLD = 5000 # 5 seconds
|
|
8
|
+
VERY_SLOW_JOB_THRESHOLD = 30_000 # 30 seconds
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
11
|
def install!
|
|
@@ -95,7 +95,7 @@ module BrainzLab
|
|
|
95
95
|
scheduled_in = scheduled_at ? ((scheduled_at - Time.now) / 60).round(1) : nil
|
|
96
96
|
|
|
97
97
|
BrainzLab::Reflex.add_breadcrumb(
|
|
98
|
-
"Job scheduled: #{job_class}#{
|
|
98
|
+
"Job scheduled: #{job_class}#{" (in #{scheduled_in}min)" if scheduled_in}",
|
|
99
99
|
category: 'job.schedule',
|
|
100
100
|
level: :info,
|
|
101
101
|
data: {
|
|
@@ -242,14 +242,12 @@ module BrainzLab
|
|
|
242
242
|
|
|
243
243
|
# Calculate queue wait time if enqueued_at is available
|
|
244
244
|
queue_wait_ms = nil
|
|
245
|
-
if job.enqueued_at
|
|
246
|
-
queue_wait_ms = ((event.time - job.enqueued_at) * 1000).round(2)
|
|
247
|
-
end
|
|
245
|
+
queue_wait_ms = ((event.time - job.enqueued_at) * 1000).round(2) if job.enqueued_at
|
|
248
246
|
|
|
249
247
|
# Record breadcrumb
|
|
250
248
|
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
251
249
|
BrainzLab::Reflex.add_breadcrumb(
|
|
252
|
-
"Job started: #{job_class}#{
|
|
250
|
+
"Job started: #{job_class}#{" (attempt #{executions})" if executions > 1}",
|
|
253
251
|
category: 'job.start',
|
|
254
252
|
level: :info,
|
|
255
253
|
data: {
|
|
@@ -293,9 +291,7 @@ module BrainzLab
|
|
|
293
291
|
job_starts = Thread.current[:brainzlab_job_starts] || {}
|
|
294
292
|
start_info = job_starts.delete(job_id) || {}
|
|
295
293
|
queue_wait_ms = nil
|
|
296
|
-
if start_info[:enqueued_at]
|
|
297
|
-
queue_wait_ms = ((start_info[:started_at] - start_info[:enqueued_at]) * 1000).round(2)
|
|
298
|
-
end
|
|
294
|
+
queue_wait_ms = ((start_info[:started_at] - start_info[:enqueued_at]) * 1000).round(2) if start_info[:enqueued_at]
|
|
299
295
|
|
|
300
296
|
# Determine level based on outcome and duration
|
|
301
297
|
level = if exception
|
|
@@ -80,7 +80,6 @@ module BrainzLab
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def handle_sql_event(event)
|
|
83
|
-
payload = event.payload
|
|
84
83
|
duration = event.duration.round(2)
|
|
85
84
|
|
|
86
85
|
# Record breadcrumb for Reflex (enhanced)
|
|
@@ -218,14 +217,12 @@ module BrainzLab
|
|
|
218
217
|
tracker[:count] += 1
|
|
219
218
|
|
|
220
219
|
# Store sample queries (limited)
|
|
221
|
-
if tracker[:queries].size < 3
|
|
222
|
-
tracker[:queries] << truncate_sql(sql, 200)
|
|
223
|
-
end
|
|
220
|
+
tracker[:queries] << truncate_sql(sql, 200) if tracker[:queries].size < 3
|
|
224
221
|
|
|
225
222
|
# Detect N+1 pattern
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
223
|
+
return unless tracker[:count] == BrainzLab.configuration.n_plus_one_threshold
|
|
224
|
+
|
|
225
|
+
report_n_plus_one(table, tracker)
|
|
229
226
|
end
|
|
230
227
|
|
|
231
228
|
def report_n_plus_one(table, tracker)
|
|
@@ -240,14 +237,14 @@ module BrainzLab
|
|
|
240
237
|
}
|
|
241
238
|
)
|
|
242
239
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
240
|
+
return unless BrainzLab.configuration.recall_effectively_enabled?
|
|
241
|
+
|
|
242
|
+
BrainzLab::Recall.warn(
|
|
243
|
+
'Potential N+1 query detected',
|
|
244
|
+
table: table,
|
|
245
|
+
query_count: tracker[:count],
|
|
246
|
+
sample_queries: tracker[:queries]
|
|
247
|
+
)
|
|
251
248
|
end
|
|
252
249
|
|
|
253
250
|
def clear_n_plus_one_tracker!
|
|
@@ -332,8 +329,8 @@ module BrainzLab
|
|
|
332
329
|
transaction = event.payload[:transaction]
|
|
333
330
|
return unless transaction
|
|
334
331
|
|
|
335
|
-
Thread.current[:brainzlab_transaction_starts] ||= {}
|
|
336
|
-
Thread.current[:brainzlab_transaction_starts][transaction
|
|
332
|
+
Thread.current[:brainzlab_transaction_starts] ||= {}.compare_by_identity
|
|
333
|
+
Thread.current[:brainzlab_transaction_starts][transaction] = event.time
|
|
337
334
|
rescue StandardError => e
|
|
338
335
|
BrainzLab.debug_log("ActiveRecord transaction start instrumentation failed: #{e.message}")
|
|
339
336
|
end
|
|
@@ -344,8 +341,8 @@ module BrainzLab
|
|
|
344
341
|
outcome = payload[:outcome] # :commit, :rollback, :restart, :incomplete
|
|
345
342
|
|
|
346
343
|
# Calculate duration from stored start time
|
|
347
|
-
starts = Thread.current[:brainzlab_transaction_starts] || {}
|
|
348
|
-
start_time = starts.delete(transaction
|
|
344
|
+
starts = Thread.current[:brainzlab_transaction_starts] || {}.compare_by_identity
|
|
345
|
+
start_time = starts.delete(transaction) || event.time
|
|
349
346
|
duration = ((event.end - start_time) * 1000).round(2)
|
|
350
347
|
|
|
351
348
|
connection_name = extract_connection_name(payload[:connection])
|
|
@@ -440,7 +437,7 @@ module BrainzLab
|
|
|
440
437
|
# Log to Recall
|
|
441
438
|
if BrainzLab.configuration.recall_effectively_enabled?
|
|
442
439
|
BrainzLab::Recall.warn(
|
|
443
|
-
|
|
440
|
+
'Strict loading violation detected',
|
|
444
441
|
owner_class: owner_class,
|
|
445
442
|
association: association_name.to_s,
|
|
446
443
|
message: "Attempted to lazily load #{association_name} on #{owner_class} with strict_loading enabled"
|
|
@@ -485,7 +482,7 @@ module BrainzLab
|
|
|
485
482
|
# Log to Recall
|
|
486
483
|
if BrainzLab.configuration.recall_effectively_enabled?
|
|
487
484
|
BrainzLab::Recall.warn(
|
|
488
|
-
|
|
485
|
+
'Deprecated association accessed',
|
|
489
486
|
owner_class: owner_class,
|
|
490
487
|
association: association_name.to_s,
|
|
491
488
|
message: message
|
|
@@ -521,9 +518,9 @@ module BrainzLab
|
|
|
521
518
|
# Handles: SELECT ... FROM table_name, SELECT ... FROM "table_name", etc.
|
|
522
519
|
return nil unless sql =~ /\ASELECT/i
|
|
523
520
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
521
|
+
return unless sql =~ /FROM\s+["'`]?(\w+)["'`]?/i
|
|
522
|
+
|
|
523
|
+
::Regexp.last_match(1)
|
|
527
524
|
end
|
|
528
525
|
|
|
529
526
|
def skip_query?(payload)
|