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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +210 -3
- data/lib/brainzlab/beacon/client.rb +21 -1
- data/lib/brainzlab/configuration.rb +81 -4
- data/lib/brainzlab/cortex/client.rb +21 -1
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +21 -1
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +245 -109
- data/lib/brainzlab/devtools/assets/devtools.js +40 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +1 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +1 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +56 -8
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +2 -2
- data/lib/brainzlab/flux/client.rb +2 -2
- data/lib/brainzlab/instrumentation/active_support_cache.rb +60 -30
- data/lib/brainzlab/instrumentation/net_http.rb +21 -16
- data/lib/brainzlab/instrumentation.rb +6 -0
- data/lib/brainzlab/nerve/client.rb +21 -1
- data/lib/brainzlab/pulse/client.rb +66 -5
- data/lib/brainzlab/pulse.rb +24 -5
- data/lib/brainzlab/rails/log_formatter.rb +1 -1
- data/lib/brainzlab/rails/railtie.rb +18 -3
- data/lib/brainzlab/recall/buffer.rb +3 -1
- data/lib/brainzlab/recall/client.rb +74 -6
- data/lib/brainzlab/recall.rb +19 -2
- data/lib/brainzlab/reflex/client.rb +66 -5
- data/lib/brainzlab/reflex.rb +40 -8
- data/lib/brainzlab/sentinel/client.rb +21 -1
- data/lib/brainzlab/synapse/client.rb +21 -1
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +32 -3
- data/lib/brainzlab/vault/client.rb +21 -1
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +53 -6
- data/lib/brainzlab.rb +67 -0
- data/lib/fluyenta-ruby.rb +3 -0
- metadata +34 -11
|
@@ -48,11 +48,11 @@ module BrainzLab
|
|
|
48
48
|
|
|
49
49
|
response = http.request(request)
|
|
50
50
|
|
|
51
|
-
BrainzLab.
|
|
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.
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
|
|
556
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
BrainzLab
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
BrainzLab
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}")
|
data/lib/brainzlab/pulse.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|