foam-otel 2.1.0 → 2.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65833871aff8351bab7083f4a9d636afd5704cd1327e95bc4af117437d12ad85
4
- data.tar.gz: 206b1b6bfe1633b44267283c775b63ae47cd429be0bc9260c27ab89fd4f8b6ed
3
+ metadata.gz: 894986849774ca1e6ee37ed18eccb168fe0b026a438abdd53432b46a5de4b9d4
4
+ data.tar.gz: c59a29ea7223dc7a92500303a2615d03d048fcf70927c12e37250d21a8785f57
5
5
  SHA512:
6
- metadata.gz: c7c97cf55df3769670636aab31b8dafa53ee8975b4648bd1339e167f5af63245609355b547e25eefcaefd68f155e8e5934e739a54f012a5137231f343a966a2e
7
- data.tar.gz: eb600f006bdce6aabf407e8b7df5f1e2fe7ece7a7f7590ac956689a6d1c148e0b5a1d32a4095606608d931f3792a908bb901fe06f804e49f52eda366b6e97650
6
+ metadata.gz: 5cddf45218f81f9c18ab2557c7737a53da552bdf429524e6ef1f1cb64fbc84e19757de30d370e6e1217cf2a5217d271b3c4a19b6b8610d36f1846699e96a2cf6
7
+ data.tar.gz: 2199d01ee1f3cb89ae50d721ad6110c6c803256ac5093ba81dfc070e8cdb3af460ff13d653e6f9dd307f5a5005673e178693ca5a630459e698adc7e29a00b76e
data/GOTCHAS.md CHANGED
@@ -971,10 +971,13 @@ exfiltratable — the value-pattern secret layer is the required second control
971
971
  foam's promise is coexistence: the app must keep working.
972
972
  THE SAME COLLISION EXISTS ON A SECOND CLASS: dd-trace-rb's httprb patch
973
973
  and the contrib `opentelemetry-instrumentation-http` patch both define
