Skip to content

feat(ingester): add OTLP/JSON decode + RFC0003.6 (encoding-rule conformance) - #131

Merged
jensholdgaard merged 2 commits into
mainfrom
feat/otlp-receiver-json-decode
Jun 6, 2026
Merged

feat(ingester): add OTLP/JSON decode + RFC0003.6 (encoding-rule conformance)#131
jensholdgaard merged 2 commits into
mainfrom
feat/otlp-receiver-json-decode

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 6, 2026

Copy link
Copy Markdown
Owner

What

Second green slice of the OTLP receiver (RFC 0003 §6.2): the OTLP/JSON decode path, flipping RFC0003.6 live. Builds on the wire-decode layer (#129).

Changes

  • receiver::decode::decode_json — decodes an ExportLogsServiceRequest from OTLP/JSON via the proto types' with-serde Deserialize (serde_json). DecodeError gains the Json variant (the #[non_exhaustive] reservation from feat(ingester): OTLP wire-decode layer + RFC0003.5 (protobuf equivalence) #129); unknown fields are ignored (serde default).
  • Shared proptest strategy extracted to tests/otlp_strategy/ and reused by RFC0003.5 + RFC0003.6.

RFC0003.6 — verified, not assumed

Per the maintainer's OTel-spec guidance, the tests assert the actual OTLP/JSON shape with-serde emits/accepts (the asserts are the verification — no speculation about crate internals). All confirmed compliant:

  • hex traceId/spanId (not base64), integer enums (not names), decimal-string 64-bit ints accepted as number or string, lowerCamelCase keys, unknown-field tolerance;
  • a JSON↔protobuf equivalence proptest over strings/bools/ints/bytes/arrays/kvlists.

Finding: with-serde lossy doubles (#130)

The equivalence proptest surfaced that with-serde's double JSON formatter is not shortest-round-trippable — ~12% of arbitrary f64 drift 1–2 ULP (serde_json itself is exact, so it's a with-serde custom formatter). Per the OTLP/JSON spec this constrains double syntax, not binary64 round-trip fidelity, so it's a precision limitation, not a conformance violation (confirmed via the maintainer's OpenTelemetry-spec review). Handling:

Verification

  • cargo test -p ourios-ingester ✓ — RFC0003.5 + RFC0003.6 live (equivalence stable at 4096 cases); 13 rfc0003_* scenarios remain ignored.
  • cargo fmt --all --check ✓ · cargo clippy --all-targets --all-features -- -D warnings ✓ (workspace)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added JSON decoding support for OTLP log service requests, enabling alternative wire format handling alongside protobuf.
  • Tests

    • Added comprehensive equivalence tests validating JSON and protobuf format interoperability.
    • Added test utilities for OTLP message generation and validation.

…rmance)

Second green slice of the OTLP receiver (RFC 0003 §6.2). Adds the
`application/json` decode path and flips RFC0003.6 live.

`receiver::decode::decode_json` decodes an `ExportLogsServiceRequest` from
OTLP/JSON via the proto types' `with-serde` `Deserialize` (serde_json);
`DecodeError` gains the `Json` variant (the #[non_exhaustive] reservation
from the prior slice). Unknown fields are ignored (serde default).

