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

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.

Returns:

  • (Boolean)


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