brainzlab 0.1.11 → 0.1.20

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +210 -3
  4. data/lib/brainzlab/beacon/client.rb +21 -1
  5. data/lib/brainzlab/configuration.rb +81 -4
  6. data/lib/brainzlab/cortex/client.rb +21 -1
  7. data/lib/brainzlab/debug.rb +305 -0
  8. data/lib/brainzlab/dendrite/client.rb +21 -1
  9. data/lib/brainzlab/development/logger.rb +150 -0
  10. data/lib/brainzlab/development/store.rb +121 -0
  11. data/lib/brainzlab/development.rb +72 -0
  12. data/lib/brainzlab/devtools/assets/devtools.css +245 -109
  13. data/lib/brainzlab/devtools/assets/devtools.js +40 -0
  14. data/lib/brainzlab/devtools/middleware/asset_server.rb +1 -0
  15. data/lib/brainzlab/devtools/middleware/debug_panel.rb +1 -0
  16. data/lib/brainzlab/devtools/middleware/error_page.rb +56 -8
  17. data/lib/brainzlab/errors.rb +490 -0
  18. data/lib/brainzlab/flux/buffer.rb +2 -2
  19. data/lib/brainzlab/flux/client.rb +2 -2
  20. data/lib/brainzlab/instrumentation/active_support_cache.rb +60 -30
  21. data/lib/brainzlab/instrumentation/net_http.rb +21 -16
  22. data/lib/brainzlab/instrumentation.rb +6 -0
  23. data/lib/brainzlab/nerve/client.rb +21 -1
  24. data/lib/brainzlab/pulse/client.rb +66 -5
  25. data/lib/brainzlab/pulse.rb +24 -5
  26. data/lib/brainzlab/rails/log_formatter.rb +1 -1
  27. data/lib/brainzlab/rails/railtie.rb +18 -3
  28. data/lib/brainzlab/recall/buffer.rb +3 -1
  29. data/lib/brainzlab/recall/client.rb +74 -6
  30. data/lib/brainzlab/recall.rb +19 -2
  31. data/lib/brainzlab/reflex/client.rb +66 -5
  32. data/lib/brainzlab/reflex.rb +40 -8
  33. data/lib/brainzlab/sentinel/client.rb +21 -1
  34. data/lib/brainzlab/synapse/client.rb +21 -1
  35. data/lib/brainzlab/testing/event_store.rb +377 -0
  36. data/lib/brainzlab/testing/helpers.rb +650 -0
  37. data/lib/brainzlab/testing/matchers.rb +391 -0
  38. data/lib/brainzlab/testing.rb +327 -0
  39. data/lib/brainzlab/utilities/circuit_breaker.rb +32 -3
  40. data/lib/brainzlab/vault/client.rb +21 -1
  41. data/lib/brainzlab/version.rb +1 -1
  42. data/lib/brainzlab/vision/client.rb +53 -6
  43. data/lib/brainzlab.rb +67 -0
  44. data/lib/fluyenta-ruby.rb +3 -0
  45. metadata +34 -11
@@ -48,11 +48,11 @@ module BrainzLab
48
48
 
49
49
  response = http.request(request)
50
50
 
51
- BrainzLab.debug("[Flux] Request failed: #{response.code} - #{response.body}") unless response.is_a?(Net::HTTPSuccess)
51
+ BrainzLab.debug_log("[Flux] Request failed: #{response.code} - #{response.body}") unless response.is_a?(Net::HTTPSuccess)
52
52
 
53
53
  response
54
54
  rescue StandardError => e
55
- BrainzLab.debug("[Flux] Request error: #{e.message}")
55
+ BrainzLab.debug_log("[Flux] Request error: #{e.message}")
56
56
  nil
57
57
  end
58
58
 
@@ -43,8 +43,10 @@ module BrainzLab
43
43
  # ============================================
44
44
  def install_cache_read_subscriber!
