Module: Bitfab::Serialize
- Defined in:
- lib/bitfab/serialize.rb
Constant Summary collapse
- MAX_SERIALIZED_BYTES =
Cap on serialized payload size. Walking arbitrary objects (e.g. an OpenAI client passed as a span input) can produce hundreds of KB to MB of useless internal state. Anything beyond this cap is replaced with a stub so the span still ships and the trace isn’t dropped server-side.
512_000- MAX_SERIALIZE_DEPTH =
Recursion guard for cyclic graphs and pathologically nested structures.
16
Class Method Summary collapse
- .class_name(value) ⇒ Object
-
.deserialize_inputs(item) ⇒ Array(Array, Hash)
Deserialize replay inputs from a span’s data into [args, kwargs].
-
.marshal_value(value) ⇒ String?
Marshal a value to a Base64-encoded string for Ruby-to-Ruby reconstruction.
- .oversized?(value) ⇒ Boolean
- .safe_to_s(value) ⇒ Object
-
.safely_call(value, method, *args) ⇒ Object
Call ‘value.<method>` and return the result, or yield to the fallback block if the call raises.
-
.serialize_inputs(args, kwargs = {}) ⇒ Object
Serialize function inputs (args + kwargs) for span data (human-readable).
-
.serialize_value(value) ⇒ Object
Serialize a value for JSON storage (human-readable).
- .serialize_value_inner(value, depth) ⇒ Object
-
.unmarshal_value(encoded) ⇒ Object
Unmarshal a Base64-encoded string back into a Ruby object.
- .unserializable_stub(value, reason) ⇒ Object
Class Method Details
.class_name(value) ⇒ Object
91 92 93 94 95 |
# File 'lib/bitfab/serialize.rb', line 91 def class_name(value) value.class.name || "Object" rescue "Object" end |
.deserialize_inputs(item) ⇒ Array(Array, Hash)
Deserialize replay inputs from a span’s data into [args, kwargs].
Prefers Marshal-serialized ‘inputSerialized` for type preservation, falls back to the raw `input` field.
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/bitfab/serialize.rb', line 153 def deserialize_inputs(item) input_serialized = item["inputSerialized"] raw_input = item["input"] if input_serialized.is_a?(String) && !input_serialized.empty? begin deserialized = unmarshal_value(input_serialized) if deserialized.is_a?(Hash) && (deserialized.key?(:args) || deserialized.key?(:kwargs)) return [deserialized[:args] || [], deserialized[:kwargs] || {}] end return deserialized.nil? ? [[], {}] : [[deserialized], {}] rescue # Fall through to raw_input end end if raw_input.is_a?(Array) [raw_input, {}] elsif raw_input.is_a?(Hash) [[], raw_input.transform_keys(&:to_sym)] elsif raw_input.nil? [[], {}] else [[raw_input], {}] end end |
.marshal_value(value) ⇒ String?
Marshal a value to a Base64-encoded string for Ruby-to-Ruby reconstruction. Handles arbitrary Ruby objects including custom classes.
128 129 130 131 132 133 134 135 136 |
# File 'lib/bitfab/serialize.rb', line 128 def marshal_value(value) dumped = Marshal.dump(value) return nil if dumped.bytesize > MAX_SERIALIZED_BYTES Base64.strict_encode64(dumped) rescue TypeError, ArgumentError # Some objects (Proc, IO, etc.) can't be marshalled nil end |
.oversized?(value) ⇒ Boolean
101 102 103 104 105 106 107 |
# File 'lib/bitfab/serialize.rb', line 101 def oversized?(value) JSON.dump(value).bytesize > MAX_SERIALIZED_BYTES rescue # If JSON.dump can't handle it, the wire path can't either, so treat as # oversized to force the stub fallback. true end |
.safe_to_s(value) ⇒ Object
84 85 86 87 88 89 |
# File 'lib/bitfab/serialize.rb', line 84 def safe_to_s(value) str = value.to_s str.is_a?(String) ? str : "<#{class_name(value)}: to_s returned non-String>" rescue "<#{class_name(value)}: to_s raised>" end |
.safely_call(value, method, *args) ⇒ Object
Call ‘value.<method>` and return the result, or yield to the fallback block if the call raises. Used to harden every call site that invokes a user-defined method on an arbitrary object.
78 79 80 81 82 |
# File 'lib/bitfab/serialize.rb', line 78 def safely_call(value, method, *args) value.public_send(method, *args) rescue yield end |
.serialize_inputs(args, kwargs = {}) ⇒ Object
Serialize function inputs (args + kwargs) for span data (human-readable).
110 111 112 113 114 115 116 117 118 119 |
# File 'lib/bitfab/serialize.rb', line 110 def serialize_inputs(args, kwargs = {}) serialized = args.map { |arg| serialize_value(arg) } unless kwargs.empty? kw = kwargs.each_with_object({}) do |(k, v), acc| acc[safe_to_s(k)] = serialize_value(v) end serialized << kw end serialized end |
.serialize_value(value) ⇒ Object
Serialize a value for JSON storage (human-readable). Handles primitives, hashes, arrays, and objects with common conversion methods. Note: We intentionally avoid as_json here because it requires ActiveSupport, and we want to keep the SDK dependency-free (stdlib only).
Guarantees:
-
Never raises. Pathological inputs (objects with raising to_s/to_h, cycles, BasicObject subclasses) return a stub string.
-
Never returns a value whose JSON encoding exceeds MAX_SERIALIZED_BYTES. Without this the wire-side JSON.dump in the http client can produce a request that times out or gets rejected, leaving a trace with zero spans.
31 32 33 34 35 36 37 38 |
# File 'lib/bitfab/serialize.rb', line 31 def serialize_value(value) result = serialize_value_inner(value, 0) return result unless oversized?(result) unserializable_stub(value, "too_large") rescue StandardError, SystemStackError unserializable_stub(value, "unexpected_error") end |
.serialize_value_inner(value, depth) ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/bitfab/serialize.rb', line 40 def serialize_value_inner(value, depth) return "<unserializable: max_depth>" if depth > MAX_SERIALIZE_DEPTH case value when nil, true, false, Integer, Float, String value when Hash value.each_with_object({}) do |(k, v), acc| acc[safe_to_s(k)] = serialize_value_inner(v, depth + 1) end when Array value.map { |v| serialize_value_inner(v, depth + 1) } when Set value.map { |v| serialize_value_inner(v, depth + 1) } when Time, DateTime safely_call(value, "iso8601", 3) { safe_to_s(value) } when Date safe_to_s(value) when Symbol value.to_s else if value.respond_to?(:to_h) h = safely_call(value, "to_h") { return unserializable_stub(value, "to_h_raised") } serialize_value_inner(h, depth + 1) elsif value.respond_to?(:to_a) a = safely_call(value, "to_a") { return unserializable_stub(value, "to_a_raised") } serialize_value_inner(a, depth + 1) else safe_to_s(value) end end rescue StandardError, SystemStackError unserializable_stub(value, "inner_error") end |
.unmarshal_value(encoded) ⇒ Object
Unmarshal a Base64-encoded string back into a Ruby object.
142 143 144 |
# File 'lib/bitfab/serialize.rb', line 142 def unmarshal_value(encoded) Marshal.load(Base64.strict_decode64(encoded)) # rubocop:disable Security/MarshalLoad end |
.unserializable_stub(value, reason) ⇒ Object
97 98 99 |
# File 'lib/bitfab/serialize.rb', line 97 def unserializable_stub(value, reason) "<unserializable: #{class_name(value)} (#{reason})>" end |