974
- `annotate_span_with_response!` on `HTTP::Client` (3-arg vs 2-arg) —
975
- upstream tracks the combination as unsupported (dd-trace-rb#3575,
976
- opentelemetry-ruby#1625, httprb/http#838). A `datadog/auto_instrument` +
974
+ `annotate_span_with_response!` on `HTTP::Client` (3-arg vs 2-arg) — the
975
+ colliding definitions verified at source (below). A
976
+ `datadog/auto_instrument` +
977
977
  contrib-http process crashes every HTTP.rb request the same way.
978
+ (The upstream issue numbers the introducing PR cited for this twin could
979
+ NOT be verified as tracking it and are withdrawn — the colliding source
980
+ definitions are the primary evidence.)
978
981
  - **Sources**:
979
982
  - Installed source — the two colliding definitions:
980
983
  opentelemetry-instrumentation-net_http 0.29.0
@@ -986,11 +989,9 @@ exfiltratable — the value-pattern secret layer is the required second control
986
989
  - The HTTP::Client twin: dd-trace-rb
987
990
  `lib/datadog/tracing/contrib/httprb/instrumentation.rb`
988
991
  (3-arg `annotate_span_with_response!(span, response, options)`) vs
989
- contrib opentelemetry-instrumentation-http `patches/client.rb`
990
- (2-arg); upstream reports
991
- https://github.com/DataDog/dd-trace-rb/issues/3575 ,
992
- https://github.com/open-telemetry/opentelemetry-ruby/issues/1625 ,
993
- https://github.com/httprb/http/issues/838 .
992
+ contrib opentelemetry-instrumentation-http
993
+ `lib/opentelemetry/instrumentation/http/patches/stable/client.rb`
994
+ (2-arg) — both definitions verified at source.
994
995
  - Ruby semantics: prepended modules join the class's ancestor chain and
995
996
  private methods resolve through the SAME chain for every caller — there
996
997
  is no per-module helper namespace.
@@ -141,23 +141,42 @@ module Foam
141
141
 
142
142
  # Reads the finished JSON body for capture WITHOUT taking anything
143
143
  # away from the app: only when the body is bounded (declared
144
- # Content-Length within the cap) and a replacement Body can be
144
+ # Content-Length within the cap, no Content-Encoding a gzip
145
+ # body's declared length is the COMPRESSED size, and the inflated
146
+ # stream could dwarf the cap) and a replacement Body can be
145
147
  # built (probed BEFORE consuming — if HTTP.rb's internals ever
146
148
  # drift, foam skips capture rather than risk the app's stream).
147
149
  # The drained bytes are re-wrapped in a fresh HTTP::Response::Body
148
150
  # over an EOF-compatible buffer and swapped back, so app-side
149
151
  # #to_s, #each and #readpartial behave exactly as if foam had
150
- # never read the response.
152
+ # never read the response. A transport failure MID-READ is
153
+ # re-armed: the swapped-in body re-raises the ORIGINAL exception
154
+ # on the app's own read — never a misleading StateError. The read
155
+ # itself inherits the app's HTTP.rb timeout configuration (bytes
156
+ # are capped here; deadlines belong to the client's .timeout).
151
157
  def readable_json_body(response)
152
158
  length = content_length(response)
153
159
  return nil unless length&.positive? && length <= BODY_PARSE_BYTE_CAP
160
+ return nil if content_encoded?(response)
154
161
  return nil unless response.instance_variable_defined?(:@body)
155
162
  return nil unless build_body("") # constructor probe, pre-consumption
156
163
 
157
- contents = response.body.to_s
158
- replacement = build_body(contents)
159
- response.instance_variable_set(:@body, replacement) if replacement
160
- contents
164
+ contents = begin
165
+ response.body.to_s
166
+ rescue StandardError, SystemStackError => e
167
+ rearm_failed_body(response, e)
168
+ return nil
169
+ end
170
+ replacement = build_body(contents) ||
171
+ build_body(contents.dup.force_encoding(Encoding::BINARY))
172
+ # No hand-back, no capture: foam never profits from a stream it
173
+ # failed to return to the app.
174
+ return nil unless replacement
175
+
176
+ response.instance_variable_set(:@body, replacement)
177
+ # Belt over the declared length: never parse more than the cap
178
+ # actually materialized (a lying Content-Length header).
179
+ contents.bytesize <= BODY_PARSE_BYTE_CAP ? contents : nil
161
180
  rescue StandardError
162
181
  nil
163
182
  end
@@ -169,11 +188,29 @@ module Foam
169
188
  nil
170
189
  end
171
190
 
191
+ def content_encoded?(response)
192
+ return false unless response.respond_to?(:headers)
193
+
194
+ encoding = response.headers["Content-Encoding"].to_s
195
+ !encoding.empty? && encoding.downcase != "identity"
196
+ rescue StandardError
197
+ true # unknown => assume encoded, skip capture
198
+ end
199
+
172
200
  def build_body(contents)
173
201
  ::HTTP::Response::Body.new(BufferedBodyStream.new(contents), encoding: contents.encoding)
174
202
  rescue StandardError
175
203
  nil
176
204
  end
205
+
206
+ # A read that died under foam must die IDENTICALLY under the app:
207
+ # swap in a body whose reads re-raise the original transport error.
208
+ def rearm_failed_body(response, error)
209
+ replacement = ::HTTP::Response::Body.new(FailingBodyStream.new(error), encoding: Encoding::BINARY)
210
+ response.instance_variable_set(:@body, replacement)
211
+ rescue StandardError
212
+ nil
213
+ end
177
214
  end
178
215
 
179
216
  # Matches HTTP::Connection's readpartial contract (nil at EOF, never
@@ -192,6 +229,18 @@ module Foam
192
229
  end
193
230
  end
194
231
 
232
+ # Re-raises the transport error foam swallowed mid-capture, so the
233
+ # app's own read fails with the REAL exception class and message.
234
+ class FailingBodyStream
235
+ def initialize(error)
236
+ @error = error
237
+ end
238
+
239
+ def readpartial(*_args)
240
+ raise @error
241
+ end
242
+ end
243
+
195
244
  # Non-streaming JSON Message → response attributes; usage rides
196
245
  # {input_tokens, output_tokens} exactly like the SDK shim.
197
246
  RESPONSE = lambda do |response|
@@ -234,8 +283,8 @@ module Foam
234
283
 
235
284
  # Response attributes. The status code is stamped ONLY on the error
236
285
  # path (the observed error signal — fleet gen_ai spans carry no
237
- # http.* on success; python removed exactly this success-path stamp
238
- # as a deliberate wire correction). error.type carries the bare
286
+ # http.* on success, the posture python's gap-filler also converged
287
+ # on). error.type carries the bare
239
288
  # status string ("400"), matching the span status description and
240
289
  # semconv's well-known values — never an invented format.
241
290
  EXTRACTOR = lambda do |response|
@@ -125,7 +125,9 @@ module Foam
125
125
  def embeddings(parameters: {})
126
126
  Foam::Otel::LLM.observe(
127
127
  provider: "openai", operation: "embeddings",
128
- request: Foam::Otel::LLM.safe_request(provider: "openai") { RubyOpenAIShim.embeddings_request(parameters) },
128
+ request: Foam::Otel::LLM.safe_request(provider: "openai", operation: "embeddings") do
129
+ RubyOpenAIShim.embeddings_request(parameters)
130
+ end,
129
131
  response_extractor: RubyOpenAIShim::EMBEDDINGS_RESPONSE
130
132
  ) { super }
131
133
  end
@@ -238,12 +238,36 @@ module Foam
238
238
  # contrib http patch define annotate_span_with_response! at
239
239
  # non-identical signatures (3-arg vs 2-arg), the exact F18 Net::HTTP
240
240
  # collision on a second class, live in the wild under
241
- # datadog/auto_instrument (upstream tracks it as unsupported:
242
- # dd-trace-rb#3575, opentelemetry-ruby#1625, httprb/http#838); the
241
+ # datadog/auto_instrument (both colliding definitions verified at
242
+ # source GOTCHAS F18); the
243
243
  # signature-dispatch shim lets both agents keep working WHEN both
244
244
  # vendor patches precede foam's init-time scan (F18 residual:
245
245
  # initialize foam after the vendor agents; a later-arriving patch is
246
246
  # unshielded until a re-init/fork re-scan).
247
- VERSION = "2.1.0"
247
+ # 2.1.1: LLM CAPTURE HARDENING + RESPONSE-STYLE MATRICES (PATCH — bug
248
+ # fixes and tests only, no new surface). HttpAnthropicShim's body
249
+ # capture: content-encoded (gzip) responses are never captured (the
250
+ # declared Content-Length is the COMPRESSED size — the 32 KiB cap
251
+ # could be bypassed by inflation; a post-materialization bytesize
252
+ # guard also covers lying Content-Length); a transport failure DURING
253
+ # capture now re-raises the ORIGINAL error on the app's own read
254
+ # (was: a misleading HTTP::StateError); if the body hand-back cannot
255
+ # be rebuilt, capture is skipped entirely. Degraded ruby-openai
256
+ # embeddings spans keep gen_ai.operation.name="embeddings" (the
257
+ # fallback stamped "chat" on a span named "embeddings openai").
258
+ # GOTCHAS F18 citation hygiene: the three upstream issue links cited
259
+ # for the HTTP::Client collision could not be verified and are
260
+ # withdrawn; the colliding source definitions are the evidence.
261
+ # Tests: full response-style matrices for both providers — every
262
+ # documented Anthropic /v1/messages shape (parallel tool_use,
263
+ # thinking/redacted_thinking, all stop_reasons incl.
264
+ # model_context_window_exceeded, citation splitting, server-tool
265
+ # round trips, error envelopes 400-529, non-object/invalid JSON) and
266
+ # every documented OpenAI chat/embeddings shape (all finish_reasons
267
+ # + unknown pass-through, parallel/custom/legacy tool calls,
268
+ # refusal, n>1 choices, usage-detail variants, Faraday error
269
+ # classes with observed status, base64 embeddings) — plus gzip
270
+ # skip-capture, mid-read re-arm, and restored-body #each pins.
271
+ VERSION = "2.1.1"
248
272
  end
249
273
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foam-otel
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foam