brainzlab 0.1.35 → 0.1.36
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/CHANGELOG.md +6 -0
- data/lib/brainzlab/beacon/client.rb +6 -4
- data/lib/brainzlab/configuration.rb +272 -301
- data/lib/brainzlab/cortex/client.rb +6 -4
- data/lib/brainzlab/debug.rb +6 -6
- data/lib/brainzlab/dendrite/client.rb +6 -4
- data/lib/brainzlab/development/logger.rb +2 -2
- data/lib/brainzlab/development/store.rb +4 -2
- data/lib/brainzlab/devtools/middleware/error_page.rb +1 -1
- data/lib/brainzlab/errors.rb +12 -5
- data/lib/brainzlab/flux/client.rb +1 -1
- 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 +13 -7
- data/lib/brainzlab/instrumentation/action_view.rb +33 -29
- data/lib/brainzlab/instrumentation/active_job.rb +10 -6
- data/lib/brainzlab/instrumentation/active_record.rb +24 -21
- data/lib/brainzlab/instrumentation/active_storage.rb +4 -4
- data/lib/brainzlab/instrumentation/active_support_cache.rb +8 -6
- data/lib/brainzlab/instrumentation/grape.rb +1 -3
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +1 -1
- data/lib/brainzlab/instrumentation/railties.rb +2 -2
- data/lib/brainzlab/nerve/client.rb +6 -4
- data/lib/brainzlab/pulse/client.rb +1 -1
- data/lib/brainzlab/pulse.rb +1 -5
- data/lib/brainzlab/rails/railtie.rb +3 -1
- data/lib/brainzlab/reflex/client.rb +1 -1
- data/lib/brainzlab/sentinel/client.rb +6 -4
- data/lib/brainzlab/synapse/client.rb +6 -4
- data/lib/brainzlab/testing/event_store.rb +3 -3
- data/lib/brainzlab/testing/matchers.rb +4 -4
- data/lib/brainzlab/testing.rb +3 -5
- data/lib/brainzlab/vault/client.rb +6 -4
- 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
|
+
if @config.on_error
|
|
188
|
+
@config.on_error.call(structured_error, { service: 'Cortex', operation: operation })
|
|
189
|
+
end
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
def handle_response_error(response, operation)
|
|
@@ -196,7 +196,9 @@ 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
|
-
@config.on_error
|
|
199
|
+
if @config.on_error
|
|
200
|
+
@config.on_error.call(structured_error, { service: 'Cortex', operation: operation })
|
|
201
|
+
end
|
|
200
202
|
|
|
201
203
|
structured_error
|
|
202
204
|
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..
|
|
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
|
+
$stderr.puts 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
|
+
if @config.on_error
|
|
231
|
+
@config.on_error.call(structured_error, { service: 'Dendrite', operation: operation })
|
|
232
|
+
end
|
|
233
233
|
end
|
|
234
234
|
|
|
235
235
|
def handle_response_error(response, operation)
|
|
@@ -239,7 +239,9 @@ 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
|
-
@config.on_error
|
|
242
|
+
if @config.on_error
|
|
243
|
+
@config.on_error.call(structured_error, { service: 'Dendrite', operation: operation })
|
|
244
|
+
end
|
|
243
245
|
|
|
244
246
|
structured_error
|
|
245
247
|
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
|
-
"#{colorize("[#{level&.upcase}]", level_color)} #{payload[:message]}"
|
|
86
|
-
|
|
85
|
+
msg = "#{colorize("[#{level&.upcase}]", level_color)} #{payload[:message]}"
|
|
86
|
+
msg
|
|
87
87
|
when 'error'
|
|
88
88
|
"#{payload[:error_class]}: #{payload[:message]}"
|
|
89
89
|
when 'trace'
|
|
@@ -89,8 +89,10 @@ module BrainzLab
|
|
|
89
89
|
private
|
|
90
90
|
|
|
91
91
|
def db
|
|
92
|
-
@db ||=
|
|
93
|
-
|
|
92
|
+
@db ||= begin
|
|
93
|
+
SQLite3::Database.new(@db_path).tap do |database|
|
|
94
|
+
database.results_as_hash = false
|
|
95
|
+
end
|
|
94
96
|
end
|
|
95
97
|
end
|
|
96
98
|
|
|
@@ -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,6 +53,13 @@ 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
|
+
|
|
56
63
|
# Return a detailed formatted version of the error with hints and documentation links.
|
|
57
64
|
# Use this method when you want the full structured output.
|
|
58
65
|
#
|
|
@@ -63,12 +70,12 @@ module BrainzLab
|
|
|
63
70
|
|
|
64
71
|
parts = ["#{self.class.name}: #{base_msg}"]
|
|
65
72
|
|
|
66
|
-
parts <<
|
|
73
|
+
parts << "" << "Hint: #{hint}" if hint
|
|
67
74
|
parts << "Docs: #{docs_url}" if docs_url
|
|
68
75
|
parts << "Code: #{code}" if code
|
|
69
76
|
|
|
70
77
|
if context && !context.empty?
|
|
71
|
-
parts <<
|
|
78
|
+
parts << "" << "Context:"
|
|
72
79
|
context.each do |key, value|
|
|
73
80
|
parts << " #{key}: #{value}"
|
|
74
81
|
end
|
|
@@ -117,7 +124,7 @@ module BrainzLab
|
|
|
117
124
|
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
118
125
|
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/configuration"
|
|
119
126
|
code ||= 'configuration_error'
|
|
120
|
-
super
|
|
127
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context)
|
|
121
128
|
end
|
|
122
129
|
end
|
|
123
130
|
|
|
@@ -134,7 +141,7 @@ module BrainzLab
|
|
|
134
141
|
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
135
142
|
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/authentication"
|
|
136
143
|
code ||= 'authentication_error'
|
|
137
|
-
super
|
|
144
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context)
|
|
138
145
|
end
|
|
139
146
|
end
|
|
140
147
|
|
|
@@ -151,7 +158,7 @@ module BrainzLab
|
|
|
151
158
|
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
152
159
|
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/troubleshooting#connection-issues"
|
|
153
160
|
code ||= 'connection_error'
|
|
154
|
-
super
|
|
161
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context)
|
|
155
162
|
end
|
|
156
163
|
end
|
|
157
164
|
|
|
@@ -91,7 +91,7 @@ module BrainzLab
|
|
|
91
91
|
duration = event.duration.round(2)
|
|
92
92
|
|
|
93
93
|
channel_class = payload[:channel_class]
|
|
94
|
-
payload[:data]
|
|
94
|
+
data = 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
|
-
payload[:message]
|
|
213
|
+
message = 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
|
-
payload[:params]
|
|
61
|
+
params = 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
|
-
payload[:controller]
|
|
103
|
-
payload[:action]
|
|
102
|
+
controller = payload[:controller]
|
|
103
|
+
action = payload[:action]
|
|
104
104
|
status = payload[:status]
|
|
105
|
-
payload[:method]
|
|
105
|
+
method = payload[:method]
|
|
106
106
|
path = payload[:path]
|
|
107
|
-
payload[:format]
|
|
107
|
+
format = 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#{": #{filename}"
|
|
365
|
+
"Sending data#{filename ? ": #{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#{": #{filename}"
|
|
394
|
+
"Streaming#{filename ? ": #{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: status == 'bounced' || status == 'failed',
|
|
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 status == 'bounced' || status == 'failed'
|
|
127
127
|
BrainzLab::Recall.error(
|
|
128
128
|
"Mailbox processing failed: #{mailbox_class}",
|
|
129
129
|
mailbox: mailbox_class,
|
|
@@ -154,7 +154,9 @@ module BrainzLab
|
|
|
154
154
|
def extract_from(inbound_email)
|
|
155
155
|
return nil unless inbound_email
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:from)
|
|
158
|
+
Array(inbound_email.mail.from).first
|
|
159
|
+
end
|
|
158
160
|
rescue StandardError
|
|
159
161
|
nil
|
|
160
162
|
end
|
|
@@ -162,7 +164,9 @@ module BrainzLab
|
|
|
162
164
|
def extract_to(inbound_email)
|
|
163
165
|
return nil unless inbound_email
|
|
164
166
|
|
|
165
|
-
|
|
167
|
+
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:to)
|
|
168
|
+
Array(inbound_email.mail.to).first
|
|
169
|
+
end
|
|
166
170
|
rescue StandardError
|
|
167
171
|
nil
|
|
168
172
|
end
|
|
@@ -170,7 +174,9 @@ module BrainzLab
|
|
|
170
174
|
def extract_subject(inbound_email)
|
|
171
175
|
return nil unless inbound_email
|
|
172
176
|
|
|
173
|
-
|
|
177
|
+
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:subject)
|
|
178
|
+
inbound_email.mail.subject
|
|
179
|
+
end
|
|
174
180
|
rescue StandardError
|
|
175
181
|
nil
|
|
176
182
|
end
|
|
@@ -115,13 +115,17 @@ module BrainzLab
|
|
|
115
115
|
template_name = extract_template_name(identifier)
|
|
116
116
|
|
|
117
117
|
# Add breadcrumb for significant collections
|
|
118
|
-
|
|
118
|
+
if count >= COLLECTION_TRACKING_THRESHOLD || duration >= BrainzLab.configuration.slow_render_threshold
|
|
119
|
+
record_collection_breadcrumb(template_name, duration, count, cache_hits)
|
|
120
|
+
end
|
|
119
121
|
|
|
120
122
|
# Add span to Pulse
|
|
121
123
|
record_collection_span(event, template_name, duration, count, cache_hits)
|
|
122
124
|
|
|
123
125
|
# Log slow collection renders to Recall
|
|
124
|
-
|
|
126
|
+
if duration >= BrainzLab.configuration.slow_render_threshold
|
|
127
|
+
log_slow_collection_render(template_name, duration, count, cache_hits)
|
|
128
|
+
end
|
|
125
129
|
|
|
126
130
|
# Track potential N+1 view pattern (many partials rendered)
|
|
127
131
|
track_collection_performance(template_name, count, duration) if count >= 50
|
|
@@ -240,7 +244,7 @@ module BrainzLab
|
|
|
240
244
|
else :error
|
|
241
245
|
end
|
|
242
246
|
|
|
243
|
-
cache_info = cache_hits
|
|
247
|
+
cache_info = cache_hits > 0 ? " (#{cache_hits}/#{count} cached)" : ""
|
|
244
248
|
|
|
245
249
|
BrainzLab::Reflex.add_breadcrumb(
|
|
246
250
|
"Render collection: #{template_name} x#{count}#{cache_info} (#{duration}ms)",
|
|
@@ -251,7 +255,7 @@ module BrainzLab
|
|
|
251
255
|
count: count,
|
|
252
256
|
cache_hits: cache_hits,
|
|
253
257
|
duration_ms: duration,
|
|
254
|
-
avg_per_item_ms: count
|
|
258
|
+
avg_per_item_ms: count > 0 ? (duration / count).round(2) : nil
|
|
255
259
|
}.compact
|
|
256
260
|
)
|
|
257
261
|
end
|
|
@@ -275,7 +279,7 @@ module BrainzLab
|
|
|
275
279
|
'view.template' => template_name,
|
|
276
280
|
'view.count' => count,
|
|
277
281
|
'view.cache_hits' => cache_hits,
|
|
278
|
-
'view.avg_per_item_ms' => count
|
|
282
|
+
'view.avg_per_item_ms' => count > 0 ? (duration / count).round(2) : nil
|
|
279
283
|
}.compact
|
|
280
284
|
}
|
|
281
285
|
|
|
@@ -313,41 +317,41 @@ module BrainzLab
|
|
|
313
317
|
count: count,
|
|
314
318
|
cache_hits: cache_hits,
|
|
315
319
|
duration_ms: duration,
|
|
316
|
-
avg_per_item_ms: count
|
|
320
|
+
avg_per_item_ms: count > 0 ? (duration / count).round(2) : nil,
|
|
317
321
|
threshold_exceeded: duration >= BrainzLab.configuration.very_slow_render_threshold ? 'critical' : 'warning'
|
|
318
322
|
)
|
|
319
323
|
end
|
|
320
324
|
|
|
321
325
|
def track_collection_performance(template_name, count, duration)
|
|
322
|
-
avg_per_item = count
|
|
326
|
+
avg_per_item = count > 0 ? (duration / count).round(2) : 0
|
|
323
327
|
|
|
324
328
|
# If average time per item is high, this might indicate N+1 in the partial
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
if avg_per_item > 5 # More than 5ms per item is suspicious
|
|
330
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
331
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
332
|
+
"Slow collection items: #{template_name} (#{avg_per_item}ms/item)",
|
|
333
|
+
category: 'view.performance',
|
|
334
|
+
level: :warning,
|
|
335
|
+
data: {
|
|
336
|
+
template: template_name,
|
|
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",
|
|
333
348
|
template: template_name,
|
|
334
349
|
count: count,
|
|
335
350
|
duration_ms: duration,
|
|
336
|
-
avg_per_item_ms: avg_per_item
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
351
|
+
avg_per_item_ms: avg_per_item
|
|
352
|
+
)
|
|
353
|
+
end
|
|
340
354
|
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
|
-
)
|
|
351
355
|
end
|
|
352
356
|
|
|
353
357
|
# ============================================
|
|
@@ -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}#{" (in #{scheduled_in}min)"
|
|
98
|
+
"Job scheduled: #{job_class}#{scheduled_in ? " (in #{scheduled_in}min)" : ''}",
|
|
99
99
|
category: 'job.schedule',
|
|
100
100
|
level: :info,
|
|
101
101
|
data: {
|
|
@@ -242,12 +242,14 @@ module BrainzLab
|
|
|
242
242
|
|
|
243
243
|
# Calculate queue wait time if enqueued_at is available
|
|
244
244
|
queue_wait_ms = nil
|
|
245
|
-
|
|
245
|
+
if job.enqueued_at
|
|
246
|
+
queue_wait_ms = ((event.time - job.enqueued_at) * 1000).round(2)
|
|
247
|
+
end
|
|
246
248
|
|
|
247
249
|
# Record breadcrumb
|
|
248
250
|
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
249
251
|
BrainzLab::Reflex.add_breadcrumb(
|
|
250
|
-
"Job started: #{job_class}#{" (attempt #{executions})"
|
|
252
|
+
"Job started: #{job_class}#{executions > 1 ? " (attempt #{executions})" : ''}",
|
|
251
253
|
category: 'job.start',
|
|
252
254
|
level: :info,
|
|
253
255
|
data: {
|
|
@@ -291,7 +293,9 @@ module BrainzLab
|
|
|
291
293
|
job_starts = Thread.current[:brainzlab_job_starts] || {}
|
|
292
294
|
start_info = job_starts.delete(job_id) || {}
|
|
293
295
|
queue_wait_ms = nil
|
|
294
|
-
|
|
296
|
+
if start_info[:enqueued_at]
|
|
297
|
+
queue_wait_ms = ((start_info[:started_at] - start_info[:enqueued_at]) * 1000).round(2)
|
|
298
|
+
end
|
|
295
299
|
|
|
296
300
|
# Determine level based on outcome and duration
|
|
297
301
|
level = if exception
|
|
@@ -80,6 +80,7 @@ module BrainzLab
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def handle_sql_event(event)
|
|
83
|
+
payload = event.payload
|
|
83
84
|
duration = event.duration.round(2)
|
|
84
85
|
|
|
85
86
|
# Record breadcrumb for Reflex (enhanced)
|
|
@@ -217,12 +218,14 @@ module BrainzLab
|
|
|
217
218
|
tracker[:count] += 1
|
|
218
219
|
|
|
219
220
|
# Store sample queries (limited)
|
|
220
|
-
|
|
221
|
+
if tracker[:queries].size < 3
|
|
222
|
+
tracker[:queries] << truncate_sql(sql, 200)
|
|
223
|
+
end
|
|
221
224
|
|
|
222
225
|
# Detect N+1 pattern
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
if tracker[:count] == BrainzLab.configuration.n_plus_one_threshold
|
|
227
|
+
report_n_plus_one(table, tracker)
|
|
228
|
+
end
|
|
226
229
|
end
|
|
227
230
|
|
|
228
231
|
def report_n_plus_one(table, tracker)
|
|
@@ -237,14 +240,14 @@ module BrainzLab
|
|
|
237
240
|
}
|
|
238
241
|
)
|
|
239
242
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
if BrainzLab.configuration.recall_effectively_enabled?
|
|
244
|
+
BrainzLab::Recall.warn(
|
|
245
|
+
"Potential N+1 query detected",
|
|
246
|
+
table: table,
|
|
247
|
+
query_count: tracker[:count],
|
|
248
|
+
sample_queries: tracker[:queries]
|
|
249
|
+
)
|
|
250
|
+
end
|
|
248
251
|
end
|
|
249
252
|
|
|
250
253
|
def clear_n_plus_one_tracker!
|
|
@@ -329,8 +332,8 @@ module BrainzLab
|
|
|
329
332
|
transaction = event.payload[:transaction]
|
|
330
333
|
return unless transaction
|
|
331
334
|
|
|
332
|
-
Thread.current[:brainzlab_transaction_starts] ||= {}
|
|
333
|
-
Thread.current[:brainzlab_transaction_starts][transaction] = event.time
|
|
335
|
+
Thread.current[:brainzlab_transaction_starts] ||= {}
|
|
336
|
+
Thread.current[:brainzlab_transaction_starts][transaction.object_id] = event.time
|
|
334
337
|
rescue StandardError => e
|
|
335
338
|
BrainzLab.debug_log("ActiveRecord transaction start instrumentation failed: #{e.message}")
|
|
336
339
|
end
|
|
@@ -341,8 +344,8 @@ module BrainzLab
|
|
|
341
344
|
outcome = payload[:outcome] # :commit, :rollback, :restart, :incomplete
|
|
342
345
|
|
|
343
346
|
# Calculate duration from stored start time
|
|
344
|
-
starts = Thread.current[:brainzlab_transaction_starts] || {}
|
|
345
|
-
start_time = starts.delete(transaction) || event.time
|
|
347
|
+
starts = Thread.current[:brainzlab_transaction_starts] || {}
|
|
348
|
+
start_time = starts.delete(transaction&.object_id) || event.time
|
|
346
349
|
duration = ((event.end - start_time) * 1000).round(2)
|
|
347
350
|
|
|
348
351
|
connection_name = extract_connection_name(payload[:connection])
|
|
@@ -437,7 +440,7 @@ module BrainzLab
|
|
|
437
440
|
# Log to Recall
|
|
438
441
|
if BrainzLab.configuration.recall_effectively_enabled?
|
|
439
442
|
BrainzLab::Recall.warn(
|
|
440
|
-
|
|
443
|
+
"Strict loading violation detected",
|
|
441
444
|
owner_class: owner_class,
|
|
442
445
|
association: association_name.to_s,
|
|
443
446
|
message: "Attempted to lazily load #{association_name} on #{owner_class} with strict_loading enabled"
|
|
@@ -482,7 +485,7 @@ module BrainzLab
|
|
|
482
485
|
# Log to Recall
|
|
483
486
|
if BrainzLab.configuration.recall_effectively_enabled?
|
|
484
487
|
BrainzLab::Recall.warn(
|
|
485
|
-
|
|
488
|
+
"Deprecated association accessed",
|
|
486
489
|
owner_class: owner_class,
|
|
487
490
|
association: association_name.to_s,
|
|
488
491
|
message: message
|
|
@@ -518,9 +521,9 @@ module BrainzLab
|
|
|
518
521
|
# Handles: SELECT ... FROM table_name, SELECT ... FROM "table_name", etc.
|
|
519
522
|
return nil unless sql =~ /\ASELECT/i
|
|
520
523
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
+
if sql =~ /FROM\s+["'`]?(\w+)["'`]?/i
|
|
525
|
+
::Regexp.last_match(1)
|
|
526
|
+
end
|
|
524
527
|
end
|
|
525
528
|
|
|
526
529
|
def skip_query?(payload)
|