brainzlab 0.1.12 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85a5cf3a64e256a569be39f7caa1ec6268755616ac6037db51f1543ee74dd50c
4
- data.tar.gz: '09c531bb0793d26db1f62faf41f9ead294130a7036f6e5f6ab270fd8c6c46040'
3
+ metadata.gz: ac1922e302a243a61b38d955da04782ae4e66c11908bea728a39b6ad3c68634e
4
+ data.tar.gz: 748d7f686bf204d0e0ef9322fb445dd75b62c13404472758631e976cc135c295
5
5
  SHA512:
6
- metadata.gz: 28c2f10fc4457d4f2f34c5bcf4d63c14a062d4fcd8437b13cbcebc1a062976361b2014f5567e0fff82682a054fa83363f7e955a23d3d59ae285a9f06500c79e6
7
- data.tar.gz: 5d130bf7242d57c3149803649262995daf298483c406002ab99e01495efb14df7a6874b8eb485daa90c79da8903ad37f6516ba0ca7e2c8930ff069a1b536a206
6
+ metadata.gz: ee4bdba72d0fed3db7489bf0335de8dd4b8139a941eb381b4a35a3425ca0fa7755f3702869ac93405a5cd0e9def39174ced81eb559c3de99b6970e353c99f977
7
+ data.tar.gz: 8aec3a840b93948c73ad1e1e518182b316b4a8a29c20c8b5a6819a88c5f12e647d9c576dc865e04991ac0ea4976e2536d21751b1291ebbaf5594713a9317b2b7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.13] - 2026-02-24
6
+
7
+ ### Fixed
8
+
9
+ - **Rails LogFormatter** - Fix `TypeError: wrong element type Hash at 0 (expected array)` in `format_params_toml`
10
+ - `hash_like?` incorrectly matched Arrays (they respond to `to_h` and `each`), causing `Array#to_h` to fail when params contained arrays of hashes
11
+
5
12
  ## [0.1.1] - 2025-12-23
6
13
 
7
14
  ### Fixed
@@ -10,7 +10,8 @@ module BrainzLab
10
10
  # mode has a custom setter with validation
11
11
  attr_reader :mode
12
12
 
13
- attr_accessor :secret_key,
13
+ attr_accessor :enabled,
14
+ :secret_key,
14
15
  :environment,
15
16
  :service,
16
17
  :host,
@@ -169,6 +170,9 @@ module BrainzLab
169
170
  }.freeze
170
171
 
171
172
  def initialize
173
+ # SDK enabled flag — set BRAINZLAB_SDK_ENABLED=false to completely disable
174
+ @enabled = ENV.fetch('BRAINZLAB_SDK_ENABLED', 'true') != 'false'
175
+
172
176
  # Authentication
173
177
  @secret_key = ENV.fetch('BRAINZLAB_SECRET_KEY', nil)
174
178
 
@@ -422,7 +426,11 @@ module BrainzLab
422
426
  end
423
427
 
424
428
  def valid?
425
- !@secret_key.nil? && !@secret_key.empty?
429
+ @enabled && !@secret_key.nil? && !@secret_key.empty?
430
+ end
431
+
432
+ def enabled?
433
+ @enabled == true
426
434
  end
427
435
 
428
436
  def reflex_valid?
@@ -537,8 +545,24 @@ module BrainzLab
537
545
  @debug == true
538
546
  end
539
547
 
548
+ # Returns hostnames of all configured SDK service URLs
549
+ # Used by Net::HTTP instrumentation to skip tracking SDK's own HTTP calls
550
+ def sdk_service_hosts
551
+ @sdk_service_hosts ||= begin
552
+ urls = [
553
+ @recall_url, @reflex_url, @pulse_url, @flux_url,
554
+ @signal_url, @vault_url, @vision_url, @cortex_url,
555
+ @beacon_url, @nerve_url, @dendrite_url, @sentinel_url,
556
+ @synapse_url
557
+ ].compact
558
+
559
+ urls.filter_map { |url| URI.parse(url).host rescue nil }.uniq
560
+ end
561
+ end
562
+
540
563
  # Check if recall is effectively enabled (considering self-tracking)