45
45
  ActiveSupport::Notifications.subscribe('cache_read.active_support') do |*args|
46
- event = ActiveSupport::Notifications::Event.new(*args)
47
- handle_cache_read(event)
46
+ BrainzLab.with_instrumentation_guard do
47
+ event = ActiveSupport::Notifications::Event.new(*args)
48
+ handle_cache_read(event)
49
+ end
48
50
  end
49
51
  end
50
52
 
@@ -76,8 +78,10 @@ module BrainzLab
76
78
  # ============================================
77
79
  def install_cache_read_multi_subscriber!
78
80
  ActiveSupport::Notifications.subscribe('cache_read_multi.active_support') do |*args|
79
- event = ActiveSupport::Notifications::Event.new(*args)
80
- handle_cache_read_multi(event)
81
+ BrainzLab.with_instrumentation_guard do
82
+ event = ActiveSupport::Notifications::Event.new(*args)
83
+ handle_cache_read_multi(event)
84
+ end
81
85
  end
82
86
  end
83
87
 
@@ -122,8 +126,10 @@ module BrainzLab
122
126
  # ============================================
123
127
  def install_cache_write_subscriber!
124
128
  ActiveSupport::Notifications.subscribe('cache_write.active_support') do |*args|
125
- event = ActiveSupport::Notifications::Event.new(*args)
126
- handle_cache_write(event)
129
+ BrainzLab.with_instrumentation_guard do
130
+ event = ActiveSupport::Notifications::Event.new(*args)
131
+ handle_cache_write(event)
132
+ end
127
133
  end
128
134
  end
129
135
 
@@ -150,8 +156,10 @@ module BrainzLab
150
156
  # ============================================
151
157
  def install_cache_write_multi_subscriber!
152
158
  ActiveSupport::Notifications.subscribe('cache_write_multi.active_support') do |*args|
153
- event = ActiveSupport::Notifications::Event.new(*args)
154
- handle_cache_write_multi(event)
159
+ BrainzLab.with_instrumentation_guard do
160
+ event = ActiveSupport::Notifications::Event.new(*args)
161
+ handle_cache_write_multi(event)
162
+ end
155
163
  end
156
164
  end
157
165
 
@@ -186,8 +194,10 @@ module BrainzLab
186
194
  # ============================================
187
195
  def install_cache_delete_subscriber!
188
196
  ActiveSupport::Notifications.subscribe('cache_delete.active_support') do |*args|
189
- event = ActiveSupport::Notifications::Event.new(*args)
190
- handle_cache_delete(event)
197
+ BrainzLab.with_instrumentation_guard do
198
+ event = ActiveSupport::Notifications::Event.new(*args)
199
+ handle_cache_delete(event)
200
+ end
191
201
  end
192
202
  end
193
203
 
@@ -211,8 +221,10 @@ module BrainzLab
211
221
  # ============================================
212
222
  def install_cache_exist_subscriber!
213
223
  ActiveSupport::Notifications.subscribe('cache_exist?.active_support') do |*args|
214
- event = ActiveSupport::Notifications::Event.new(*args)
215
- handle_cache_exist(event)
224
+ BrainzLab.with_instrumentation_guard do
225
+ event = ActiveSupport::Notifications::Event.new(*args)
226
+ handle_cache_exist(event)
227
+ end
216
228
  end
217
229
  end
218
230
 
@@ -236,8 +248,10 @@ module BrainzLab
236
248
  # ============================================
237
249
  def install_cache_fetch_hit_subscriber!
238
250
  ActiveSupport::Notifications.subscribe('cache_fetch_hit.active_support') do |*args|
239
- event = ActiveSupport::Notifications::Event.new(*args)
240
- handle_cache_fetch_hit(event)
251
+ BrainzLab.with_instrumentation_guard do
252
+ event = ActiveSupport::Notifications::Event.new(*args)
253
+ handle_cache_fetch_hit(event)
254
+ end
241
255
  end
