Skip to content

feat(ingester): add the OTLP/HTTP listener (RFC0003.13/.14 + .11 HTTP arms) - #135

Merged
jensholdgaard merged 3 commits into
mainfrom
feat/otlp-receiver-http
Jun 6, 2026
Merged

feat(ingester): add the OTLP/HTTP listener (RFC0003.13/.14 + .11 HTTP arms)#135
jensholdgaard merged 3 commits into
mainfrom
feat/otlp-receiver-http

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 6, 2026

Copy link
Copy Markdown
Owner

What

Sixth green slice of the OTLP receiver (RFC 0003 §6.2): the OTLP/HTTP listener (axum), flipping RFC0003.13/.14 live and covering RFC0003.11's HTTP error arms.

Design

receiver::http::router mounts a POST handler at a configurable path (default /v1/logs) wrapping IngestPipeline behind a shared Mutex (the single-writer WAL serializes concurrent requests; the lock never spans an .await, so a plain std::sync::Mutex suffices). The handler:

  • Content-Typeapplication/x-protobuf (decode_protobuf) / application/json (decode_json) / else 415;
  • Content-Encoding → identity / gzip (flate2); unsupported → 415, corrupt gzip → 400;
  • malformed body → 400, oversize → 413 (DefaultBodyLimit), tenant-resolution failure → 400, unconfigured path → 404;
  • success → 200 ExportLogsServiceResponse (partial_success unset) encoded per the request format.

Scenarios

  • .13 (live) — identity and gzip of the same payload decode to the same request (gzip is decompressed, not just accepted) + recover the original; unsupported encoding → 415.
  • .14 (live) — default /v1/logs handled; other path 404; operator override path handled and the default then 404s.
  • .11 HTTP arms (in tests/http_transport_errors, not flipping rfc0003_11) — malformed 400, bad/missing Content-Type 415, corrupt gzip 400, wrong path 404, oversize 413, each asserting no OtlpBatch frame is appended.

rfc0003_11 stays #[ignore]'d until its gRPC cancellation arm exists (next slice), keeping the §5 acceptance gate honest. All HTTP tests run in-process via tower::ServiceExt::oneshot (no socket).

Notes

  • Journal gains a Send bound (pipeline as shared state). Deps: axum 0.8 (http1+tokio) + flate2 (rust_backend); tower dev-dep.
  • The handler calls the blocking ingest under the lock; a production multi-threaded server should offload via spawn_blocking — wired when the server binary lands (§9 process-model).

Verification

  • cargo test -p ourios-ingester ✓ — RFC0003.1/.3–.10/.12/.13/.14 live; 3 ignored (.2/.11/.15). Workspace green.
  • cargo fmt --all --check ✓ · cargo clippy --all-targets --all-features -- -D warnings

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added OTLP/HTTP logs receiver with configurable path and request size limits
    • Support for protobuf and JSON payload formats
    • Automatic gzip decompression for compressed requests
    • Proper HTTP status codes for error scenarios
  • Tests

    • Added integration tests for HTTP transport error handling, content encoding, and path configuration

… arms)

Sixth green slice of the OTLP receiver (RFC 0003 §6.2). Adds
`receiver::http`: an axum POST handler (default `/v1/logs`, configurable)
wrapping the IngestPipeline behind a shared Mutex (single-writer WAL ⇒
requests serialize). It dispatches on Content-Type (application/x-protobuf
→ decode_protobuf; application/json → decode_json; else 415), handles
Content-Encoding identity + gzip via flate2 (unsupported → 415; corrupt
gzip → 400), 400s a malformed body, 413s an oversize body
(DefaultBodyLimit), 400s a tenant-resolution failure, 404s an unconfigured
path, and on success returns a 200 ExportLogsServiceResponse
(partial_success unset) encoded per the request format.

Flips RFC0003.13 (identity + gzip decode to the same request; unsupported
encoding → 415) and RFC0003.14 (default path handled; other path 404;
operator override). HTTP transport-error arms of RFC0003.11 (malformed
400, bad/missing Content-Type 415, corrupt gzip 400, wrong path 404,
oversize 413, no WAL frame appended) land in tests/http_transport_errors;
rfc0003_11 itself stays `#[ignore]`'d until its gRPC cancellation arm
exists. All HTTP tests run in-process via tower::ServiceExt::oneshot (no
socket).