RFC0003.6 verifies the OTLP-JSON deviations from plain proto3-JSON by
asserting the actual encoded/decoded shape (these asserts ARE the
verification of with-serde's behaviour, not assumptions): hex
traceId/spanId (not base64), integer enums (not names), decimal-string
64-bit ints accepted as number-or-string, lowerCamelCase keys, and
unknown-field tolerance — all confirmed compliant. A JSON<->protobuf
equivalence proptest (reusing the RFC0003.5 strategy, now extracted to a
shared tests/otlp_strategy module) covers strings/bools/ints/bytes/
arrays/kvlists.

Finding (#130): with-serde's `double` JSON formatter is not
shortest-round-trippable — ~12% of arbitrary f64 drift 1-2 ULP. Per the
OTLP/JSON spec this is a precision limitation, not a syntax violation
(the spec constrains double syntax, not binary64 round-trip fidelity), so
doubles are excluded from the strict equivalence proptest
(export_request_json_safe) and covered by a targeted test that pins
"nice" doubles round-trip exactly + documents the limitation. The drift
itself is not asserted (that would break when upstream improves).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 52 minutes and 15 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 009dce37-aabc-40b5-876d-19c32f539ab5

📥 Commits

Reviewing files that changed from the base of the PR and between 9e28530 and 442f5e9.

📒 Files selected for processing (2)
  • crates/ourios-ingester/src/receiver.rs
  • crates/ourios-ingester/src/receiver/decode.rs
📝 Walkthrough

Walkthrough

This PR implements OTLP/HTTP JSON decoding support for the ingester, extending the existing protobuf-only wire decoding with a new decode_json() function and comprehensive RFC0003.6 equivalence tests to verify semantic parity and OTLP/JSON spec compliance.

Changes

OTLP/HTTP JSON Decoding Implementation and RFC0003.6 Testing

Layer / File(s) Summary
JSON Decoding Dependency and Core API
crates/ourios-ingester/Cargo.toml, crates/ourios-ingester/src/receiver/decode.rs, crates/ourios-ingester/src/receiver.rs
serde_json dependency is added with the std feature. DecodeError gains a Json(serde_json::Error) variant with corresponding Display and Error::source implementations. A new decode_json(bytes: &[u8]) -> Result<ExportLogsServiceRequest, DecodeError> function decodes OTLP/JSON payloads via serde_json::from_slice, mapping errors to DecodeError::Json. The function is re-exported from receiver module alongside existing decode_protobuf.
Test Infrastructure - Shared Generator Strategy
crates/ourios-ingester/tests/otlp_strategy/mod.rs
New otlp_strategy module provides reusable Proptest strategies for generating ExportLogsServiceRequest test values. Parameterized recursive AnyValue generators support both export_request() (includes finite doubles for protobuf tests) and export_request_json_safe() (excludes doubles to avoid JSON encoding limitations with NaN/infinities).
Protobuf Equivalence Test Refactor
crates/ourios-ingester/tests/rfc0003_5_grpc_http_protobuf_equivalence.rs
Test refactored to use the shared export_request strategy from otlp_strategy module, replacing previous in-file request generation. Core equivalence and roundtrip assertions remain unchanged.
JSON↔Protobuf Equivalence Test Suite
crates/ourios-ingester/tests/rfc0003_6_json_protobuf_equivalence.rs
Placeholder "red gate" test replaced with comprehensive RFC0003.6 validation: (1) proptest asserting decode_json and decode_protobuf produce identical in-memory values for generated JSON-safe requests, (2) deterministic roundtrip test verifying exact bit preservation for "nice" f64 values, (3) spec-asserting encoder test checking OTLP/JSON deviations (hex trace/span IDs, integer-valued enums, decimal-string 64-bit ints, lowerCamelCase keys), and (4) three decoder compliance tests covering fully-compliant payloads, 64-bit ints as JSON numbers, and unknown field tolerance.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • jensholdgaard/ourios#101: Introduced the ingester crate and receiver scaffold that this PR extends with JSON decoding implementation.
  • jensholdgaard/ourios#128: Added the RFC0003.6 test placeholder that this PR replaces with full equivalence and compliance assertions.

Poem

🐰 JSON flows where protobuf once did reign,
Two decoders harmonize in test's refrain,
With doubles dancing safely through the wire,
Spec-compliant translations we acquire—
OTLP's duality, now both precise and plain! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding OTLP/JSON decode support and RFC0003.6 encoding-rule conformance, which aligns with all file changes.
Description check ✅ Passed The description is comprehensive and covers all required sections from the template, including summary, related issues/RFCs, and a detailed implementation breakdown. Testing and code quality checklist items are confirmed in the text.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/otlp-receiver-json-decode

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 and usage tips.

@jensholdgaard
jensholdgaard requested a review from Copilot June 6, 2026 14:09
@jensholdgaard

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Implements the OTLP/JSON decode path for the ingester’s OTLP receiver slice (RFC 0003 §6.2), and flips RFC0003.6 “OTLP/JSON encoding-rule conformance” tests live alongside the existing protobuf equivalence checks.

Changes:

  • Add receiver::decode_json (serde-based OTLP/JSON ExportLogsServiceRequest decoding) and extend DecodeError with a Json variant.
  • Replace the RFC0003.6 red-gate stub with live equivalence + spec-asserting JSON shape tests (incl. unknown-field tolerance and 64-bit int number-or-string acceptance).
  • Extract the shared proptest strategy into tests/otlp_strategy/ and reuse it for RFC0003.5 and RFC0003.6.

Reviewed changes

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

Show a summary per file
File Description
crates/ourios-ingester/tests/rfc0003_6_json_protobuf_equivalence.rs Adds live RFC0003.6 proptest + spec-asserting OTLP/JSON shape tests.
crates/ourios-ingester/tests/rfc0003_5_grpc_http_protobuf_equivalence.rs Switches to the shared OTLP proptest strategy module.
crates/ourios-ingester/tests/otlp_strategy/mod.rs New shared proptest strategies for OTLP request generation (JSON-safe and full).
crates/ourios-ingester/src/receiver/decode.rs Adds decode_json and DecodeError::Json.
crates/ourios-ingester/src/receiver.rs Re-exports decode_json from the receiver module.
crates/ourios-ingester/Cargo.toml Adds serde_json dependency for OTLP/JSON decoding.
Cargo.lock Locks the new serde_json dependency.

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

Comment on lines 20 to 24
@@ -24,12 +24,16 @@ use prost::Message;
pub enum DecodeError {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 442f5e9 — the #[non_exhaustive] note no longer claims a future Json variant (it's in this PR); it now explains the general forward-compat reservation for future transports/encodings.

Comment on lines 15 to +17
pub mod decode;

pub use decode::{DecodeError, decode_protobuf};
pub use decode::{DecodeError, decode_json, decode_protobuf};

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 442f5e9 — the module doc now says the decode layer covers "protobuf + OTLP/JSON" rather than "protobuf today; OTLP/JSON next".

@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.

🧹 Nitpick comments (2)
crates/ourios-ingester/tests/rfc0003_6_json_protobuf_equivalence.rs (2)

77-127: ⚡ Quick win

Consider testing non-finite doubles (NaN, Infinity) to verify proto3-JSON string serialization.

The current "nice doubles" test covers finite values that serialize as JSON numbers. However, non-finite f64 values (NaN, Infinity, -Infinity) follow a different code path: they serialize as JSON strings per proto3-JSON spec. Testing these would verify the with-serde string serialization/deserialization behavior documented in the codebase learnings.

📝 Example test addition
// Add after line 97
for non_finite in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
    let back = roundtrip_double_via_otlp_json(non_finite);
    // NaN is special: NaN != NaN, so check bit pattern
    if non_finite.is_nan() {
        assert!(back.is_nan(), "NaN must round-trip as NaN via OTLP/JSON");
    } else {
        assert_eq!(
            back, non_finite,
            "non-finite double {non_finite} must round-trip via OTLP/JSON string encoding",
        );
    }
}
🤖 Prompt for 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.

In `@crates/ourios-ingester/tests/rfc0003_6_json_protobuf_equivalence.rs` around
lines 77 - 127, Add a new loop in the
rfc0003_6_nice_doubles_roundtrip_through_otlp_json test that rounds-trip
non-finite f64 values (f64::NAN, f64::INFINITY, f64::NEG_INFINITY) via
roundtrip_double_via_otlp_json and asserts correct behavior: for NaN use
is_nan()/bit-check to verify NaN round-trips as NaN, and for +/-Infinity assert
equality with the original value; reuse the existing
roundtrip_double_via_otlp_json helper and keep failure messages consistent with
the existing assertions.

Source: Learnings


129-179: 💤 Low value

Consider testing decoder accepts uppercase hex traceId/spanId.

The encoder test uses to_lowercase() (lines 141, 149) when verifying hex format, suggesting case-insensitivity is expected. However, the decoder test on line 188-189 only uses lowercase hex. Testing uppercase hex would provide more thorough coverage of the case-insensitivity requirement.

📝 Example test addition
// Add a new test after rfc0003_6_decoder_accepts_compliant_otlp_json
#[test]
fn rfc0003_6_decoder_accepts_uppercase_hex_trace_span_ids() {
    let uppercase_hex = br#"{"resourceLogs":[{"scopeLogs":[{"logRecords":[{
        "timeUnixNano":"1700000000000000000",
        "traceId":"0102030405060708090A0B0C0D0E0F10",
        "spanId":"0102030405060708"
    }]}]}]}"#;
    let req = decode_json(uppercase_hex).expect("uppercase hex trace/span IDs accepted");
    let rec = &req.resource_logs[0].scope_logs[0].log_records[0];
    assert_eq!(rec.trace_id, (1u8..=16).collect::<Vec<u8>>());
    assert_eq!(rec.span_id, (1u8..=8).collect::<Vec<u8>>());
}
🤖 Prompt for 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.

