bitfab 0.36.4 → 0.36.5
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/lib/bitfab/compress.rb +15 -4
- data/lib/bitfab/http_client.rb +14 -5
- data/lib/bitfab/otel.rb +50 -9
- data/lib/bitfab/payload_budget.rb +16 -14
- data/lib/bitfab/serialize.rb +6 -4
- data/lib/bitfab/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49b714ee6d468fc34c0c6009802c5736d33f03275f06d7d7148635b5d70b7caa
|
|
4
|
+
data.tar.gz: 3816843ddea8722c0222cfc0bed5c65ea05f5abb5ff764233e5ab302d609f2d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f5504e7d7622ccf55145b9124ee0d8ca42f774bdaea720a92382af42148bbf16afb72708d673fb359b37a9840364a770e4a1a87003f016aff2ebd1a59a52b38
|
|
7
|
+
data.tar.gz: db5fbc39adf358e640ae315348eb15ceb5798c67747d092eaaf37651cf9a89542ae9c4d8899a44ebfd4425ba623598e9e5af382dec11f119a917d52cf9ccb0dd
|
data/lib/bitfab/compress.rb
CHANGED
|
@@ -12,18 +12,29 @@ module Bitfab
|
|
|
12
12
|
# batches) ride uncompressed.
|
|
13
13
|
MIN_COMPRESSED_BYTES = 8_192
|
|
14
14
|
|
|
15
|
+
PreparedRequest = Struct.new(:body, :content_encoding, :raw_bytes, :wire_bytes)
|
|
16
|
+
|
|
15
17
|
module_function
|
|
16
18
|
|
|
17
19
|
# Returns the body to send and the Content-Encoding it carries, or nil when
|
|
18
20
|
# the body is sent as-is. Compression is best-effort: any failure sends the
|
|
19
21
|
# original body rather than dropping the span.
|
|
20
22
|
def encode_request_body(body)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
prepared = prepare_request_body(body)
|
|
24
|
+
[prepared.body, prepared.content_encoding]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def prepare_request_body(body)
|
|
28
|
+
size = body.bytesize
|
|
29
|
+
return PreparedRequest.new(body, nil, size, size) if ENV[DISABLE_COMPRESSION_ENV]
|
|
30
|
+
return PreparedRequest.new(body, nil, size, size) if size < MIN_COMPRESSED_BYTES
|
|
31
|
+
|
|
32
|
+
compressed = Zlib.gzip(body)
|
|
33
|
+
return PreparedRequest.new(body, nil, size, size) if compressed.bytesize >= size
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
PreparedRequest.new(compressed, "gzip", size, compressed.bytesize)
|
|
25
36
|
rescue
|
|
26
|
-
|
|
37
|
+
PreparedRequest.new(body, nil, body.bytesize, body.bytesize)
|
|
27
38
|
end
|
|
28
39
|
end
|
|
29
40
|
end
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -58,6 +58,16 @@ module Bitfab
|
|
|
58
58
|
# POST an already-encoded body. The span transport encodes its own batches,
|
|
59
59
|
# so routing them back through #request would encode the same data twice.
|
|
60
60
|
def send_encoded(endpoint, body, timeout: nil, max_retries: 1, retry_delay: 0.1)
|
|
61
|
+
send_prepared(
|
|
62
|
+
endpoint,
|
|
63
|
+
Compress.prepare_request_body(body),
|
|
64
|
+
timeout:,
|
|
65
|
+
max_retries:,
|
|
66
|
+
retry_delay:
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def send_prepared(endpoint, prepared, timeout: nil, max_retries: 1, retry_delay: 0.1)
|
|
61
71
|
uri = URI("#{@service_url}#{endpoint}")
|
|
62
72
|
request_timeout = timeout || @timeout
|
|
63
73
|
|
|
@@ -70,9 +80,8 @@ module Bitfab
|
|
|
70
80
|
http.read_timeout = request_timeout
|
|
71
81
|
|
|
72
82
|
req = Net::HTTP::Post.new(uri.path, headers)
|
|
73
|
-
|
|
74
|
-
req
|
|
75
|
-
req.body = encoded_body
|
|
83
|
+
req["Content-Encoding"] = prepared.content_encoding if prepared.content_encoding
|
|
84
|
+
req.body = prepared.body
|
|
76
85
|
|
|
77
86
|
response = http.request(req)
|
|
78
87
|
|
|
@@ -279,8 +288,8 @@ module Bitfab
|
|
|
279
288
|
end
|
|
280
289
|
end
|
|
281
290
|
|
|
282
|
-
def send_transport_request(endpoint,
|
|
283
|
-
|
|
291
|
+
def send_transport_request(endpoint, request, timeout)
|
|
292
|
+
send_prepared(endpoint, request, timeout:, max_retries: 1)
|
|
284
293
|
end
|
|
285
294
|
|
|
286
295
|
# Normalize each entry to a hash with stable string keys, accepting either
|
data/lib/bitfab/otel.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "net/http"
|
|
|
5
5
|
require "time"
|
|
6
6
|
require "opentelemetry/sdk"
|
|
7
7
|
|
|
8
|
+
require_relative "compress"
|
|
8
9
|
require_relative "serialize"
|
|
9
10
|
require_relative "version"
|
|
10
11
|
require_relative "warn_once"
|
|
@@ -22,6 +23,7 @@ module Bitfab
|
|
|
22
23
|
MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES"
|
|
23
24
|
EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY"
|
|
24
25
|
MAX_EXPORT_REQUEST_BYTES = 3_000_000
|
|
26
|
+
MAX_DECOMPRESSED_REQUEST_BYTES = 8_000_000
|
|
25
27
|
MAX_QUEUE_SIZE = 8_192
|
|
26
28
|
DIRECT_MAX_EXPORT_BATCH_SIZE = 512
|
|
27
29
|
DIRECT_MAX_REQUEST_BATCH_SIZE = 8
|
|
@@ -314,13 +316,35 @@ module Bitfab
|
|
|
314
316
|
end
|
|
315
317
|
|
|
316
318
|
def send_batch(envelope, batch)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
request_spans = batch.spans
|
|
320
|
+
request_raw_bytes = batch.size
|
|
321
|
+
already_trimmed = false
|
|
322
|
+
loop do
|
|
323
|
+
if request_raw_bytes <= MAX_DECOMPRESSED_REQUEST_BYTES
|
|
324
|
+
prepared = Compress.prepare_request_body(Encoder.encode_request(envelope, request_spans))
|
|
325
|
+
if prepared.wire_bytes <= @max_request_bytes
|
|
326
|
+
send_with_retries(prepared)
|
|
327
|
+
return true
|
|
328
|
+
end
|
|
329
|
+
end
|
|
321
330
|
|
|
322
|
-
|
|
323
|
-
|
|
331
|
+
if batch.spans.size != 1
|
|
332
|
+
warn_oversized(batch.spans.size)
|
|
333
|
+
return false
|
|
334
|
+
end
|
|
335
|
+
if already_trimmed
|
|
336
|
+
warn_oversized(1)
|
|
337
|
+
return false
|
|
338
|
+
end
|
|
339
|
+
trimmed = Encoder.trim_span(batch.spans.first)
|
|
340
|
+
if trimmed.nil?
|
|
341
|
+
warn_oversized(1)
|
|
342
|
+
return false
|
|
343
|
+
end
|
|
344
|
+
request_spans = [trimmed]
|
|
345
|
+
request_raw_bytes = envelope.size + trimmed.size
|
|
346
|
+
already_trimmed = true
|
|
347
|
+
end
|
|
324
348
|
rescue PayloadTooLargeError
|
|
325
349
|
warn_oversized(batch.spans.size)
|
|
326
350
|
false
|
|
@@ -343,11 +367,11 @@ module Bitfab
|
|
|
343
367
|
)
|
|
344
368
|
end
|
|
345
369
|
|
|
346
|
-
def send_with_retries(
|
|
370
|
+
def send_with_retries(request)
|
|
347
371
|
attempt = 0
|
|
348
372
|
begin
|
|
349
373
|
attempt += 1
|
|
350
|
-
response = @direct_sender.call(OTLP_TRACES_ENDPOINT,
|
|
374
|
+
response = @direct_sender.call(OTLP_TRACES_ENDPOINT, request, EXPORT_TIMEOUT_MILLIS / 1000.0)
|
|
351
375
|
check_partial_success(response)
|
|
352
376
|
rescue PartialSuccessError
|
|
353
377
|
raise
|
|
@@ -456,7 +480,10 @@ module Bitfab
|
|
|
456
480
|
# Encoding is the expensive part of a submit and needs no mutual
|
|
457
481
|
# exclusion, so it stays outside the lock.
|
|
458
482
|
name = Encoder.span_name(operation, payload)
|
|
459
|
-
encoded_payload = Serialize.safe_generate(
|
|
483
|
+
encoded_payload = Serialize.safe_generate(
|
|
484
|
+
payload,
|
|
485
|
+
max_carrier_bytes: PayloadBudget::MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
|
|
486
|
+
)
|
|
460
487
|
started_at = Encoder.timestamp(payload, "started_at")
|
|
461
488
|
ended_at = Encoder.timestamp(payload, "ended_at")
|
|
462
489
|
errored = Encoder.error?(payload)
|
|
@@ -616,6 +643,20 @@ module Bitfab
|
|
|
616
643
|
EncodedSpan.new(encoded, encoded.bytesize)
|
|
617
644
|
end
|
|
618
645
|
|
|
646
|
+
def trim_span(span)
|
|
647
|
+
carrier = JSON.parse(span.encoded)
|
|
648
|
+
attribute = carrier.fetch("attributes").find { |entry| entry["key"] == PAYLOAD_ATTRIBUTE }
|
|
649
|
+
return nil if attribute.nil?
|
|
650
|
+
|
|
651
|
+
value = attribute.fetch("value")
|
|
652
|
+
payload = JSON.parse(value.fetch("stringValue"))
|
|
653
|
+
value["stringValue"] = Serialize.safe_generate(payload)
|
|
654
|
+
encoded = JSON.generate(carrier)
|
|
655
|
+
EncodedSpan.new(encoded, encoded.bytesize)
|
|
656
|
+
rescue KeyError, JSON::ParserError, TypeError
|
|
657
|
+
nil
|
|
658
|
+
end
|
|
659
|
+
|
|
619
660
|
def request_envelope(first)
|
|
620
661
|
scope = first.instrumentation_scope
|
|
621
662
|
resource = JSON.generate({"attributes" => attributes(first.resource.attribute_enumerator.to_h)})
|
|
@@ -19,10 +19,12 @@ module Bitfab
|
|
|
19
19
|
# on a body sized exactly to a 2.4 MB cap, prose produced a 2.4 MB carrier but
|
|
20
20
|
# backslash-dense content produced 4.8 MB, which the exporter dropped.
|
|
21
21
|
#
|
|
22
|
-
# 2.8 MB leaves room beneath the 3 MB
|
|
23
|
-
#
|
|
22
|
+
# The normal 2.8 MB fallback leaves room beneath the 3 MB wire target. Trace
|
|
23
|
+
# transport may first preserve a carrier up to 7.8 MB when its single-span
|
|
24
|
+
# request compresses below that target and stays under the 8 MB raw ceiling.
|
|
24
25
|
module PayloadBudget
|
|
25
26
|
MAX_SPAN_CARRIER_BYTES = 2_800_000
|
|
27
|
+
MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES = 7_800_000
|
|
26
28
|
|
|
27
29
|
# Span fields that identify the span rather than carry user data. Trimming
|
|
28
30
|
# one would leave a span that no longer says what it is, so they stay
|
|
@@ -47,23 +49,23 @@ module Bitfab
|
|
|
47
49
|
# can at most double it, so anything under half the budget always fits and
|
|
48
50
|
# anything past the budget never does. Ordinary spans settle on the first
|
|
49
51
|
# comparison and never pay for the scan.
|
|
50
|
-
def fits_carrier_budget?(body)
|
|
52
|
+
def fits_carrier_budget?(body, max_bytes = MAX_SPAN_CARRIER_BYTES)
|
|
51
53
|
size = body.bytesize
|
|
52
|
-
return true if size * 2 + 2 <=
|
|
53
|
-
return false if size + 2 >
|
|
54
|
+
return true if size * 2 + 2 <= max_bytes
|
|
55
|
+
return false if size + 2 > max_bytes
|
|
54
56
|
|
|
55
|
-
carrier_byte_length(body) <=
|
|
57
|
+
carrier_byte_length(body) <= max_bytes
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
# Return a body within the budget, plus the fields that had to be stubbed.
|
|
59
|
-
def enforce(payload, body, &encode)
|
|
60
|
-
return [body, []] if fits_carrier_budget?(body)
|
|
61
|
+
def enforce(payload, body, max_bytes = MAX_SPAN_CARRIER_BYTES, &encode)
|
|
62
|
+
return [body, []] if fits_carrier_budget?(body, max_bytes)
|
|
61
63
|
return [body, []] unless payload.is_a?(Hash)
|
|
62
64
|
|
|
63
|
-
trimmed_payload, trimmed = trim(payload, &encode)
|
|
65
|
+
trimmed_payload, trimmed = trim(payload, max_bytes, &encode)
|
|
64
66
|
return [body, []] if trimmed_payload.nil?
|
|
65
67
|
|
|
66
|
-
mark_trimmed(trimmed_payload, trimmed)
|
|
68
|
+
mark_trimmed(trimmed_payload, trimmed, max_bytes)
|
|
67
69
|
[encode.call(trimmed_payload), trimmed]
|
|
68
70
|
rescue
|
|
69
71
|
[body, []]
|
|
@@ -73,7 +75,7 @@ module Bitfab
|
|
|
73
75
|
# Returns [trimmed_payload, trimmed_keys], or nil when nothing could be
|
|
74
76
|
# trimmed: the caller then ships the oversized body and lets the exporter
|
|
75
77
|
# report the drop, which still beats silently emptying a span.
|
|
76
|
-
def trim(payload, &encode)
|
|
78
|
+
def trim(payload, max_bytes = MAX_SPAN_CARRIER_BYTES, &encode)
|
|
77
79
|
copy, containers = clone_trimmable(payload)
|
|
78
80
|
candidates = collect_candidates(containers)
|
|
79
81
|
return nil if candidates.empty?
|
|
@@ -83,7 +85,7 @@ module Bitfab
|
|
|
83
85
|
container[key] = "<unserializable: too_large_#{size}_bytes>"
|
|
84
86
|
trimmed << key
|
|
85
87
|
body = encode.call(copy)
|
|
86
|
-
return [copy, trimmed] if fits_carrier_budget?(body)
|
|
88
|
+
return [copy, trimmed] if fits_carrier_budget?(body, max_bytes)
|
|
87
89
|
end
|
|
88
90
|
nil
|
|
89
91
|
end
|
|
@@ -135,7 +137,7 @@ module Bitfab
|
|
|
135
137
|
|
|
136
138
|
# Record the trim in the payload's own errors, which is what the server
|
|
137
139
|
# reads to flag a trace as incomplete.
|
|
138
|
-
def mark_trimmed(payload, trimmed)
|
|
140
|
+
def mark_trimmed(payload, trimmed, max_bytes)
|
|
139
141
|
key = payload.key?(:errors) ? :errors : "errors"
|
|
140
142
|
existing = payload[key]
|
|
141
143
|
errors = existing.is_a?(Array) ? existing.dup : []
|
|
@@ -143,7 +145,7 @@ module Bitfab
|
|
|
143
145
|
"source" => "sdk",
|
|
144
146
|
"step" => "payload_budget",
|
|
145
147
|
"error" => "trimmed oversized field(s) to fit the " \
|
|
146
|
-
"#{
|
|
148
|
+
"#{max_bytes}-byte span carrier budget: " \
|
|
147
149
|
"#{trimmed.uniq.join(", ")}"
|
|
148
150
|
}
|
|
149
151
|
payload[key] = errors
|
data/lib/bitfab/serialize.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Bitfab
|
|
|
14
14
|
# further. It is deliberately the same number as the whole-span budget: one
|
|
15
15
|
# legitimately large value may use the entire budget, and PayloadBudget is
|
|
16
16
|
# what enforces the total once every field is in.
|
|
17
|
-
MAX_SERIALIZED_BYTES = PayloadBudget::
|
|
17
|
+
MAX_SERIALIZED_BYTES = PayloadBudget::MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
|
|
18
18
|
|
|
19
19
|
# Recursion guard for cyclic graphs and pathologically nested structures.
|
|
20
20
|
MAX_SERIALIZE_DEPTH = 16
|
|
@@ -260,18 +260,20 @@ module Bitfab
|
|
|
260
260
|
# raises and stubs strays) instead of letting JSON.generate raise and drop
|
|
261
261
|
# the whole span/trace silently. A degraded payload warns loudly so the
|
|
262
262
|
# trace isn't quietly left incomplete or not replayable.
|
|
263
|
-
def safe_generate(payload)
|
|
263
|
+
def safe_generate(payload, max_carrier_bytes: PayloadBudget::MAX_SPAN_CARRIER_BYTES)
|
|
264
264
|
# Budget the value that was actually encoded, not the caller's original: a
|
|
265
265
|
# cyclic or otherwise non-encodable field cannot be sized (JSON.generate
|
|
266
266
|
# raises on it), so on the original graph the biggest field is skipped as
|
|
267
267
|
# a trim candidate and the oversized body ships anyway. The sanitized copy
|
|
268
268
|
# has those values already replaced with stubs, so every field is sizeable.
|
|
269
269
|
body, encoded = encode_unbounded(payload)
|
|
270
|
-
body, trimmed = PayloadBudget.enforce(encoded, body)
|
|
270
|
+
body, trimmed = PayloadBudget.enforce(encoded, body, max_carrier_bytes) do |value|
|
|
271
|
+
encode_unbounded(value).first
|
|
272
|
+
end
|
|
271
273
|
if trimmed.any?
|
|
272
274
|
Bitfab.warn_once(
|
|
273
275
|
"payload-over-budget",
|
|
274
|
-
"a span payload exceeded the #{
|
|
276
|
+
"a span payload exceeded the #{max_carrier_bytes}-byte carrier budget; its " \
|
|
275
277
|
"largest field(s) (#{trimmed.uniq.join(", ")}) were replaced with placeholders " \
|
|
276
278
|
"so the span still ships. The span is incomplete and may not be replayable."
|
|
277
279
|
)
|
data/lib/bitfab/version.rb
CHANGED