`Journal` gains a `Send` bound so the pipeline can be shared state. Adds
axum (0.8, http1+tokio) + flate2 (rust_backend) deps; tower as a dev-dep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard requested a review from Copilot June 6, 2026 17:17
@jensholdgaard

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@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 49 minutes and 54 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: 3c58d085-945c-47c8-a4b0-4c0d562d3f9f

📥 Commits

Reviewing files that changed from the base of the PR and between d7500fd and a49aaa1.

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

Walkthrough

This PR implements a complete OTLP/HTTP receiver for the ourios ingester using Axum, featuring configurable routing at /v1/logs (or custom path), transparent gzip decompression, content-type and content-encoding negotiation, structured error mapping to HTTP status codes, and comprehensive integration tests validating transport errors, compression, and path configuration per RFC0003.11/0003.13/0003.14.

Changes

OTLP/HTTP Receiver with RFC Compliance

Layer / File(s) Summary
Dependencies and contract updates
crates/ourios-ingester/Cargo.toml, crates/ourios-ingester/src/lib.rs, crates/ourios-ingester/src/receiver.rs, crates/ourios-ingester/src/receiver/pipeline.rs
Cargo adds axum and flate2 runtime deps and tower dev-deps; module docs updated; Journal trait constrained to Send for shared async state.
HTTP receiver implementation
crates/ourios-ingester/src/receiver/http.rs
Axum router with HttpConfig (configurable path and 4 MiB default body limit), request handler parses Content-Type and Content-Encoding, optionally gunzips, decodes ExportLogsServiceRequest, dispatches to mutex-guarded IngestPipeline, and maps errors to specific HTTP status codes (400/413/415/500) with poisoned-lock recovery and wire-format-matched response encoding.
HTTP test support utilities
crates/ourios-ingester/tests/ingest_support/mod.rs
CapturingJournal collects ingested payloads, capturing_pipeline() helper, post_request() builder, send() router driver, and gzip() compression helper for in-process Axum request testing.
HTTP transport error tests
crates/ourios-ingester/tests/http_transport_errors.rs
assert_rejected helper and test cases covering malformed protobuf, unsupported/missing content-type, corrupt gzip, wrong path, and oversized bodies with expected HTTP status validation and empty-capture assertions.
Compression and path configuration tests
crates/ourios-ingester/tests/rfc0003_13_compression.rs, crates/ourios-ingester/tests/rfc0003_14_path_config.rs
RFC0003.13: identity and gzip payloads decode equally, unsupported encoding → 415; RFC0003.14: default /v1/logs route and configurable path overrides with 404 fallback.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jensholdgaard/ourios#134: This PR constrains Journal: Send to enable the shared async pipeline state that the HTTP receiver depends on.
  • jensholdgaard/ourios#131: The HTTP receiver's content-type negotiation dispatches to decode_json and related JSON support added in that PR.
  • jensholdgaard/ourios#128: This PR converts RFC0003.11/0003.13/0003.14 placeholder tests into real Axum-based assertions that exercise the new HTTP receiver implementation.

Poem

🐰 A server springs to life with Axum's might,
Parsing gzip, protobuf, JSON—all just right!
/v1/logs receives with status codes clear,
RFC0003 compliance draws near!
Hop-hop, the HTTP listener's here! 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding an OTLP/HTTP listener with specific RFC coverage. It is concise, clear, and directly reflects the primary contribution.
Description check ✅ Passed The description is comprehensive and well-structured with clear sections (What, Design, Scenarios, Notes, Verification). It includes RFC references, technical details, testing coverage, and verification results. All required template sections are addressed.
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-http

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.

@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

Adds an OTLP/HTTP receiver to ourios-ingester (RFC0003 §6.2), including routing at a configurable path, request decoding by Content-Type/Content-Encoding, controlled HTTP error mapping (RFC0003.11 HTTP arms), and in-process axum router tests that flip RFC0003.13/.14 green.

