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.

The normal 2.8 MB fallback leaves room beneath the 3 MB wire target. Trace transport may first preserve a carrier up to 7.8 MB when its single-span request compresses below that target and stays under the 8 MB raw ceiling.

Constant Summary collapse

MAX_SPAN_CARRIER_BYTES =
2_800_000
MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES =
7_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

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.



43
44
45
# File 'lib/bitfab/payload_budget.rb', line 43

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.



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
120
121
# File 'lib/bitfab/payload_budget.rb', line 94

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



123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/bitfab/payload_budget.rb', line 123

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, max_bytes = MAX_SPAN_CARRIER_BYTES, &encode) ⇒ Object

Return a body within the budget, plus the fields that had to be stubbed.



61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/bitfab/payload_budget.rb', line 61

def enforce(payload, body, max_bytes = MAX_SPAN_CARRIER_BYTES, &encode)
  return [body, []] if fits_carrier_budget?(body, max_bytes)
  return [body, []] unless payload.is_a?(Hash)

  trimmed_payload, trimmed = trim(payload, max_bytes, &encode)
  return [body, []] if trimmed_payload.nil?

  mark_trimmed(trimmed_payload, trimmed, max_bytes)
  [encode.call(trimmed_payload), trimmed]
rescue
  [body, []]
end

.fits_carrier_budget?(body, max_bytes = MAX_SPAN_CARRIER_BYTES) ⇒ 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.

Returns:

  • (Boolean)


52
53
54
55
56
57
58
# File 'lib/bitfab/payload_budget.rb', line 52

def fits_carrier_budget?(body, max_bytes = MAX_SPAN_CARRIER_BYTES)
  size = body.bytesize
  return true if size * 2 + 2 <= max_bytes
  return false if size + 2 > max_bytes

  carrier_byte_length(body) <= max_bytes
end

.mark_trimmed(payload, trimmed, max_bytes) ⇒ Object

Record the trim in the payload's own errors, which is what the server reads to flag a trace as incomplete.



140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/bitfab/payload_budget.rb', line 140

def mark_trimmed(payload, trimmed, max_bytes)
  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_bytes}-byte span carrier budget: " \
      "#{trimmed.uniq.join(", ")}"
  }
  payload[key] = errors
end

.trim(payload, max_bytes = MAX_SPAN_CARRIER_BYTES, &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.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/bitfab/payload_budget.rb', line 78

def trim(payload, max_bytes = MAX_SPAN_CARRIER_BYTES, &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, max_bytes)
  end
  nil
end