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 +4 -4
- data/GOTCHAS.md +9 -8
- data/lib/foam/otel/llm/http_anthropic_shim.rb +57 -8
- data/lib/foam/otel/llm/ruby_openai_shim.rb +3 -1
- data/lib/foam/otel/version.rb +27 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 894986849774ca1e6ee37ed18eccb168fe0b026a438abdd53432b46a5de4b9d4
|
|
4
|
+
data.tar.gz: c59a29ea7223dc7a92500303a2615d03d048fcf70927c12e37250d21a8785f57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
976
|
-
|
|
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
|
|
990
|
-
|
|
991
|
-
|
|
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
|
|
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 =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
238
|
-
#
|
|
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"
|
|
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
|
data/lib/foam/otel/version.rb
CHANGED
|
@@ -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 (
|
|
242
|
-
#
|
|
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
|
-
|
|
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
|