Changes:

  • Introduce receiver::http axum router/handler for OTLP/HTTP (/v1/logs by default) with gzip support and body-size limiting.
  • Add new acceptance + transport-error tests for RFC0003.13/.14 and RFC0003.11 HTTP error arms, plus shared HTTP test utilities.
  • Extend Journal with a Send bound and wire new dependencies (axum, flate2, tower dev-dep).

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/ourios-ingester/src/receiver/http.rs New OTLP/HTTP router + handler implementing RFC-controlled HTTP behavior.
crates/ourios-ingester/src/receiver/pipeline.rs Adds Send bound to Journal for sharing pipeline state in listeners.
crates/ourios-ingester/src/receiver.rs Exposes the new receiver::http module and updates module docs.
crates/ourios-ingester/src/lib.rs Updates crate-level docs to include OTLP/HTTP listener slice.
crates/ourios-ingester/tests/ingest_support/mod.rs Adds HTTP test helpers (router driving, gzip helper, capturing journal).
crates/ourios-ingester/tests/rfc0003_13_compression.rs Flips RFC0003.13 live test (identity vs gzip equivalence + 415 on unsupported).
crates/ourios-ingester/tests/rfc0003_14_path_config.rs Flips RFC0003.14 live test (default path + operator override semantics).
crates/ourios-ingester/tests/http_transport_errors.rs New tests for RFC0003.11 HTTP transport error arms (400/413/415/404, no WAL append).
crates/ourios-ingester/Cargo.toml Adds axum + flate2 deps and tower dev-dependency for in-process router testing.
Cargo.lock Locks new transitive dependencies for the HTTP listener stack.

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

Comment on lines +102 to +107
// WAL-before-ack ingest. The lock spans only this synchronous call.
let outcome = pipeline
.lock()
.expect("ingest pipeline lock not poisoned")
.ingest(request);

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 — the handler now recovers the guard via PoisonError::into_inner instead of expect, so a poisoned lock cannot panic here; the "no panics" promise holds.

Comment on lines +163 to +168
WireFormat::Json => (
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
serde_json::to_vec(&response).unwrap_or_default(),
)
.into_response(),

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 — the JSON arm now returns 500 on a serialize failure rather than a 200 with an empty body (encoding the trivial default response should never fail, but a 500 is the honest fallback).

Comment on lines +28 to +29
/// `Send` so the pipeline can live behind an `Arc<tokio::Mutex<_>>` as
/// shared state in the async HTTP/gRPC listeners.

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 the doc — the listener uses a std::sync::Mutex (the lock never spans an .await), so the bound doc now says Arc<Mutex<_>>, not tokio::Mutex.

Review fixes:
- The handler promised "no panics" but `lock().expect()` panics on a
  poisoned mutex; recover the guard via `PoisonError::into_inner` so a
  prior panic can't cascade into a panic here.
- `success_response`'s JSON arm used `to_vec().unwrap_or_default()`, which
  could return a 200 with an empty body on a (theoretical) serialize
  failure; return 500 instead — never a 200 without the response.
- The `Journal: Send` doc said `Arc<tokio::Mutex<_>>`; the listener uses a
  `std::sync::Mutex` (the lock never spans an await), so the doc now says
  `Arc<Mutex<_>>`.

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 9 out of 10 changed files in this pull request and generated 4 comments.

Comment on lines +102 to +110
// WAL-before-ack ingest. The lock spans only this synchronous call.
// Recover the guard even if a prior holder panicked: a poisoned lock
// must not turn into a panic here (the handler promises not to), so
// take the inner guard regardless and let this request proceed.
let outcome = pipeline
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.ingest(request);

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 a49aaa1 — the blocking ingest (WAL append + fsync) now runs via tokio::task::spawn_blocking, so it no longer stalls the async runtime; a blocking-task panic maps to 500.