242
256
  end
243
257
 
@@ -264,8 +278,10 @@ module BrainzLab
264
278
  # ============================================
265
279
  def install_cache_generate_subscriber!
266
280
  ActiveSupport::Notifications.subscribe('cache_generate.active_support') do |*args|
267
- event = ActiveSupport::Notifications::Event.new(*args)
268
- handle_cache_generate(event)
281
+ BrainzLab.with_instrumentation_guard do
282
+ event = ActiveSupport::Notifications::Event.new(*args)
283
+ handle_cache_generate(event)
284
+ end
269
285
  end
270
286
  end
271
287
 
@@ -312,8 +328,10 @@ module BrainzLab
312
328
  # ============================================
313
329
  def install_cache_increment_subscriber!
314
330
  ActiveSupport::Notifications.subscribe('cache_increment.active_support') do |*args|
315
- event = ActiveSupport::Notifications::Event.new(*args)
316
- handle_cache_increment(event)
331
+ BrainzLab.with_instrumentation_guard do
332
+ event = ActiveSupport::Notifications::Event.new(*args)
333
+ handle_cache_increment(event)
334
+ end
317
335
  end
318
336
  end
319
337
 
@@ -349,8 +367,10 @@ module BrainzLab
349
367
  # ============================================
350
368
  def install_cache_decrement_subscriber!
351
369
  ActiveSupport::Notifications.subscribe('cache_decrement.active_support') do |*args|
352
- event = ActiveSupport::Notifications::Event.new(*args)
353
- handle_cache_decrement(event)
370
+ BrainzLab.with_instrumentation_guard do
371
+ event = ActiveSupport::Notifications::Event.new(*args)
372
+ handle_cache_decrement(event)
373
+ end
354
374
  end
355
375
  end
356
376
 
@@ -386,8 +406,10 @@ module BrainzLab
386
406
  # ============================================
387
407
  def install_cache_delete_multi_subscriber!
388
408
  ActiveSupport::Notifications.subscribe('cache_delete_multi.active_support') do |*args|
389
- event = ActiveSupport::Notifications::Event.new(*args)
390
- handle_cache_delete_multi(event)
409
+ BrainzLab.with_instrumentation_guard do
410
+ event = ActiveSupport::Notifications::Event.new(*args)
411
+ handle_cache_delete_multi(event)
412
+ end
391
413
  end
392
414
  end
393
415
 
@@ -422,8 +444,10 @@ module BrainzLab
422
444
  # ============================================
423
445
  def install_cache_delete_matched_subscriber!
424
446
  ActiveSupport::Notifications.subscribe('cache_delete_matched.active_support') do |*args|
425
- event = ActiveSupport::Notifications::Event.new(*args)
426
- handle_cache_delete_matched(event)
447
+ BrainzLab.with_instrumentation_guard do
448
+ event = ActiveSupport::Notifications::Event.new(*args)
449
+ handle_cache_delete_matched(event)
450
+ end
427
451
  end
428
452
  end
429
453
 
@@ -466,8 +490,10 @@ module BrainzLab
466
490
  # ============================================
467
491
  def install_cache_cleanup_subscriber!
468
492
  ActiveSupport::Notifications.subscribe('cache_cleanup.active_support') do |*args|
469
- event = ActiveSupport::Notifications::Event.new(*args)
470
- handle_cache_cleanup(event)
493
+ BrainzLab.with_instrumentation_guard do
494
+ event = ActiveSupport::Notifications::Event.new(*args)
495
+ handle_cache_cleanup(event)
496
+ end
471
497
  end
472
498
  end
473
499
 
@@ -502,8 +528,10 @@ module BrainzLab
502
528
  # ============================================
503
529
  def install_cache_prune_subscriber!
504
530
  ActiveSupport::Notifications.subscribe('cache_prune.active_support') do |*args|
