debugbundle 1.2.0 → 1.4.0
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/LICENSE +202 -0
- data/README.md +2 -2
- data/debugbundle.gemspec +2 -2
- data/lib/debugbundle/acknowledgement.rb +59 -0
- data/lib/debugbundle/before_send.rb +172 -0
- data/lib/debugbundle/client.rb +99 -225
- data/lib/debugbundle/client_event_support.rb +247 -0
- data/lib/debugbundle/config.rb +5 -2
- data/lib/debugbundle/rails.rb +4 -0
- data/lib/debugbundle/transport.rb +13 -3
- data/lib/debugbundle/version.rb +1 -1
- data/lib/debugbundle.rb +2 -0
- data/spec/before_send_spec.rb +74 -0
- data/spec/client_spec.rb +196 -0
- data/spec/remote_config_spec.rb +63 -385
- data/spec/spec_helper.rb +4 -1
- metadata +8 -3
data/lib/debugbundle/client.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'digest'
|
|
|
4
4
|
require 'time'
|
|
5
5
|
require 'uri'
|
|
6
6
|
|
|
7
|
+
require 'debugbundle/client_event_support'
|
|
7
8
|
require 'debugbundle/runtime'
|
|
8
9
|
|
|
9
10
|
module DebugBundle
|
|
@@ -96,6 +97,7 @@ module DebugBundle
|
|
|
96
97
|
@last_event_at = nil
|
|
97
98
|
@retry_at = nil
|
|
98
99
|
@consecutive_failures = 0
|
|
100
|
+
@acknowledgement_state = nil
|
|
99
101
|
@at_exit_registered = false
|
|
100
102
|
@thread_exception_registered = false
|
|
101
103
|
@logger_bindings = {}
|
|
@@ -110,6 +112,10 @@ module DebugBundle
|
|
|
110
112
|
end
|
|
111
113
|
|
|
112
114
|
def capture_exception(error, context: nil, handled: true)
|
|
115
|
+
capture_exception_internal(error, context: context, handled: handled, run_before_send: true)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def capture_exception_internal(error, context:, handled:, run_before_send:)
|
|
113
119
|
return unless capture_enabled?
|
|
114
120
|
|
|
115
121
|
poll_remote_config_if_due!
|
|
@@ -133,11 +139,22 @@ module DebugBundle
|
|
|
133
139
|
extra_context = merged_context.except('request', 'response', 'correlation')
|
|
134
140
|
extra_context['causes'] = causes unless causes.empty?
|
|
135
141
|
|
|
136
|
-
|
|
142
|
+
event = base_event('backend_exception', payload, extra_context)
|
|
143
|
+
event = apply_before_send(event) if run_before_send
|
|
144
|
+
return if event.nil?
|
|
145
|
+
|
|
146
|
+
event_payload = event.fetch('payload')
|
|
147
|
+
suppression_key = [
|
|
148
|
+
event['event_type'],
|
|
149
|
+
event_payload['name'],
|
|
150
|
+
event_payload['message'],
|
|
151
|
+
event_payload['stack']
|
|
152
|
+
].join(':')
|
|
137
153
|
return unless @suppression.should_capture(suppression_key, now: monotonic_now)
|
|
138
154
|
|
|
139
|
-
enqueue_event(
|
|
155
|
+
enqueue_event(event)
|
|
140
156
|
end
|
|
157
|
+
private :capture_exception_internal
|
|
141
158
|
|
|
142
159
|
def capture_error(error, context: nil, handled: true) = capture_exception(error, context: context, handled: handled)
|
|
143
160
|
|
|
@@ -147,7 +164,6 @@ module DebugBundle
|
|
|
147
164
|
poll_remote_config_if_due!
|
|
148
165
|
|
|
149
166
|
normalized_level = normalize_level(level || :warning)
|
|
150
|
-
return unless level_enabled?(normalized_level)
|
|
151
167
|
|
|
152
168
|
merged_context = merge_context(context)
|
|
153
169
|
payload = {
|
|
@@ -155,7 +171,10 @@ module DebugBundle
|
|
|
155
171
|
'message' => message.to_s,
|
|
156
172
|
'attributes' => merged_context
|
|
157
173
|
}
|
|
158
|
-
|
|
174
|
+
event = apply_before_send(base_event('log_event', payload, merged_context))
|
|
175
|
+
return if event.nil? || !level_enabled?(normalized_level)
|
|
176
|
+
|
|
177
|
+
enqueue_event(event)
|
|
159
178
|
end
|
|
160
179
|
|
|
161
180
|
def capture_request(request, response, context: nil)
|
|
@@ -167,7 +186,6 @@ module DebugBundle
|
|
|
167
186
|
sanitized_request = request_payload(request)
|
|
168
187
|
sanitized_response = response_payload(response)
|
|
169
188
|
response_status = (sanitized_response['status_code'] || 0).to_i
|
|
170
|
-
return unless capture_request_event?(response_status, sanitized_request)
|
|
171
189
|
|
|
172
190
|
payload = {
|
|
173
191
|
'method' => sanitized_request['method'],
|
|
@@ -181,7 +199,12 @@ module DebugBundle
|
|
|
181
199
|
'response_headers' => sanitized_response['headers'],
|
|
182
200
|
'response_body' => sanitized_response['body']
|
|
183
201
|
}
|
|
184
|
-
|
|
202
|
+
event = apply_before_send(
|
|
203
|
+
base_event('request_event', payload, merged_context.merge('request' => sanitized_request))
|
|
204
|
+
)
|
|
205
|
+
return if event.nil? || !capture_request_event?(response_status, sanitized_request)
|
|
206
|
+
|
|
207
|
+
enqueue_event(event)
|
|
185
208
|
end
|
|
186
209
|
|
|
187
210
|
def capture_message(message, level: nil, context: nil)
|
|
@@ -203,18 +226,22 @@ module DebugBundle
|
|
|
203
226
|
if heavy
|
|
204
227
|
return if matching_directives.empty?
|
|
205
228
|
|
|
206
|
-
raw_value =
|
|
207
|
-
|
|
229
|
+
resolved, raw_value = resolve_probe_value(data, block)
|
|
230
|
+
return unless resolved
|
|
231
|
+
|
|
232
|
+
emit_probe_events(label.to_s, normalize_probe_data(raw_value), matching_directives)
|
|
208
233
|
return
|
|
209
234
|
end
|
|
210
235
|
|
|
211
236
|
return if !@probe_buffers.key?(label) && @probe_buffers.size >= config.max_probe_labels
|
|
212
237
|
|
|
213
|
-
raw_value =
|
|
238
|
+
resolved, raw_value = resolve_probe_value(data, block)
|
|
239
|
+
return unless resolved
|
|
240
|
+
|
|
214
241
|
entry = {
|
|
215
242
|
'label' => label.to_s,
|
|
216
|
-
'data' =>
|
|
217
|
-
'
|
|
243
|
+
'data' => normalize_probe_data(raw_value),
|
|
244
|
+
'timestamp' => now.iso8601
|
|
218
245
|
}
|
|
219
246
|
|
|
220
247
|
bucket = (@probe_buffers[label.to_s] ||= [])
|
|
@@ -240,7 +267,13 @@ module DebugBundle
|
|
|
240
267
|
error = $ERROR_INFO
|
|
241
268
|
next unless error.is_a?(Exception)
|
|
242
269
|
|
|
243
|
-
client.
|
|
270
|
+
client.__send__(
|
|
271
|
+
:capture_exception_internal,
|
|
272
|
+
error,
|
|
273
|
+
context: nil,
|
|
274
|
+
handled: false,
|
|
275
|
+
run_before_send: false
|
|
276
|
+
)
|
|
244
277
|
client.flush
|
|
245
278
|
end
|
|
246
279
|
true
|
|
@@ -329,11 +362,7 @@ module DebugBundle
|
|
|
329
362
|
|
|
330
363
|
case result.status_code
|
|
331
364
|
when 200..299
|
|
332
|
-
|
|
333
|
-
@retry_at = nil
|
|
334
|
-
@consecutive_failures = 0
|
|
335
|
-
@last_event_at = now
|
|
336
|
-
true
|
|
365
|
+
handle_successful_result(result, batch)
|
|
337
366
|
when 429
|
|
338
367
|
@consecutive_failures += 1
|
|
339
368
|
retry_after_seconds = (result.retry_after_seconds || 1).clamp(1, RETRY_AFTER_CAP_SECONDS)
|
|
@@ -358,6 +387,7 @@ module DebugBundle
|
|
|
358
387
|
def status
|
|
359
388
|
return :disconnected unless config.enabled?
|
|
360
389
|
return :degraded unless config.configured?
|
|
390
|
+
return @acknowledgement_state if @acknowledgement_state
|
|
361
391
|
return :disconnected if @consecutive_failures >= 3
|
|
362
392
|
return :degraded if rate_limited?
|
|
363
393
|
|
|
@@ -368,6 +398,48 @@ module DebugBundle
|
|
|
368
398
|
|
|
369
399
|
private
|
|
370
400
|
|
|
401
|
+
def handle_successful_result(result, batch)
|
|
402
|
+
decision = Acknowledgement.decide(result.body, batch.length)
|
|
403
|
+
return handle_protocol_failure if decision[:kind] == :protocol_failure
|
|
404
|
+
|
|
405
|
+
if decision[:kind] == :legacy
|
|
406
|
+
remove_buffered_events(batch)
|
|
407
|
+
record_success
|
|
408
|
+
return true
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
retryable_indices = decision.fetch(:retryable_indices)
|
|
412
|
+
retryable_events = retryable_indices.map { |index| batch.fetch(index) }
|
|
413
|
+
remove_buffered_events(batch - retryable_events)
|
|
414
|
+
@last_event_at = now if decision.fetch(:accepted).positive?
|
|
415
|
+
|
|
416
|
+
if retryable_events.any?
|
|
417
|
+
@consecutive_failures += 1
|
|
418
|
+
@retry_at = now + 1
|
|
419
|
+
@acknowledgement_state = :degraded
|
|
420
|
+
false
|
|
421
|
+
else
|
|
422
|
+
@retry_at = nil
|
|
423
|
+
@consecutive_failures = 0
|
|
424
|
+
@acknowledgement_state = decision.fetch(:accepted).positive? ? nil : :disconnected
|
|
425
|
+
decision.fetch(:accepted).positive?
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def handle_protocol_failure
|
|
430
|
+
@consecutive_failures += 1
|
|
431
|
+
@retry_at = now + 1
|
|
432
|
+
@acknowledgement_state = :degraded
|
|
433
|
+
false
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def record_success
|
|
437
|
+
@retry_at = nil
|
|
438
|
+
@consecutive_failures = 0
|
|
439
|
+
@acknowledgement_state = nil
|
|
440
|
+
@last_event_at = now
|
|
441
|
+
end
|
|
442
|
+
|
|
371
443
|
def build_default_transport
|
|
372
444
|
return nil unless config.enabled?
|
|
373
445
|
|
|
@@ -391,204 +463,6 @@ module DebugBundle
|
|
|
391
463
|
)
|
|
392
464
|
end
|
|
393
465
|
|
|
394
|
-
def capture_enabled? = config.enabled? && config.configured?
|
|
395
|
-
|
|
396
|
-
def merge_context(context)
|
|
397
|
-
merged = @context.merge(stringify_hash(context || {}))
|
|
398
|
-
@redactor.redact_value(merged)
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def stringify_hash(value)
|
|
402
|
-
return {} unless value.is_a?(Hash)
|
|
403
|
-
|
|
404
|
-
value.each_with_object({}) do |(key, nested_value), result|
|
|
405
|
-
result[key.to_s] = nested_value
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def request_payload(request)
|
|
410
|
-
source = object_to_hash(request)
|
|
411
|
-
{
|
|
412
|
-
'method' => source['method'] || 'UNKNOWN',
|
|
413
|
-
'path' => source['path'] || '/',
|
|
414
|
-
'query' => @redactor.redact_value(source['query'] || {}),
|
|
415
|
-
'headers' => sanitized_headers(source['headers'] || {}),
|
|
416
|
-
'body' => @redactor.redact_value(source['body'] || {})
|
|
417
|
-
}
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def response_payload(response)
|
|
421
|
-
source = object_to_hash(response)
|
|
422
|
-
{
|
|
423
|
-
'status_code' => source['status_code'] || source['status'] || 0,
|
|
424
|
-
'headers' => sanitized_headers(source['headers'] || {}),
|
|
425
|
-
'body' => @redactor.redact_value(source['body'] || {})
|
|
426
|
-
}
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def runtime_payload = Runtime.payload
|
|
430
|
-
|
|
431
|
-
def exception_causes(error)
|
|
432
|
-
causes = []
|
|
433
|
-
current = error.cause
|
|
434
|
-
|
|
435
|
-
while current
|
|
436
|
-
causes << {
|
|
437
|
-
'name' => current.class.name,
|
|
438
|
-
'message' => current.message.to_s,
|
|
439
|
-
'stack' => Array(current.backtrace).join("\n")
|
|
440
|
-
}
|
|
441
|
-
current = current.cause
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
causes
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
def probe_snapshot
|
|
448
|
-
items = @probe_buffers.values.flatten.map do |entry|
|
|
449
|
-
entry.merge('activation_id' => nil)
|
|
450
|
-
end
|
|
451
|
-
return {} if items.empty?
|
|
452
|
-
|
|
453
|
-
{ 'version' => 1, 'items' => items }
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def enqueue_event(event)
|
|
457
|
-
return unless sampled_in?
|
|
458
|
-
|
|
459
|
-
@buffer_mutex.synchronize do
|
|
460
|
-
@buffer << event
|
|
461
|
-
@buffer.shift while @buffer.length > MAX_BUFFER_SIZE
|
|
462
|
-
end
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
def buffered_batch
|
|
466
|
-
@buffer_mutex.synchronize { @buffer.dup }
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
def remove_buffered_events(events)
|
|
470
|
-
event_ids = events.map { |event| event['event_id'] }
|
|
471
|
-
@buffer_mutex.synchronize do
|
|
472
|
-
@buffer.reject! { |event| event_ids.include?(event['event_id']) }
|
|
473
|
-
end
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def sampled_in?
|
|
477
|
-
return false if config.sample_rate <= 0.0
|
|
478
|
-
return true if config.sample_rate >= 1.0
|
|
479
|
-
|
|
480
|
-
@random_provider.call.to_f < config.sample_rate
|
|
481
|
-
rescue StandardError
|
|
482
|
-
true
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def append_suppression_aggregates
|
|
486
|
-
@suppression.drain_aggregates(now: monotonic_now).each do |aggregate|
|
|
487
|
-
enqueue_event(base_event('error_suppressed', aggregate, {}))
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def base_event(event_type, payload, context)
|
|
492
|
-
event = {
|
|
493
|
-
'schema_version' => SCHEMA_VERSION,
|
|
494
|
-
'event_id' => SecureRandom.uuid,
|
|
495
|
-
'event_type' => event_type,
|
|
496
|
-
'project_token' => config.project_token,
|
|
497
|
-
'sdk_name' => SDK_NAME,
|
|
498
|
-
'sdk_version' => DebugBundle::VERSION,
|
|
499
|
-
'service' => {
|
|
500
|
-
'name' => service_name,
|
|
501
|
-
'runtime' => 'ruby',
|
|
502
|
-
'framework' => context['framework'],
|
|
503
|
-
'environment' => environment_name
|
|
504
|
-
},
|
|
505
|
-
'occurred_at' => now.iso8601,
|
|
506
|
-
'correlation' => correlation_payload(context),
|
|
507
|
-
'payload' => @redactor.redact_value(payload)
|
|
508
|
-
}
|
|
509
|
-
envelope_context = event_context(context)
|
|
510
|
-
event['context'] = envelope_context unless envelope_context.empty?
|
|
511
|
-
event
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
def service_name = config.service || DEFAULT_SERVICE_NAME
|
|
515
|
-
|
|
516
|
-
def environment_name = config.environment || DEFAULT_ENVIRONMENT
|
|
517
|
-
|
|
518
|
-
def correlation_payload(context)
|
|
519
|
-
request = object_to_hash(context['request'])
|
|
520
|
-
correlation = object_to_hash(context['correlation'])
|
|
521
|
-
{
|
|
522
|
-
'request_id' => correlation['request_id'] || request['request_id'] || context['request_id'],
|
|
523
|
-
'trace_id' => correlation['trace_id'] || request['trace_id'] || context['trace_id'],
|
|
524
|
-
'session_id' => correlation['session_id'] || context['session_id'],
|
|
525
|
-
'user_id_hash' => correlation['user_id_hash'] || context['user_id_hash']
|
|
526
|
-
}
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
def event_context(context)
|
|
530
|
-
object_to_hash(context).except(
|
|
531
|
-
'request',
|
|
532
|
-
'response',
|
|
533
|
-
'correlation',
|
|
534
|
-
'request_id',
|
|
535
|
-
'trace_id',
|
|
536
|
-
'session_id',
|
|
537
|
-
'user_id_hash'
|
|
538
|
-
)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def object_to_hash(value)
|
|
542
|
-
case value
|
|
543
|
-
when Hash
|
|
544
|
-
stringify_hash(value)
|
|
545
|
-
else
|
|
546
|
-
if value.respond_to?(:to_h)
|
|
547
|
-
stringify_hash(value.to_h)
|
|
548
|
-
elsif value.respond_to?(:to_hash)
|
|
549
|
-
stringify_hash(value.to_hash)
|
|
550
|
-
else
|
|
551
|
-
{}
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
rescue StandardError
|
|
555
|
-
{}
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
def sanitized_headers(headers)
|
|
559
|
-
stringify_hash(headers).each_with_object({}) do |(key, value), result|
|
|
560
|
-
normalized_key = key.to_s.downcase
|
|
561
|
-
next unless DEFAULT_HEADER_ALLOWLIST.include?(normalized_key)
|
|
562
|
-
|
|
563
|
-
result[normalized_key] = @redactor.redact_value(value)
|
|
564
|
-
end
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
def normalize_level(level)
|
|
568
|
-
candidate = level.to_s.strip.downcase.to_sym
|
|
569
|
-
return candidate if LOG_LEVEL_RANKS.key?(candidate)
|
|
570
|
-
|
|
571
|
-
:warning
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
def level_enabled?(level)
|
|
575
|
-
threshold = [normalize_level(config.log_level), policy_log_level].max_by { |entry| LOG_LEVEL_RANKS.fetch(entry) }
|
|
576
|
-
LOG_LEVEL_RANKS.fetch(level) >= LOG_LEVEL_RANKS.fetch(threshold)
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
def policy_log_level
|
|
580
|
-
case @capture_policy.capture_logs
|
|
581
|
-
when 'off'
|
|
582
|
-
:fatal
|
|
583
|
-
when 'error'
|
|
584
|
-
:error
|
|
585
|
-
when 'info'
|
|
586
|
-
:info
|
|
587
|
-
else
|
|
588
|
-
:warning
|
|
589
|
-
end
|
|
590
|
-
end
|
|
591
|
-
|
|
592
466
|
def capture_request_event?(status_code, request)
|
|
593
467
|
mode = @capture_policy.capture_request_events
|
|
594
468
|
|
|
@@ -667,15 +541,12 @@ module DebugBundle
|
|
|
667
541
|
end
|
|
668
542
|
|
|
669
543
|
def emit_probe_events(label, data, matching_directives)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
allowed_directives.each do |directive|
|
|
678
|
-
enqueue_event(
|
|
544
|
+
request_directives = matching_request_trigger_directives(label)
|
|
545
|
+
candidate_directives = (matching_directives + request_directives).uniq(&:id)
|
|
546
|
+
return if candidate_directives.empty?
|
|
547
|
+
|
|
548
|
+
candidate_directives.each do |directive|
|
|
549
|
+
event = apply_before_send(
|
|
679
550
|
base_event(
|
|
680
551
|
'probe_event',
|
|
681
552
|
{
|
|
@@ -687,6 +558,9 @@ module DebugBundle
|
|
|
687
558
|
{}
|
|
688
559
|
)
|
|
689
560
|
)
|
|
561
|
+
allowed = request_directives.include?(directive) ||
|
|
562
|
+
@capture_policy.capture_probe_events == 'standalone_when_activated'
|
|
563
|
+
enqueue_event(event) if event && allowed
|
|
690
564
|
end
|
|
691
565
|
end
|
|
692
566
|
|
|
@@ -729,7 +603,7 @@ module DebugBundle
|
|
|
729
603
|
end
|
|
730
604
|
|
|
731
605
|
def capture_thread_exception(error)
|
|
732
|
-
|
|
606
|
+
capture_exception_internal(error, context: nil, handled: false, run_before_send: false)
|
|
733
607
|
flush
|
|
734
608
|
rescue StandardError
|
|
735
609
|
nil
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DebugBundle
|
|
4
|
+
class Client
|
|
5
|
+
module EventSupport
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def capture_enabled? = config.enabled? && config.configured?
|
|
9
|
+
|
|
10
|
+
def merge_context(context)
|
|
11
|
+
merged = @context.merge(stringify_hash(context || {}))
|
|
12
|
+
@redactor.redact_value(merged)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stringify_hash(value)
|
|
16
|
+
return {} unless value.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
value.each_with_object({}) do |(key, nested_value), result|
|
|
19
|
+
result[key.to_s] = nested_value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def request_payload(request)
|
|
24
|
+
source = object_to_hash(request)
|
|
25
|
+
{
|
|
26
|
+
'method' => source['method'] || 'UNKNOWN',
|
|
27
|
+
'path' => source['path'] || '/',
|
|
28
|
+
'query' => @redactor.redact_value(source['query'] || {}),
|
|
29
|
+
'headers' => sanitized_headers(source['headers'] || {}),
|
|
30
|
+
'body' => @redactor.redact_value(source['body'] || {})
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def response_payload(response)
|
|
35
|
+
source = object_to_hash(response)
|
|
36
|
+
{
|
|
37
|
+
'status_code' => source['status_code'] || source['status'] || 0,
|
|
38
|
+
'headers' => sanitized_headers(source['headers'] || {}),
|
|
39
|
+
'body' => @redactor.redact_value(source['body'] || {})
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def runtime_payload = Runtime.payload
|
|
44
|
+
|
|
45
|
+
def exception_causes(error)
|
|
46
|
+
causes = []
|
|
47
|
+
current = error.cause
|
|
48
|
+
|
|
49
|
+
while current
|
|
50
|
+
causes << {
|
|
51
|
+
'name' => current.class.name,
|
|
52
|
+
'message' => current.message.to_s,
|
|
53
|
+
'stack' => Array(current.backtrace).join("\n")
|
|
54
|
+
}
|
|
55
|
+
current = current.cause
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
causes
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def probe_snapshot
|
|
62
|
+
items = @probe_buffers.values.flatten.map do |entry|
|
|
63
|
+
entry.merge('activation_id' => nil)
|
|
64
|
+
end
|
|
65
|
+
return {} if items.empty?
|
|
66
|
+
|
|
67
|
+
{ 'version' => 1, 'items' => items }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolve_probe_value(data, block)
|
|
71
|
+
[true, block ? block.call : data]
|
|
72
|
+
rescue StandardError
|
|
73
|
+
[false, nil]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def normalize_probe_data(value)
|
|
77
|
+
redacted = @redactor.redact_value(value)
|
|
78
|
+
redacted.is_a?(Hash) ? redacted : { 'value' => redacted }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def apply_before_send(event)
|
|
82
|
+
BeforeSend.apply(event, config.before_send)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def enqueue_event(event)
|
|
86
|
+
return unless sampled_in?
|
|
87
|
+
|
|
88
|
+
@buffer_mutex.synchronize do
|
|
89
|
+
@buffer << event
|
|
90
|
+
@buffer.shift while @buffer.length > MAX_BUFFER_SIZE
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def buffered_batch
|
|
95
|
+
@buffer_mutex.synchronize { @buffer.dup }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def remove_buffered_events(events)
|
|
99
|
+
event_ids = events.map { |event| event['event_id'] }
|
|
100
|
+
@buffer_mutex.synchronize do
|
|
101
|
+
@buffer.reject! { |event| event_ids.include?(event['event_id']) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def sampled_in?
|
|
106
|
+
return false if config.sample_rate <= 0.0
|
|
107
|
+
return true if config.sample_rate >= 1.0
|
|
108
|
+
|
|
109
|
+
@random_provider.call.to_f < config.sample_rate
|
|
110
|
+
rescue StandardError
|
|
111
|
+
true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def append_suppression_aggregates
|
|
115
|
+
@suppression.drain_aggregates(now: monotonic_now).each do |aggregate|
|
|
116
|
+
event = apply_before_send(base_event('error_suppressed', aggregate, {}))
|
|
117
|
+
enqueue_event(event) if event
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def base_event(event_type, payload, context)
|
|
122
|
+
redacted_payload = @redactor.redact_value(payload)
|
|
123
|
+
preserve_redacted_probe_data!(event_type, payload, redacted_payload)
|
|
124
|
+
event = {
|
|
125
|
+
'schema_version' => SCHEMA_VERSION,
|
|
126
|
+
'event_id' => SecureRandom.uuid,
|
|
127
|
+
'event_type' => event_type,
|
|
128
|
+
'project_token' => config.project_token,
|
|
129
|
+
'sdk_name' => SDK_NAME,
|
|
130
|
+
'sdk_version' => DebugBundle::VERSION,
|
|
131
|
+
'service' => {
|
|
132
|
+
'name' => service_name,
|
|
133
|
+
'runtime' => 'ruby',
|
|
134
|
+
'framework' => context['framework'],
|
|
135
|
+
'environment' => environment_name
|
|
136
|
+
},
|
|
137
|
+
'occurred_at' => now.iso8601,
|
|
138
|
+
'correlation' => correlation_payload(context),
|
|
139
|
+
'payload' => redacted_payload
|
|
140
|
+
}
|
|
141
|
+
envelope_context = event_context(context)
|
|
142
|
+
event['context'] = envelope_context unless envelope_context.empty?
|
|
143
|
+
event
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Probe values are redacted before entering the in-memory probe buffer. Re-running
|
|
147
|
+
# the depth limiter after nesting them inside an event would replace valid scalar
|
|
148
|
+
# and list values with truncation markers.
|
|
149
|
+
def preserve_redacted_probe_data!(event_type, payload, redacted_payload)
|
|
150
|
+
if event_type == 'backend_exception' && payload.key?('probe_data')
|
|
151
|
+
preserve_probe_snapshot_values!(payload, redacted_payload)
|
|
152
|
+
elsif event_type == 'probe_event' && payload.key?('data')
|
|
153
|
+
redacted_payload['data'] = payload['data']
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def preserve_probe_snapshot_values!(payload, redacted_payload)
|
|
158
|
+
original_items = payload.dig('probe_data', 'items')
|
|
159
|
+
redacted_items = redacted_payload.dig('probe_data', 'items')
|
|
160
|
+
return unless original_items.is_a?(Array) && redacted_items.is_a?(Array)
|
|
161
|
+
|
|
162
|
+
original_items.zip(redacted_items).each do |original, redacted|
|
|
163
|
+
redacted['data'] = original['data'] if original.is_a?(Hash) && redacted.is_a?(Hash)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def service_name = config.service || DEFAULT_SERVICE_NAME
|
|
168
|
+
|
|
169
|
+
def environment_name = config.environment || DEFAULT_ENVIRONMENT
|
|
170
|
+
|
|
171
|
+
def correlation_payload(context)
|
|
172
|
+
request = object_to_hash(context['request'])
|
|
173
|
+
correlation = object_to_hash(context['correlation'])
|
|
174
|
+
{
|
|
175
|
+
'request_id' => correlation['request_id'] || request['request_id'] || context['request_id'],
|
|
176
|
+
'trace_id' => correlation['trace_id'] || request['trace_id'] || context['trace_id'],
|
|
177
|
+
'session_id' => correlation['session_id'] || context['session_id'],
|
|
178
|
+
'user_id_hash' => correlation['user_id_hash'] || context['user_id_hash']
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def event_context(context)
|
|
183
|
+
object_to_hash(context).except(
|
|
184
|
+
'request',
|
|
185
|
+
'response',
|
|
186
|
+
'correlation',
|
|
187
|
+
'request_id',
|
|
188
|
+
'trace_id',
|
|
189
|
+
'session_id',
|
|
190
|
+
'user_id_hash'
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def object_to_hash(value)
|
|
195
|
+
case value
|
|
196
|
+
when Hash
|
|
197
|
+
stringify_hash(value)
|
|
198
|
+
else
|
|
199
|
+
return stringify_hash(value.to_h) if value.respond_to?(:to_h)
|
|
200
|
+
return stringify_hash(value.to_hash) if value.respond_to?(:to_hash)
|
|
201
|
+
|
|
202
|
+
{}
|
|
203
|
+
end
|
|
204
|
+
rescue StandardError
|
|
205
|
+
{}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def sanitized_headers(headers)
|
|
209
|
+
stringify_hash(headers).each_with_object({}) do |(key, value), result|
|
|
210
|
+
normalized_key = key.to_s.downcase
|
|
211
|
+
next unless DEFAULT_HEADER_ALLOWLIST.include?(normalized_key)
|
|
212
|
+
|
|
213
|
+
result[normalized_key] = @redactor.redact_value(value)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def normalize_level(level)
|
|
218
|
+
candidate = level.to_s.strip.downcase.to_sym
|
|
219
|
+
return candidate if LOG_LEVEL_RANKS.key?(candidate)
|
|
220
|
+
|
|
221
|
+
:warning
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def level_enabled?(level)
|
|
225
|
+
threshold = [normalize_level(config.log_level), policy_log_level].max_by do |entry|
|
|
226
|
+
LOG_LEVEL_RANKS.fetch(entry)
|
|
227
|
+
end
|
|
228
|
+
LOG_LEVEL_RANKS.fetch(level) >= LOG_LEVEL_RANKS.fetch(threshold)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def policy_log_level
|
|
232
|
+
case @capture_policy.capture_logs
|
|
233
|
+
when 'off'
|
|
234
|
+
:fatal
|
|
235
|
+
when 'error'
|
|
236
|
+
:error
|
|
237
|
+
when 'info'
|
|
238
|
+
:info
|
|
239
|
+
else
|
|
240
|
+
:warning
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
include EventSupport
|
|
246
|
+
end
|
|
247
|
+
end
|