Comment on lines +126 to +130
match media_type {
"application/x-protobuf" => Some(WireFormat::Protobuf),
"application/json" => Some(WireFormat::Json),
_ => None,
}

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 a49aaa1content_type now lowercases the media type before matching (HTTP media types are case-insensitive). New test asserts Application/X-Protobuf is accepted.

Comment on lines +139 to +143
Some(value) => match value.to_str().ok()?.trim() {
"" | "identity" => Some(Encoding::Identity),
"gzip" => Some(Encoding::Gzip),
_ => None,
},

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 a49aaa1content_encoding now matches case-insensitively, so GZIP/Identity are accepted (covered by the same case-insensitivity test).

Comment on lines +148 to +152
use std::io::Read;
let mut decoder = flate2::read::GzDecoder::new(bytes);
let mut out = Vec::new();
decoder.read_to_end(&mut out)?;
Ok(out)

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.

Good catch (decompression bomb) — gunzip now caps decompression at the configured body size via Read::take, returning 413 if the body inflates past it (DefaultBodyLimit only bounds the compressed bytes). New test: a ~1 KB gzip that inflates to 1 MB under a 4 KiB cap → 413.

@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: 2

🤖 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-ingester/src/receiver/http.rs`:
- Around line 123-131: The content_type function currently does exact-case
matching which will reject valid headers; update content_type to perform
case-insensitive comparisons by normalizing the media_type (e.g.,
media_type.to_ascii_lowercase()) before matching against
"application/x-protobuf" and "application/json". Do the same for the
Content-Encoding handling (the code block around the content-encoding check,
likely a function or branch that inspects header::CONTENT_ENCODING) by comparing
tokens with eq_ignore_ascii_case or converting to_ascii_lowercase and matching
"gzip" etc., so both Content-Type and Content-Encoding are treated
case-insensitively.
- Around line 84-93: The gzip branch (content_encoding + Encoding::Gzip using
gunzip and reading into an unbounded Vec) can be exploited to decompress huge
data and exhaust memory; update the decompression to enforce an inflated-size
limit (based on config.max_body_bytes or a configured max_inflated_bytes) by
streaming the gzip decoder and stopping/read-checking bytes as you append,
returning StatusCode::PAYLOAD_TOO_LARGE (or BAD_REQUEST per policy) when the
limit is exceeded; implement the bound either inside gunzip or replace its use
in the Encoding::Gzip arm (referencing content_encoding, Encoding::Gzip, gunzip,
and the raw variable) so decompression never grows unbounded.
🪄 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: 2cdb80d5-d031-4568-bba8-a3bc28573715

📥 Commits

Reviewing files that changed from the base of the PR and between 19e05d0 and d7500fd.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • crates/ourios-ingester/Cargo.toml
  • crates/ourios-ingester/src/lib.rs
  • crates/ourios-ingester/src/receiver.rs
  • crates/ourios-ingester/src/receiver/http.rs
  • crates/ourios-ingester/src/receiver/pipeline.rs
  • crates/ourios-ingester/tests/http_transport_errors.rs
  • crates/ourios-ingester/tests/ingest_support/mod.rs
  • crates/ourios-ingester/tests/rfc0003_13_compression.rs
  • crates/ourios-ingester/tests/rfc0003_14_path_config.rs

Comment thread crates/ourios-ingester/src/receiver/http.rs
Comment thread crates/ourios-ingester/src/receiver/http.rs
…headers

Second review round on the HTTP listener:
- `ingest` does blocking I/O (WAL append + fsync); run it via
  `tokio::task::spawn_blocking` so it doesn't stall the async runtime
  (a blocking-task panic maps to 500).
- Bound gzip decompression to the configured body size (DefaultBodyLimit
  only bounds the *compressed* body): a body inflating past the cap → 413,
  defusing a decompression bomb. New test asserts a tiny gzip that
  inflates to 1 MB under a 4 KiB cap → 413.
- Content-Type and Content-Encoding are matched case-insensitively (both
  are case-insensitive per HTTP); new test for uppercased values.

The decompressed cap rides on a small `AppState { pipeline,
max_decompressed_bytes }` so the handler sees the limit the body layer
also enforces.

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 9 out of 10 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