505
- event = ActiveSupport::Notifications::Event.new(*args)
506
- handle_cache_prune(event)
531
+ BrainzLab.with_instrumentation_guard do
532
+ event = ActiveSupport::Notifications::Event.new(*args)
533
+ handle_cache_prune(event)
534
+ end
507
535
  end
508
536
  end
509
537
 
@@ -552,8 +580,10 @@ module BrainzLab
552
580
  # ============================================
553
581
  def install_message_serializer_fallback_subscriber!
554
582
  ActiveSupport::Notifications.subscribe('message_serializer_fallback.active_support') do |*args|
555
- event = ActiveSupport::Notifications::Event.new(*args)
556
- handle_message_serializer_fallback(event)
583
+ BrainzLab.with_instrumentation_guard do
584
+ event = ActiveSupport::Notifications::Event.new(*args)
585
+ handle_message_serializer_fallback(event)
586
+ end
557
587
  end
558
588
  end
559
589
 
@@ -63,6 +63,9 @@ module BrainzLab
63
63
 
64
64
  def should_track?
65
65
  return false unless BrainzLab.configuration.instrument_http
66
+ # Skip tracking SDK's own HTTP calls to its service endpoints
67
+ # to prevent recursive cascading (SDK HTTP → track → Recall.debug → buffer → flush → SDK HTTP → ...)
68
+ return false if BrainzLab.configuration.sdk_service_hosts.include?(address)
66
69
 
67
70
  ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
68
71
  !ignore_hosts.include?(address)
@@ -82,22 +85,24 @@ module BrainzLab
82
85
  duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
83
86
  level = error || (status && status >= 400) ? :error : :info
84
87
 
85
- # Add breadcrumb for Reflex
86
- if BrainzLab.configuration.reflex_enabled
87
- BrainzLab::Reflex.add_breadcrumb(
88
- "#{method} #{url}",
89
- category: 'http',
90
- level: level,
91
- data: { method: method, url: url, status_code: status, duration_ms: duration_ms, error: error }.compact
92
- )
93
- end
94
-
95
- # Log to Recall at debug level (avoid noise)
96
- if BrainzLab.configuration.recall_enabled
97
- BrainzLab::Recall.debug(
98
- "HTTP #{method} #{url} -> #{status || 'ERROR'}",
99
- method: method, url: url, status_code: status, duration_ms: duration_ms, error: error
100
- )
88
+ BrainzLab.with_instrumentation_guard do
89
+ # Add breadcrumb for Reflex (in-memory, safe)
90
+ if BrainzLab.configuration.reflex_enabled
91
+ BrainzLab::Reflex.add_breadcrumb(
92
+ "#{method} #{url}",
93
+ category: 'http',
94
+ level: level,
95
+ data: { method: method, url: url, status_code: status, duration_ms: duration_ms, error: error }.compact
96
+ )
97
+ end
98
+
99
+ # Log to Recall at debug level (skipped if already instrumenting)
100
+ if BrainzLab.configuration.recall_enabled
101
+ BrainzLab::Recall.debug(
102
+ "HTTP #{method} #{url} -> #{status || 'ERROR'}",
103
+ method: method, url: url, status_code: status, duration_ms: duration_ms, error: error
104
+ )
105
+ end
101
106
  end
102
107
  rescue StandardError => e
103
108
  # Don't let instrumentation errors crash the app
@@ -6,6 +6,12 @@ module BrainzLab
6
6
  def install!
7
7
  config = BrainzLab.configuration
8
8
 
9
+ # Skip all instrumentation if SDK is disabled
10
+ unless config.enabled?
11
+ BrainzLab.debug_log('[Instrumentation] SDK disabled via BRAINZLAB_SDK_ENABLED=false, skipping all instrumentation')
12
+ return
13
+ end
14
+
9
15
  # Skip Rails-specific instrumentation if brainzlab-rails gem is handling it
10
16
  # This prevents double-tracking of events
