Skip to content

fix(otel): share one AnyValue decoder across all OTLP paths - #187

Open
Leroyyyyyyyyy wants to merge 1 commit into
agentevals-dev:mainfrom
Leroyyyyyyyyy:fix/shared-anyvalue-decoder
Open

fix(otel): share one AnyValue decoder across all OTLP paths#187
Leroyyyyyyyyy wants to merge 1 commit into
agentevals-dev:mainfrom
Leroyyyyyyyyy:fix/shared-anyvalue-decoder

Conversation

@Leroyyyyyyyyy

Copy link
Copy Markdown

Fixes #173

Problem

Three places decoded the OTLP AnyValue union, and only one did it fully:

Call site arrayValue / kvlistValue bytesValue
extraction.flatten_otlp_attributes attribute dropped entirely dropped
loader.otlp.OtlpJsonLoader._extract_attributes json.dumps of the raw proto wrapper dropped
api.otlp_processing._parse_otlp_any_value decoded recursively ✅ returned as-is

Feeding one span attribute — gen_ai.response.finish_reasons carrying arrayValue: ["stop"] — through the receiver paths produced three different answers before this change:

Path Before After
HTTP protobuf → OtlpJsonLoader '{"values": [{"stringValue": "stop"}]}' ["stop"]
OTLP/JSON → OtlpJsonLoader '{"values": [{"stringValue": "stop"}]}' ["stop"]
extraction / streaming attribute missing entirely ["stop"]
log body ["stop"] ["stop"]

Note that even after a json.loads, the loader's value is still the proto wrapper — not the decoded list.

Approach

Moved the already-correct recursive decoder into a new agentevals/otlp_anyvalue.py and pointed all three call sites at it. The gRPC receiver needs no change: it hands MessageToDict output to the same process_traces.

The new module imports only the standard library. That is deliberate: extraction imports loader.base, which eagerly initialises the loader package (and therefore loader.otlp), so having loader.otlp import from extraction would create a real import cycle. A leaf module has no edge back into the package and cannot participate in one.

Two behaviours are intentionally preserved:

  • bytesValue is returned unchanged. MessageToDict base64-encodes protobuf bytes fields and OTLP/JSON does the same, so call sites already receive a str. Decoding to real bytes would be a behaviour change beyond this fix.
  • Attributes carrying none of the seven union fields are still skipped rather than becoming {}is_any_value() keeps the prior semantics of both flatteners.

The loader's dict-shaped attribute branch (_flatten_nested_dict, for ClickHouse-style nested JSON) is untouched; only the OTLP array branch now shares the decoder.

Testing

array / kvlist / bytes attributes had no test coverage on either path — which is how the mismatch survived. Added 7 tests across tests/test_extraction.py and tests/test_otlp_loader.py, including the nested arrayValue-of-kvlistValue shape used for tool calls.
758 passed, 6 skipped (unit suite)
ruff check . / ruff format --check . clean

Three AnyValue decoders had drifted apart. extraction's
flatten_otlp_attributes silently dropped array/kvlist/bytes attributes,
and the OTLP JSON loader json.dumps()'d the raw proto wrapper instead of
decoding it. gen_ai.response.finish_reasons therefore surfaced as
'{"values": [{"stringValue": "stop"}]}' on one path and vanished on
another, where both should yield ["stop"].

Move the already-correct recursive decoder out of api/otlp_processing.py
into a dependency-free module and route all three call sites through it.
The new module imports only the standard library, so extraction,
loader.otlp and api.otlp_processing can all share it without creating an
import cycle.

bytesValue is returned unchanged: MessageToDict base64-encodes protobuf
bytes fields, so callers already receive a str today. Decoding it here
would change existing behaviour, which is out of scope for this fix.

Adds coverage for array/kvlist/bytes attributes, which previously had
none on either path.

Fixes agentevals-dev#173
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[OTel] Three different AnyValue decoders, two of them lossy

1 participant