541
564
  def recall_effectively_enabled?
565
+ return false unless @enabled
542
566
  return false unless @recall_enabled
543
567
  return true unless @disable_self_tracking
544
568
 
@@ -549,6 +573,7 @@ module BrainzLab
549
573
 
550
574
  # Check if reflex is effectively enabled (considering self-tracking)
551
575
  def reflex_effectively_enabled?
576
+ return false unless @enabled
552
577
  return false unless @reflex_enabled
553
578
  return true unless @disable_self_tracking
554
579
 
@@ -559,6 +584,7 @@ module BrainzLab
559
584
 
560
585
  # Check if pulse is effectively enabled (considering self-tracking)
561
586
  def pulse_effectively_enabled?
587
+ return false unless @enabled
562
588
  return false unless @pulse_enabled
563
589
  return true unless @disable_self_tracking
564
590
 
@@ -569,6 +595,7 @@ module BrainzLab
569
595
 
570
596
  # Check if flux is effectively enabled (considering self-tracking)
571
597
  def flux_effectively_enabled?
598
+ return false unless @enabled
572
599
  return false unless @flux_enabled
573
600
  return true unless @disable_self_tracking
574
601
 
@@ -579,6 +606,7 @@ module BrainzLab
579
606
 
580
607
  # Check if signal is effectively enabled (considering self-tracking)
581
608
  def signal_effectively_enabled?
609
+ return false unless @enabled
582
610
  return false unless @signal_enabled
583
611
  return true unless @disable_self_tracking
584
612
 
@@ -18,6 +18,7 @@ module BrainzLab
18
18
 
19
19
  def call(env)
20
20
  return @app.call(env) unless DevTools.enabled?
21
+ return @app.call(env) if env['REQUEST_METHOD'] == 'OPTIONS'
21
22
 
22
23
  path = env['PATH_INFO']
23
24
  asset_prefix = DevTools.asset_path
@@ -38,6 +38,7 @@ module BrainzLab
38
38
  return false unless DevTools.debug_panel_enabled?
39
39
  return false unless DevTools.allowed_environment?
40
40
  return false unless DevTools.allowed_ip?(extract_ip(env))
41
+ return false if env['REQUEST_METHOD'] == 'OPTIONS'
41
42
  return false if asset_request?(env['PATH_INFO'])
42
43
  return false if devtools_asset_request?(env['PATH_INFO'])
43
44
  return false if turbo_stream_request?(env)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module BrainzLab
4
6
  module DevTools
5
7
  module Middleware
@@ -10,29 +12,33 @@ module BrainzLab
10
12
  end
11
13
 
12
14
  def call(env)
15
+ return @app.call(env) if env['REQUEST_METHOD'] == 'OPTIONS'
13
16
  return @app.call(env) unless should_handle?(env)
14
17
 
15
18
  begin
16
19
  status, headers, body = @app.call(env)
17
20
 
18
21
  # Check if this is an error response that we should intercept
19
- if status >= 400 && html_response?(headers) && !json_request?(env)
22
+ if status >= 400 && html_response?(headers) && !json_request?(env) && !api_path?(env)
20
23
  # Check if this looks like Rails' default error page
21
24
  body_content = collect_body(body)
22
25
  if body_content.include?('Action Controller: Exception caught') || body_content.include?('background: #C00')
23
26
  # Extract exception info from the page
24
27
  exception_info = extract_exception_from_html(body_content)
25
28
  if exception_info
26
- data = collect_debug_data_from_info(env, exception_info)
27
- return render_error_page_from_info(exception_info, data)
29
+ data = collect_debug_data_from_info(env, exception_info, status)
30
+ return render_error_page_from_info(exception_info, data, status)
28
31
  end
29
32
  end
30
33
  end
31
34
 
32
35
  [status, headers, body]
33
36
  rescue Exception => e
34
- # Don't intercept if request wants JSON
35
- return raise_exception(e) if json_request?(env)
37
+ # For JSON/API requests, return a proper JSON error response
38
+ if json_request?(env) || api_path?(env)
39
+ capture_to_reflex(e)
40
+ return json_error_response(e)
41
+ end
36
42
 