11
17
  if config.rails_instrumentation_handled_externally
@@ -208,7 +208,27 @@ module BrainzLab
208
208
  end
209
209
 
210
210
  def log_error(operation, error)
211
- BrainzLab.debug_log("[Nerve::Client] #{operation} failed: #{error.message}")
211
+ structured_error = ErrorHandler.wrap(error, service: 'Nerve', operation: operation)
212
+ BrainzLab.debug_log("[Nerve::Client] #{operation} failed: #{structured_error.message}")
213
+
214
+ # Call on_error callback if configured
215
+ if @config.on_error
216
+ @config.on_error.call(structured_error, { service: 'Nerve', operation: operation })
217
+ end
218
+ end
219
+
220
+ def handle_response_error(response, operation)
221
+ return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent)
222
+
223
+ structured_error = ErrorHandler.from_response(response, service: 'Nerve', operation: operation)
224
+ BrainzLab.debug_log("[Nerve::Client] #{operation} failed: #{structured_error.message}")
225
+
226
+ # Call on_error callback if configured
227
+ if @config.on_error
228
+ @config.on_error.call(structured_error, { service: 'Nerve', operation: operation })
229
+ end
230
+
231
+ structured_error
212
232
  end
213
233
  end
214
234
  end
@@ -84,20 +84,29 @@ module BrainzLab
84
84
 
85
85
  def post(path, body)
86
86
  uri = URI.join(@config.pulse_url, path)
87
+
88
+ # Call on_send callback if configured
89
+ invoke_on_send(:pulse, :post, path, body)
90
+
91
+ # Log debug output for request
92
+ log_debug_request(path, body)
93
+
87
94
  request = Net::HTTP::Post.new(uri)
88
95
  request['Content-Type'] = 'application/json'
89
96
  request['Authorization'] = "Bearer #{@config.pulse_auth_key}"
90
97
  request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
91
98
  request.body = JSON.generate(body)
92
99
 
93
- execute_with_retry(uri, request)
100
+ execute_with_retry(uri, request, path)
94
101
  rescue StandardError => e
95
- log_error("Failed to send to Pulse: #{e.message}")
102
+ handle_error(e, context: { path: path, body_size: body.to_s.length })
96
103
  nil
97
104
  end
98
105
 
99
- def execute_with_retry(uri, request)
106
+ def execute_with_retry(uri, request, path)
100
107
  retries = 0
108
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
109
+
101
110
  begin
102
111
  http = Net::HTTP.new(uri.host, uri.port)
103
112
  http.use_ssl = uri.scheme == 'https'
@@ -105,6 +114,10 @@ module BrainzLab
105
114
  http.read_timeout = 10
106
115
 
107
116
  response = http.request(request)
117
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
118
+
119
+ # Log debug output for response
120
+ log_debug_response(response.code.to_i, duration_ms)
108
121
 
109
122
  case response.code.to_i
110
123
  when 200..299
@@ -116,7 +129,10 @@ module BrainzLab
116
129
  when 429, 500..599
117
130
  raise RetryableError, "Server error: #{response.code}"
118
131
  else
119
- log_error("Pulse API error: #{response.code} - #{response.body}")
132
+ handle_error(
133
+ StandardError.new("Pulse API error: #{response.code}"),
134
+ context: { path: path, status: response.code, body: response.body }
135
+ )
120
136
  nil
121
137
  end
122
138
  rescue RetryableError, Net::OpenTimeout, Net::ReadTimeout => e
@@ -125,12 +141,57 @@ module BrainzLab
125
141
  sleep(RETRY_DELAY * retries)
126
142
  retry
127
143
  end
128
- log_error("Failed after #{MAX_RETRIES} retries: #{e.message}")
144
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
145
+ log_debug_response(0, duration_ms, error: e.message)
146
+ handle_error(e, context: { path: path, retries: retries })
129
147
  nil
130
148
  end
131
149
  end
