Skip to content

fix(core): lenient shim strips null-valued AnyValue fields (upstream #3603 case)#604

Merged
jensholdgaard merged 3 commits into
mainfrom
anyvalue-shim-null-field
Jul 23, 2026
Merged

fix(core): lenient shim strips null-valued AnyValue fields (upstream #3603 case)#604
jensholdgaard merged 3 commits into
mainfrom
anyvalue-shim-null-field

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Bridge the null-field AnyValue gap in the lenient OTLP/JSON shim

Our ourios_core::otlp::lenient_json shim already rewrites the two whole-value unset AnyValue encodings — {} and null — to an absent field, so the broken upstream with-serde deserialiser doesn't reject the whole batch. It did not handle a field set to null inside an AnyValue object: {"intValue":null} / {"doubleValue":null}.

Why it matters. proto3-JSON maps a null on any field to that field's default — for a oneof, unset. Real exporters emit this: the Vercel AI SDK writes doubleValue: null for non-finite token counts (upstream opentelemetry-rust#3603), and one such value anywhere in a payload failed the entire parse — exactly the GenAI/agent traffic RFC 0037 targets, and the same gap #3603 fixes at the deserializer level.

The change. Widen the per-slot unset check from inner.is_empty() to inner.values().all(is_null):

  • subsumes {} (vacuously true — no behaviour change for the existing case),
  • catches all-null-field objects ({"intValue":null}, {"doubleValue":null}),
  • preserves a meaningful {"stringValue":""} — an empty string is a set value the spec says MUST be stored, not unset (values().all(is_null) is false for it).
  • Array-element unset AnyValues stay a parse error — the pre-existing positional limitation only the deserializer-level upstream fix (#3603) can reach; unchanged and still documented.

Invariant (OTLP fidelity — RFC 0003.6 / RFC 0018)

This is the same bounded, documented fidelity concession the shim already makes: an unset AnyValue decodes to absent (None) rather than present-but-unset (Some(AnyValue{value:None})). For body the two collapse identically; for a KeyValue.value the presence bit is the documented concession. RFC 0018's Some(empty)-vs-None round-trip tests are unchanged — I'm widening the shim's coverage of unset encodings, not changing what it does with them. No RFC needed (a bug-fix extension of the shim's existing charter).

Verification

  • Core (otlp.rs): lenient_json_strips_a_null_field_but_keeps_a_meaningful_empty_string — one doc exercises both the null-field strip (2 positions) and empty-string preservation through the lenient retry.
  • Production path (ourios-ingester): rfc0003_6_null_valued_anyvalue_field_decodes_via_lenient_shimdecode_json accepts the null-field payload (doesn't reject the batch). It doubles as the null-field arm of the shim-retirement flip signal: the shim is not retired until both upstream #3595 ({}) and #3603 (null field) ship.
  • fmt + clippy clean; existing shim tests (empty/null encodings, unknown-key rejection, valid-input passthrough) all still pass.

Refs #549, #546.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved JSON ingestion to accept additional representations of unset values.
    • Records containing null-valued fields now decode successfully and are treated as absent values.
    • Meaningful empty strings remain preserved as valid values.
  • Tests

    • Added coverage for null-valued fields in log bodies and nested attributes.

…#3603 case)

The OTLP/JSON lenient shim already rewrote the two whole-value unset encodings
(`{}` and `null`) to an absent field so the broken upstream `with-serde`
deserialiser doesn't reject the batch. It did NOT handle a *field* set to null
inside an AnyValue object — `{"intValue":null}` / `{"doubleValue":null}` —
which proto3-JSON maps to that field's default (for a oneof, unset). Real
exporters emit this: the Vercel AI SDK writes `doubleValue: null` for
non-finite token counts (upstream opentelemetry-rust#3603), and one such value
anywhere in a batch failed the whole payload parse — exactly the GenAI traffic
RFC 0037 targets.

Widen the per-slot unset check from `inner.is_empty()` to
`inner.values().all(is_null)`: it subsumes `{}` (vacuously true), catches
all-null-field objects, and crucially LEAVES a meaningful `{"stringValue":""}`
intact (an empty string is a set value the spec says MUST be stored — not
unset). Array-element unset AnyValues stay a parse error (the pre-existing
positional limitation only the deserializer-level upstream fix can reach).

Same bounded, documented fidelity concession as before (unset AnyValue → absent
`None`, not present-but-unset `Some(None)`); RFC 0018's Some(empty)-vs-None
round-trip tests are unchanged. Covered at the core shim level and the
production `decode_json` path; the ingester test also carries the null-field
arm of the shim-retirement flip signal, so the shim is not retired until BOTH
upstream #3595 (`{}`) and #3603 (null field) ship.

Refs #549, #546

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQY9wfrfRggqSpMLH8Xj3Y
Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
@jensholdgaard
jensholdgaard requested a review from Copilot July 23, 2026 08:53
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jensholdgaard, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6a349a7-2bae-43b0-ac4e-a927db26d58b

📥 Commits

Reviewing files that changed from the base of the PR and between c336dfc and bc9770f.

📒 Files selected for processing (2)
  • crates/ourios-core/src/otlp.rs
  • crates/ourios-ingester/tests/it/rfc0003_6_json_protobuf_equivalence.rs
📝 Walkthrough

Walkthrough

The OTLP JSON leniency shim now recognizes AnyValue objects containing only null fields as unset. Core tests cover body and nested attribute values, while an RFC0003.6 integration test verifies lenient decoding and absent results.

Changes

OTLP AnyValue leniency

Layer / File(s) Summary
AnyValue normalization and coverage
crates/ourios-core/src/otlp.rs
The normalizer treats objects whose fields are all null as unset, documents the expanded encodings, and tests preservation of meaningful empty strings.
Ingestion decoding validation
crates/ourios-ingester/tests/it/rfc0003_6_json_protobuf_equivalence.rs
The integration test confirms null-valued body and attribute AnyValues use the lenient path and decode as absent values.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the core change: lenient stripping of null-valued AnyValue fields in the OTLP shim.
Description check ✅ Passed The description covers the summary, related references, verification, and RFC context; only the checklist is not fully formatted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch anyvalue-shim-null-field

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/ourios-core/src/otlp.rs`:
- Around line 1067-1069: Update the AnyValue-object detection around the
serde_json::Value::Object handling so it only treats an object as removable when
every key is a recognized AnyValue variant and every associated value is null;
unknown keys such as bogusValue must prevent stripping. Add a regression case
covering an object containing bogusValue: null.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98542361-f3a5-476a-8e70-b94ebcc3a38b

📥 Commits

Reviewing files that changed from the base of the PR and between 743b6cc and c336dfc.

📒 Files selected for processing (2)
  • crates/ourios-core/src/otlp.rs
  • crates/ourios-ingester/tests/it/rfc0003_6_json_protobuf_equivalence.rs

Comment thread crates/ourios-core/src/otlp.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends ourios_core::otlp::lenient_json to tolerate proto3-JSON AnyValue objects where the oneof field is explicitly set to null (e.g. {"intValue":null}, {"doubleValue":null}), preventing valid OTLP/JSON payloads from being rejected by the current upstream opentelemetry-proto with-serde deserializer.

Changes:

  • Broaden unset-AnyValue detection in the lenient JSON shim to treat all-null-field AnyValue objects as “unset” and strip the corresponding body/value field before retrying parse.
  • Add a core unit test ensuring null-field stripping works while preserving meaningful empty-string values.
  • Add an ingester integration test that decode_json accepts a payload containing null-valued AnyValue fields, serving as part of the shim-retirement flip signal.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
crates/ourios-core/src/otlp.rs Widen lenient OTLP/JSON normalization to strip null-valued AnyValue fields; add coverage for null-field behavior and empty-string preservation.
crates/ourios-ingester/tests/it/rfc0003_6_json_protobuf_equivalence.rs Add production-path test ensuring decode_json accepts null-field AnyValue payloads via the lenient retry.
Comments suppressed due to low confidence (1)

crates/ourios-core/src/otlp.rs:1330

  • Consider adding a regression test (near the end of this module) that an AnyValue object with an unknown key set to null (e.g. { "bogusValue": null }) is still rejected. The existing unknown-key test covers only a non-null value, and the null case is the one most likely to regress when widening “unset” detection.
        );
    }
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ourios-core/src/otlp.rs
Review fix (CodeRabbit + Copilot): the first cut used
`inner.values().all(is_null)`, which also stripped `{"bogusValue":null}` — an
unknown key with a null value — turning a malformed AnyValue into valid input
and weakening the existing unknown-key-rejection guarantee. Tighten the check to
require every entry be a *recognized* oneof field name (stringValue/boolValue/
intValue/doubleValue/bytesValue/arrayValue/kvlistValue) AND null-valued. `{}`
still strips (vacuously true), `{"intValue":null}` strips, `{"stringValue":""}`
is preserved, and `{"bogusValue":null}` now stays a parse error. Extended
lenient_json_does_not_loosen_unknown_key_rejection to cover the null-valued
unknown key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQY9wfrfRggqSpMLH8Xj3Y
Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread crates/ourios-core/src/otlp.rs Outdated
Comment thread crates/ourios-ingester/tests/it/rfc0003_6_json_protobuf_equivalence.rs Outdated
…t doc

Review fixes (Copilot):

- The core shim unit test asserted lenient == true, which would break once the
  upstream with-serde fix lands (direct parse succeeds → false). Parse via
  from_slice instead of from_slice_flagged so it's a pure behaviour test —
  asserts the decoded result, not which path produced it — and stays green
  across the upstream fix. The lenient-flag flip signal stays where it belongs,
  in the ingester equivalence test.
- Align the new ingester test's doc comment to the file's established
  "Scenario RFC0003.6 — <title>. See docs/rfcs/0003-otlp-receiver.md §5."
  format so scenarios stay uniformly greppable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQY9wfrfRggqSpMLH8Xj3Y
Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@jensholdgaard
jensholdgaard merged commit e8d26e8 into main Jul 23, 2026
27 checks passed
@jensholdgaard
jensholdgaard deleted the anyvalue-shim-null-field branch July 23, 2026 09:48
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.

2 participants