37
43
  # Still capture to Reflex if available
38
44
  capture_to_reflex(e)
@@ -87,7 +93,7 @@ module BrainzLab
87
93
  .gsub(' ', ' ')
88
94
  end
89
95
 
90
- def collect_debug_data_from_info(env, info)
96
+ def collect_debug_data_from_info(env, info, status = 500)
91
97
  context = defined?(BrainzLab::Context) ? BrainzLab::Context.current : nil
92
98
  collector_data = Data::Collector.get_request_data
93
99
 
@@ -113,7 +119,7 @@ module BrainzLab
113
119
  }
114
120
  end
115
121
 
116
- def render_error_page_from_info(info, data)
122
+ def render_error_page_from_info(info, data, status = 500)
117
123
  # Create a simple exception-like object
118
124
  exception = StandardError.new(info[:message])
119
125
  exception.define_singleton_method(:class) do
@@ -126,7 +132,7 @@ module BrainzLab
126
132
  html = @renderer.render(exception, data)
127
133
 
128
134
  [
129
- 500,
135
+ status,
130
136
  {
131
137
  'Content-Type' => 'text/html; charset=utf-8',
132
138
  'Content-Length' => html.bytesize.to_s,
@@ -169,6 +175,48 @@ module BrainzLab
169
175
  env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
170
176
  end
171
177
 
178
+ def api_path?(env)
179
+ path = env['PATH_INFO'] || ''
180
+ path.start_with?('/api/')
181
+ end
182
+
183
+ def exception_to_status(exception)
184
+ case exception.class.name
185
+ when 'ActionController::RoutingError', 'AbstractController::ActionNotFound'
186
+ 404
187
+ when 'ActionController::MethodNotAllowed'
188
+ 405
189
+ when 'ActionController::BadRequest', 'ActionDispatch::Http::Parameters::ParseError'
190
+ 400
191
+ when 'ActionController::UnknownFormat'
192
+ 406
193
+ else
194
+ 500
195
+ end
196
+ end
197
+
198
+ def json_error_response(exception)
199
+ status_code = exception_to_status(exception)
200
+ message = case status_code
201
+ when 400 then 'Bad request'
202
+ when 404 then 'Not found'
203
+ when 405 then 'Method not allowed'
204
+ when 406 then 'Not acceptable'
205
+ else 'Internal server error'
206
+ end
207
+
208
+ body = JSON.generate({ error: message })
209
+ [
210
+ status_code,
211
+ {
212
+ 'Content-Type' => 'application/json; charset=utf-8',
213
+ 'Content-Length' => body.bytesize.to_s,
214
+ 'X-Content-Type-Options' => 'nosniff'
215
+ },
216
+ [body]
217
+ ]
218
+ end
219
+
172
220
  def capture_to_reflex(exception)
173
221
  return unless defined?(BrainzLab::Reflex)
174
222
 
@@ -76,7 +76,7 @@ module BrainzLab
76
76
 
77
77
  @client.send_batch(events: events, metrics: metrics)
78
78
  rescue StandardError => e
79
- BrainzLab.debug("[Flux] Batch send failed: #{e.message}")
79
+ BrainzLab.debug_log("[Flux] Batch send failed: #{e.message}")
80
80
  end
81
81
 
82
82
  def start_flush_thread
@@ -86,7 +86,7 @@ module BrainzLab
86
86
  begin
87
87
  flush! if size.positive?
88
88
  rescue StandardError => e
89
- BrainzLab.debug("[Flux] Flush thread error: #{e.message}")
89
+ BrainzLab.debug_log("[Flux] Flush thread error: #{e.message}")
90
90
  end
91
91
  end
92
92
  end
@@ -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
@@ -82,7 +82,13 @@ module BrainzLab
82
82
  ensure_provisioned!
83
83
  return unless BrainzLab.configuration.pulse_valid?
84
84
 
85
- client.send_metric(payload)
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
86
92
  end
87
93
 
88
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BrainzLab
4
- VERSION = '0.1.12'
4
+ VERSION = '0.1.20'
5
5
  end
data/lib/brainzlab.rb CHANGED
@@ -35,7 +35,32 @@ require_relative 'brainzlab/utilities'
35
35
  require_relative 'brainzlab/development'
36
36
 
37
37
  module BrainzLab
38
+ # Thread-local re-entrancy guard for instrumentation.
39
+ # When true, SDK operations that would make HTTP calls are skipped
40
+ # to prevent recursive instrumentation from blocking the host app.
41
+ INSTRUMENTING_KEY = :brainzlab_instrumenting
42
+
38
43
  class << self
44
+ # Returns true when inside an instrumentation handler.
45
+ # Used by Recall.log, Pulse.record_metric, etc. to skip HTTP calls
46
+ # that would block the host app during notification callbacks.
47
+ def instrumenting?
48
+ Thread.current[INSTRUMENTING_KEY] == true
49
+ end
50
+
51
+ # Executes a block within the instrumentation guard.
52
+ # Prevents recursive/cascading SDK HTTP calls from instrumentation handlers.
53
+ def with_instrumentation_guard
54
+ return if Thread.current[INSTRUMENTING_KEY]
55
+
56
+ Thread.current[INSTRUMENTING_KEY] = true
57
+ begin
58
+ yield
59
+ ensure
60
+ Thread.current[INSTRUMENTING_KEY] = nil
61
+ end
62
+ end
63
+
39
64
  def configure
40
65
  yield(configuration) if block_given?
41
66
  configuration
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'brainzlab'
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainzlab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.12
4
+ version: 0.1.20
5
5
  platform: ruby
6
6
  authors:
7
- - Brainz Lab
7
+ - BrainzLab
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -93,11 +93,11 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '3.0'
96
- description: Official Ruby SDK for BrainzLab observability platform. Includes Recall
97
- (structured logging), Reflex (error tracking), and Pulse (APM with distributed tracing).
98
- Auto-instruments Rails, Sidekiq, GraphQL, Redis, and more.
96
+ description: Ruby SDK for the BrainzLab observability platform. Includes Recall (structured
97
+ logging), Reflex (error tracking), and Pulse (APM with distributed tracing). Auto-instruments
98
+ Rails, Sidekiq, GraphQL, Redis, and more.
99
99
  email:
100
- - support@brainzlab.ai
100
+ - rubygems@brainz.llc
101
101
  executables: []
102
102
  extensions: []
103
103
  extra_rdoc_files: []
@@ -219,6 +219,7 @@ files:
219
219
  - lib/brainzlab/vision.rb
220
220
  - lib/brainzlab/vision/client.rb
221
221
  - lib/brainzlab/vision/provisioner.rb
222
+ - lib/fluyenta-ruby.rb
222
223
  - lib/generators/brainzlab/install/install_generator.rb
223
224
  - lib/generators/brainzlab/install/templates/brainzlab.rb.tt
224
225
  homepage: https://brainzlab.ai
@@ -226,11 +227,11 @@ licenses:
226
227
  - Nonstandard
227
228
  metadata:
228
229
  homepage_uri: https://brainzlab.ai
229
- source_code_uri: https://github.com/brainz-lab/brainzlab-ruby
230
- changelog_uri: https://github.com/brainz-lab/brainzlab-ruby/blob/main/CHANGELOG.md
230
+ source_code_uri: https://github.com/brainz-llc/brainzlab-ruby
231
+ changelog_uri: https://github.com/brainz-llc/brainzlab-ruby/blob/main/CHANGELOG.md
231
232
  documentation_uri: https://docs.brainzlab.ai/sdk/ruby
232
233
  rubygems_mfa_required: 'true'
233
- github_repo: ssh://github.com/brainz-lab/brainzlab-ruby
234
+ github_repo: ssh://github.com/brainz-llc/brainzlab-ruby
234
235
  rdoc_options: []
235
236
  require_paths:
236
237
  - lib
@@ -247,6 +248,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
247
248
  requirements: []
248
249
  rubygems_version: 3.6.9
249
250
  specification_version: 4
250
- summary: Ruby SDK for BrainzLab - Recall logging, Reflex error tracking, and Pulse
251
- APM
251
+ summary: Ruby SDK for BrainzLab observability
252
252
  test_files: []