132
150
 
151
+ def log_debug_request(path, body)
152
+ return unless BrainzLab::Debug.enabled?
153
+
154
+ data = if body.is_a?(Hash) && body[:traces]
155
+ { count: body[:traces].size }
156
+ elsif body.is_a?(Hash) && body[:name]
157
+ { name: body[:name] }
158
+ else
159
+ {}
160
+ end
161
+
162
+ BrainzLab::Debug.log_request(:pulse, 'POST', path, data: data)
163
+ end
164
+
165
+ def log_debug_response(status, duration_ms, error: nil)
166
+ return unless BrainzLab::Debug.enabled?
167
+
168
+ BrainzLab::Debug.log_response(:pulse, status, duration_ms, error: error)
169
+ end
170
+
171
+ def invoke_on_send(service, method, path, payload)
172
+ return unless @config.on_send
173
+
174
+ @config.on_send.call(service, method, path, payload)
175
+ rescue StandardError => e
176
+ # Don't let callback errors break the SDK
177
+ log_error("on_send callback error: #{e.message}")
178
+ end
179
+
180
+ def handle_error(error, context: {})
181
+ log_error("#{error.message}")
182
+
183
+ # Call on_error callback if configured
184
+ return unless @config.on_error
185
+
186
+ @config.on_error.call(error, context.merge(service: :pulse))
187
+ rescue StandardError => e
188
+ # Don't let callback errors break the SDK
189
+ log_error("on_error callback error: #{e.message}")
190
+ end
191
+
133
192
  def log_error(message)
193
+ BrainzLab::Debug.log(message, level: :error) if BrainzLab::Debug.enabled?
194
+
134
195
  return unless @config.logger
135
196
 
136
197
  @config.logger.error("[BrainzLab::Pulse] #{message}")
@@ -47,10 +47,17 @@ module BrainzLab
47
47
  def record_trace(name, started_at:, ended_at:, kind: 'request', **attributes)
48
48
  return unless enabled?
49
49
 
50
+ payload = build_trace_payload(name, kind, started_at, ended_at, attributes)
51
+
52
+ # In development mode, log locally instead of sending to server
53
+ if BrainzLab.configuration.development_mode?
54
+ Development.record(service: :pulse, event_type: 'trace', payload: payload)
55
+ return
56
+ end
57
+
50
58
  ensure_provisioned!
51
59
  return unless BrainzLab.configuration.pulse_valid?
52
60
 
53
- payload = build_trace_payload(name, kind, started_at, ended_at, attributes)
54
61
  client.send_trace(payload)
55
62
  end
56
63
 
@@ -58,9 +65,6 @@ module BrainzLab
58
65
  def record_metric(name, value:, kind: 'gauge', tags: {})
59
66
  return unless enabled?
60
67
 
61
- ensure_provisioned!
62
- return unless BrainzLab.configuration.pulse_valid?
63
-
64
68
  payload = {
65
69
  name: name,
66
70
  value: value,
@@ -69,7 +73,22 @@ module BrainzLab
69
73
  tags: tags
70
74
  }
71
75
 
72
- client.send_metric(payload)
76
+ # In development mode, log locally instead of sending to server
77
+ if BrainzLab.configuration.development_mode?
78
+ Development.record(service: :pulse, event_type: 'metric', payload: payload)
79
+ return
80
+ end
81
+
82
+ ensure_provisioned!
83
+ return unless BrainzLab.configuration.pulse_valid?
84
+
85
+ if BrainzLab.instrumenting?
86
+ # During instrumentation, send in background thread to avoid
87
+ # blocking the host app with synchronous HTTP
88
+ Thread.new { client.send_metric(payload) }
89
+ else
90
+ client.send_metric(payload)
91
+ end
73
92
  end
74
93
 
75
94
  # Convenience methods for metrics
@@ -641,7 +641,7 @@ module BrainzLab
641
641
  end
642
642
 
