Module: Bitfab::PayloadBudget
- Defined in:
- lib/bitfab/payload_budget.rb
Overview
The ceiling on a span's encoded payload, and the trimming that enforces it.
A span's whole payload (input, output, contexts, prompt, metadata) ships as a single bitfab.payload string attribute, and the exporter drops any carrier that exceeds the per-request byte ceiling outright rather than trimming it. Capping each value on its own cannot prevent that: two values that each fit can still add up to an undeliverable span. So the budget is enforced on the encoded payload as a whole, and an oversized span ships with its largest fields stubbed instead of vanishing.
The budget is measured on the carrier (the payload re-escaped into the OTLP attribute), not on the payload body, because the carrier is what the exporter weighs. Bounding the body instead leaves escape-heavy content to blow the request ceiling anyway: a body of escaped JSON, Windows paths, or regexes is nearly all backslashes, and every one of them doubles. Measured on a body sized exactly to a 2.4 MB cap, prose produced a 2.4 MB carrier but backslash-dense content produced 4.8 MB, which the exporter dropped.
2.8 MB leaves room beneath the 3 MB request ceiling for the span and request envelopes wrapped around the attribute.
Constant Summary collapse
- MAX_SPAN_CARRIER_BYTES =
2_800_000- STRUCTURAL_SPAN_KEYS =
Span fields that identify the span rather than carry user data. Trimming one would leave a span that no longer says what it is, so they stay whatever the payload costs.
["name", "type", "function_name", "error_source"].freeze
Class Method Summary collapse
-
.carrier_byte_length(body) ⇒ Object
The byte length
bodyoccupies once re-escaped as a JSON string value. -
.clone_trimmable(payload) ⇒ Object
Copy the records holding user data so trimming never mutates the caller's.
- .collect_candidates(containers) ⇒ Object
-
.enforce(payload, body, &encode) ⇒ Object
Return a body within the budget, plus the fields that had to be stubbed.
-
.fits_carrier_budget?(body) ⇒ Boolean
Whether
bodyfits the carrier budget, measuring exactly only when the cheap bounds cannot already decide it. -
.mark_trimmed(payload, trimmed) ⇒ Object
Record the trim in the payload's own errors, which is what the server reads to flag a trace as incomplete.
-
.trim(payload, &encode) ⇒ Object
Stub the largest payload fields until the encoded body fits the budget.
Class Method Details
.carrier_byte_length(body) ⇒ Object
The byte length body occupies once re-escaped as a JSON string value.
body is itself JSON text, so the first encode already replaced every
control character with a \uXXXX sequence. Only " and \ are left to
escape, and each costs exactly one more byte, which bounds the expansion
at 2x and is what lets .fits_carrier_budget? skip this scan for all but
the largest payloads.
41 42 43 |
# File 'lib/bitfab/payload_budget.rb', line 41 def carrier_byte_length(body) body.bytesize + body.count("\"\\\\") + 2 end |
.clone_trimmable(payload) ⇒ Object
Copy the records holding user data so trimming never mutates the caller's.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/bitfab/payload_budget.rb', line 92 def clone_trimmable(payload) copy = payload.dup containers = [] span_data = copy["span_data"] || copy[:span_data] if span_data.is_a?(Hash) clone = span_data.dup copy[copy.key?("span_data") ? "span_data" : :span_data] = clone containers << clone end raw_span = copy["rawSpan"] || copy[:rawSpan] raw_span_data = raw_span.is_a?(Hash) ? (raw_span["span_data"] || raw_span[:span_data]) : nil if raw_span_data.is_a?(Hash) clone = raw_span_data.dup raw_span_copy = raw_span.dup raw_span_copy[raw_span.key?("span_data") ? "span_data" : :span_data] = clone copy[copy.key?("rawSpan") ? "rawSpan" : :rawSpan] = raw_span_copy containers << clone end # No span_data anywhere: a trace-level or otherwise unfamiliar payload. # Trim its own fields rather than give up, so an oversized body still # ships. containers << copy if containers.empty? [copy, containers] end |
.collect_candidates(containers) ⇒ Object
121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/bitfab/payload_budget.rb', line 121 def collect_candidates(containers) candidates = containers.flat_map do |container| container.filter_map do |key, value| next if STRUCTURAL_SPAN_KEYS.include?(key.to_s) || value.nil? begin [container, key, JSON.generate(value).bytesize] rescue next end end end candidates.sort_by { |entry| -entry[2] } end |
.enforce(payload, body, &encode) ⇒ Object
Return a body within the budget, plus the fields that had to be stubbed.
59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/bitfab/payload_budget.rb', line 59 def enforce(payload, body, &encode) return [body, []] if fits_carrier_budget?(body) return [body, []] unless payload.is_a?(Hash) trimmed_payload, trimmed = trim(payload, &encode) return [body, []] if trimmed_payload.nil? mark_trimmed(trimmed_payload, trimmed) [encode.call(trimmed_payload), trimmed] rescue [body, []] end |
.fits_carrier_budget?(body) ⇒ Boolean
Whether body fits the carrier budget, measuring exactly only when the
cheap bounds cannot already decide it. Escaping never shrinks the body and
can at most double it, so anything under half the budget always fits and
anything past the budget never does. Ordinary spans settle on the first
comparison and never pay for the scan.
50 51 52 53 54 55 56 |
# File 'lib/bitfab/payload_budget.rb', line 50 def fits_carrier_budget?(body) size = body.bytesize return true if size * 2 + 2 <= MAX_SPAN_CARRIER_BYTES return false if size + 2 > MAX_SPAN_CARRIER_BYTES carrier_byte_length(body) <= MAX_SPAN_CARRIER_BYTES end |
.mark_trimmed(payload, trimmed) ⇒ Object
Record the trim in the payload's own errors, which is what the server reads to flag a trace as incomplete.
138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/bitfab/payload_budget.rb', line 138 def mark_trimmed(payload, trimmed) key = payload.key?(:errors) ? :errors : "errors" existing = payload[key] errors = existing.is_a?(Array) ? existing.dup : [] errors << { "source" => "sdk", "step" => "payload_budget", "error" => "trimmed oversized field(s) to fit the " \ "#{MAX_SPAN_CARRIER_BYTES}-byte span carrier budget: " \ "#{trimmed.uniq.join(", ")}" } payload[key] = errors end |
.trim(payload, &encode) ⇒ Object
Stub the largest payload fields until the encoded body fits the budget. Returns [trimmed_payload, trimmed_keys], or nil when nothing could be trimmed: the caller then ships the oversized body and lets the exporter report the drop, which still beats silently emptying a span.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/bitfab/payload_budget.rb', line 76 def trim(payload, &encode) copy, containers = clone_trimmable(payload) candidates = collect_candidates(containers) return nil if candidates.empty? trimmed = [] candidates.each do |container, key, size| container[key] = "<unserializable: too_large_#{size}_bytes>" trimmed << key body = encode.call(copy) return [copy, trimmed] if fits_carrier_budget?(body) end nil end |