In `@crates/ourios-ingester/tests/rfc0003_6_json_protobuf_equivalence.rs` around
lines 129 - 179, Add a decoder test that verifies uppercase hex traceId/spanId
are accepted: create a new test function (e.g.,
rfc0003_6_decoder_accepts_uppercase_hex_trace_span_ids) that constructs an
OTLP/JSON request with traceId and spanId using uppercase hex (matching values
used in rfc0003_6_json_encoder_emits_otlp_json_deviations), call
decode_json(...) and assert the resulting request's
resource_logs[0].scope_logs[0].log_records[0].trace_id and .span_id equal the
expected byte vectors (1..=16 and 1..=8 respectively); place this test after
rfc0003_6_decoder_accepts_compliant_otlp_json and reuse the same decode_json
helper so the decoder handling of case-insensitive hex is covered.
🤖 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.

Nitpick comments:
In `@crates/ourios-ingester/tests/rfc0003_6_json_protobuf_equivalence.rs`:
- Around line 77-127: Add a new loop in the
rfc0003_6_nice_doubles_roundtrip_through_otlp_json test that rounds-trip
non-finite f64 values (f64::NAN, f64::INFINITY, f64::NEG_INFINITY) via
roundtrip_double_via_otlp_json and asserts correct behavior: for NaN use
is_nan()/bit-check to verify NaN round-trips as NaN, and for +/-Infinity assert
equality with the original value; reuse the existing
roundtrip_double_via_otlp_json helper and keep failure messages consistent with
the existing assertions.
- Around line 129-179: Add a decoder test that verifies uppercase hex
traceId/spanId are accepted: create a new test function (e.g.,
rfc0003_6_decoder_accepts_uppercase_hex_trace_span_ids) that constructs an
OTLP/JSON request with traceId and spanId using uppercase hex (matching values
used in rfc0003_6_json_encoder_emits_otlp_json_deviations), call
decode_json(...) and assert the resulting request's
resource_logs[0].scope_logs[0].log_records[0].trace_id and .span_id equal the
expected byte vectors (1..=16 and 1..=8 respectively); place this test after
rfc0003_6_decoder_accepts_compliant_otlp_json and reuse the same decode_json
helper so the decoder handling of case-insensitive hex is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b9dc52a-f5ae-4045-9245-de42366f2350

📥 Commits

Reviewing files that changed from the base of the PR and between 7667e36 and 9e28530.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • crates/ourios-ingester/Cargo.toml
  • crates/ourios-ingester/src/receiver.rs
  • crates/ourios-ingester/src/receiver/decode.rs
  • crates/ourios-ingester/tests/otlp_strategy/mod.rs
  • crates/ourios-ingester/tests/rfc0003_5_grpc_http_protobuf_equivalence.rs
  • crates/ourios-ingester/tests/rfc0003_6_json_protobuf_equivalence.rs

Both doc comments were written in the RFC0003.5 slice when OTLP/JSON was
still upcoming. This PR implements it, so: the `DecodeError`
`#[non_exhaustive]` note no longer promises a future `Json` variant (it's
here) and instead explains the general forward-compat reservation, and the
receiver module doc says the decode layer covers "protobuf + OTLP/JSON",
not "protobuf today; OTLP/JSON next". Surfaced in review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

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 6 out of 7 changed files in this pull request and generated no new comments.

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