643
643
  def hash_like?(obj)
644
- obj.is_a?(Hash) || (obj.respond_to?(:to_h) && obj.respond_to?(:each))
644
+ obj.is_a?(Hash) || (!obj.is_a?(Array) && obj.respond_to?(:to_h) && obj.respond_to?(:each))
645
645
  end
646
646
 
647
647
  def format_params_toml(params, prefix = '', depth = 0)
@@ -55,6 +55,12 @@ module BrainzLab
55
55
  end
56
56
 
57
57
  config.after_initialize do
58
+ # Skip all SDK initialization if disabled
59
+ unless BrainzLab.configuration.enabled?
60
+ BrainzLab.debug_log('[Railtie] SDK disabled via BRAINZLAB_SDK_ENABLED=false, skipping initialization')
61
+ next
62
+ end
63
+
58
64
  # Set up custom log formatter
59
65
  BrainzLab::Rails::Railtie.setup_log_formatter if BrainzLab.configuration.log_formatter_enabled
60
66
 
@@ -114,8 +120,13 @@ module BrainzLab
114
120
  end
115
121
 
116
122
  def silence_rails_logging
117
- # Create a null logger that discards all output
118
- null_logger = Logger.new(File::NULL)
123
+ # Create a null logger that discards all output.
124
+ # Use ActiveSupport::Logger (not a plain ::Logger) so the logger
125
+ # retains LoggerSilence#silence — SolidQueue's poller calls
126
+ # `ActiveRecord::Base.logger.silence { ... }` every cycle, and a
127
+ # plain Logger lacks `silence` (NoMethodError → workers crash-loop).
128
+ logger_class = defined?(ActiveSupport::Logger) ? ActiveSupport::Logger : Logger
129
+ null_logger = logger_class.new(File::NULL)
119
130
  null_logger.level = Logger::FATAL
120
131
 
121
132
  # Silence ActiveRecord SQL logging
@@ -217,7 +228,11 @@ module BrainzLab
217
228
  context.request_method = request.request_method
218
229
  context.request_path = request.path
219
230
  context.request_url = request.url
220
- context.request_params = filter_params(request.params.to_h)
231
+ context.request_params = begin
232
+ filter_params(request.params.to_h)
233
+ rescue ActionDispatch::Http::Parameters::ParseError
234
+ {}
235
+ end
221
236
  context.request_headers = extract_headers(env)
222
237
 
223
238
  # Add breadcrumb for request start
@@ -19,7 +19,9 @@ module BrainzLab
19
19
 
20
20
  def push(log_entry)
21
21
  @buffer.push(log_entry)
22
- flush if @buffer.size >= @config.recall_buffer_size
22
+ # Skip synchronous flush during instrumentation to avoid blocking the host app.
23
+ # The background flush thread will send these entries within recall_flush_interval seconds.
24
+ flush if @buffer.size >= @config.recall_buffer_size && !BrainzLab.instrumenting?
23
25
  end
24
26
 
25
27
  def flush
@@ -32,20 +32,29 @@ module BrainzLab
32
32
 
33
33
  def post(path, body)
34
34
  uri = URI.join(@config.recall_url, path)
35
+
36
+ # Call on_send callback if configured
37
+ invoke_on_send(:recall, :post, path, body)
38
+
39
+ # Log debug output for request
40
+ log_debug_request(path, body)
41
+
35
42
  request = Net::HTTP::Post.new(uri)
36
43
  request['Content-Type'] = 'application/json'
37
44
  request['Authorization'] = "Bearer #{@config.secret_key}"
38
45
  request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
39
46
  request.body = JSON.generate(body)
40
47
 
41
- execute_with_retry(uri, request)
48
+ execute_with_retry(uri, request, path)
42
49
  rescue StandardError => e
43
- log_error("Failed to send to Recall: #{e.message}")
50
+ handle_error(e, context: { path: path, body_size: body.to_s.length })
44
51
  nil
45
52
  end
46
53
 
47
- def execute_with_retry(uri, request)
54
+ def execute_with_retry(uri, request, path)
48
55
  retries = 0
56
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
57
+
49
58
  begin
50
59
  http = Net::HTTP.new(uri.host, uri.port)
51
60
  http.use_ssl = uri.scheme == 'https'
@@ -53,6 +62,10 @@ module BrainzLab
53
62
  http.read_timeout = 10
54
63
 
55
64
  response = http.request(request)
65
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
66
+
67
+ # Log debug output for response
68
+ log_debug_response(response.code.to_i, duration_ms)
56
69
 
57
70
  case response.code.to_i
58
71
  when 200..299
@@ -64,7 +77,10 @@ module BrainzLab
64
77
  when 429, 500..599
65
78
  raise RetryableError, "Server error: #{response.code}"
66
79
  else
67
- log_error("Recall API error: #{response.code} - #{response.body}")
80
+ handle_error(
81
+ StandardError.new("Recall API error: #{response.code}"),
82
+ context: { path: path, status: response.code, body: response.body }
83
+ )
68
84
  nil
69
85
  end
70
86
  rescue RetryableError, Net::OpenTimeout, Net::ReadTimeout => e
@@ -73,15 +89,67 @@ module BrainzLab
73
89
  sleep(RETRY_DELAY * retries)
74
90
  retry
75
91
  end
76
- log_error("Failed after #{MAX_RETRIES} retries: #{e.message}")
92
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
93
+ log_debug_response(0, duration_ms, error: e.message)
94
+ handle_error(e, context: { path: path, retries: retries })
77
95
  nil
78
96
  end
79
97
  end
80
98
 
99
+ def log_debug_request(path, body)
100
+ return unless BrainzLab::Debug.enabled?
101
+
102
+ data = if body.is_a?(Hash) && body[:logs]
103
+ { count: body[:logs].size }
104
+ elsif body.is_a?(Hash) && body[:message]
105
+ { message: body[:message] }
106
+ else
107
+ {}
108
+ end
109
+
110
+ BrainzLab::Debug.log_request(:recall, 'POST', path, data: data)
111
+ end
112
+
113
+ def log_debug_response(status, duration_ms, error: nil)
114
+ return unless BrainzLab::Debug.enabled?
115
+
116
+ BrainzLab::Debug.log_response(:recall, status, duration_ms, error: error)
117
+ end
118
+
119
+ def invoke_on_send(service, method, path, payload)
120
+ return unless @config.on_send
121
+
122
+ @config.on_send.call(service, method, path, payload)
123
+ rescue StandardError => e
124
+ # Don't let callback errors break the SDK
125
+ log_error("on_send callback error: #{e.message}")
126
+ end
127
+
128
+ def handle_error(error, context: {})
129
+ # Wrap the error in a structured error if it's not already one
130
+ structured_error = if error.is_a?(BrainzLab::Error)
131
+ error
132
+ else
133
+ ErrorHandler.wrap(error, service: 'Recall', operation: context[:path] || 'unknown')
134
+ end
135
+
136
+ log_error(structured_error.message)
137
+
138
+ # Call on_error callback if configured
139
+ return unless @config.on_error
140
+
141
+ @config.on_error.call(structured_error, context.merge(service: :recall))
142
+ rescue StandardError => e
143
+ # Don't let callback errors break the SDK
144
+ log_error("on_error callback error: #{e.message}")
145
+ end
146
+
81
147
  def log_error(message)
148
+ BrainzLab::Debug.log(message, level: :error) if BrainzLab::Debug.enabled?
149
+
82
150
  return unless @config.logger
83
151
 
84
- @config.logger.error("[BrainzLab] #{message}")
152
+ @config.logger.error("[BrainzLab::Recall] #{message}")
85
153
  end
86
154
 
87
155
  class RetryableError < StandardError; end