Skip to content

feat(ingester): add the OTLP/gRPC LogsService listener (RFC0003.11/.15) - #136

Merged
jensholdgaard merged 5 commits into
mainfrom
feat/otlp-receiver-grpc
Jun 6, 2026
Merged

feat(ingester): add the OTLP/gRPC LogsService listener (RFC0003.11/.15)#136
jensholdgaard merged 5 commits into
mainfrom
feat/otlp-receiver-grpc

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 6, 2026

Copy link
Copy Markdown
Owner

What

Seventh green slice of the OTLP receiver (RFC 0003 §6.2): the OTLP/gRPC LogsService (tonic), flipping RFC0003.11 fully and RFC0003.15.

receiver::grpc::LogsReceiver implements logs_service_server::LogsService over the shared IngestPipeline. export hands the (tonic-decoded) request to the WAL-before-ack pipeline via spawn_blocking (mirrors the HTTP handler; poison-recovered, panic-free) and maps the result:

  • tenant-resolution failure → Status::invalid_argument naming the failing ResourceLogs index + attribute (RFC0003.4);
  • WAL failure → Status::internal (not acked, §3.4);
  • success → empty ExportLogsServiceResponse.

Scenarios

  • .11 (now fully green) — HTTP error arms in tests/http_transport_errors (prior slice) + gRPC arms here: tenant failure → INVALID_ARGUMENT (never a panic), valid request succeeds. Honest limitation: "gRPC client cancellation mid-decode" can't be reproduced by an in-process direct call (tonic decodes before the handler); the test documents this and asserts the testable invariants (panic-free; atomic ingest under the lock ⇒ no partial WAL state on a dropped future). The socket-level cancellation path comes with a served tonic server (a follow-up) — flagged as an OTLP/tonic nuance, not faked.
  • .15 — N=8 concurrent Export calls on one shared pipeline (multi-thread runtime) each ack, and the WAL ends with exactly N durable OtlpBatch frames (single-writer WAL serializes; each acks after its own append+fsync). Stable across repeated runs.

Notes

  • Adds gen-tonic on opentelemetry-proto + tonic (codegen). No socket server is stood up in this slice — the service is exercised in-process.

Verification

  • cargo test -p ourios-ingester ✓ — only RFC0003.2 (crash-before-ack) remains ignored. Workspace green.
  • cargo fmt --all --check ✓ · cargo clippy --all-targets --all-features -- -D warnings

🤖 Generated with Claude Code

Summary by CodeRabbit

New Features

  • Added gRPC transport endpoint for OTLP logs ingestion, providing an alternative to HTTP ingestion. Includes proper error handling for tenant resolution failures and WAL append operations with concurrent request support.

Tests

  • Added integration tests validating gRPC transport error handling and concurrent log export durability to persistent storage.

Seventh green slice of the OTLP receiver (RFC 0003 §6.2). `receiver::grpc`
implements opentelemetry-proto's logs_service_server::LogsService over the
shared IngestPipeline: export() hands the tonic-decoded
ExportLogsServiceRequest to the WAL-before-ack pipeline (via spawn_blocking,
mirroring the HTTP handler; poison-recovered, panic-free) and maps the
result — tenant-resolution failure → Status::invalid_argument naming the
failing ResourceLogs index + attribute (RFC0003.4), WAL failure →
Status::internal, success → an empty ExportLogsServiceResponse.

Flips RFC0003.11 fully: HTTP arms in tests/http_transport_errors (prior
slice) + gRPC arms here (tenant failure → INVALID_ARGUMENT, never a panic;
valid request succeeds). The "gRPC client cancellation mid-decode" arm
can't be reproduced by an in-process direct call (tonic decodes before the
handler); the test documents this honestly and asserts the testable
invariants (panic-free; atomic ingest under the lock leaves no partial WAL
state) — flagged as an OTLP/tonic nuance, not faked.

Flips RFC0003.15: N concurrent Export calls on one shared pipeline
(multi-thread runtime) each ack, and the WAL ends with exactly N durable
OtlpBatch frames (the single-writer WAL serializes; each acks after its
own append+fsync).

Adds the `gen-tonic` feature on opentelemetry-proto + tonic (codegen).

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

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6371b0f1-24bc-4033-b8fd-69a672c0d268

📥 Commits

Reviewing files that changed from the base of the PR and between 3c0dcda and 01c6c3f.

⛔ 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/grpc.rs
  • crates/ourios-ingester/src/receiver/http.rs
  • crates/ourios-ingester/src/receiver/pipeline.rs
  • crates/ourios-ingester/tests/ingest_support/mod.rs
  • crates/ourios-ingester/tests/rfc0003_11_transport_errors.rs
  • crates/ourios-ingester/tests/rfc0003_15_concurrent_wal_before_ack.rs

📝 Walkthrough

Walkthrough

This PR adds gRPC receiver support to ourios-ingester by implementing a LogsReceiver handler, consolidating the shared pipeline abstraction, and validating error-handling and concurrent export behavior through concrete tests.

Changes

gRPC receiver implementation

Layer / File(s) Summary
gRPC dependencies and codegen
crates/ourios-ingester/Cargo.toml, crates/ourios-ingester/src/lib.rs
Add tonic dependency with codegen feature; update opentelemetry-proto features from gen-tonic-messages to gen-tonic to generate server trait LogsService.
Shared pipeline abstraction consolidation
crates/ourios-ingester/src/receiver/pipeline.rs, crates/ourios-ingester/src/receiver/http.rs, crates/ourios-ingester/src/receiver.rs
Define SharedPipeline type alias (Arc<Mutex<IngestPipeline>>) in pipeline.rs as canonical source; remove local alias from http.rs; update receiver module re-exports and all import paths to use centralized definition.
gRPC LogsReceiver handler
crates/ourios-ingester/src/receiver/grpc.rs, crates/ourios-ingester/src/receiver.rs
Implement LogsReceiver struct wrapping SharedPipeline with export handler that clones the pipeline, spawns blocking WAL append+fsync, recovers from poisoned locks, and maps tenant-resolution errors to INVALID_ARGUMENT status and ingest/WAL errors to INTERNAL status.
Test infrastructure for gRPC and WAL
crates/ourios-ingester/tests/ingest_support/mod.rs
Update SharedPipeline import to use centralized re-export; add shared_wal_pipeline helper for tests requiring real WAL-backed shared pipeline.
gRPC error handling tests
crates/ourios-ingester/tests/rfc0003_11_transport_errors.rs
Replace ignored stub with concrete async tests: unresolvable_request() builder constructs request with missing service.name; first test validates INVALID_ARGUMENT status with error message naming the missing field and failing ResourceLogs[0] with empty pipeline capture; second test asserts valid request succeeds.
Concurrent export durability test
crates/ourios-ingester/tests/rfc0003_15_concurrent_wal_before_ack.rs
Replace ignored placeholder with active Tokio async test spawning N concurrent gRPC Export calls on shared LogsReceiver, asserting all succeed, dropping receiver to release WAL handle, and verifying replay yields exactly N durable OtlpBatch frames with no other frame kinds.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • jensholdgaard/ourios#128: Prior PR introducing the RFC0003.11 and RFC0003.15 test stubs that this PR transforms into concrete gRPC validation tests.
  • jensholdgaard/ourios#134: PR introducing IngestPipeline, ReceiveError, and the foundational receiver::pipeline module upon which the shared pipeline abstraction is built.
  • jensholdgaard/ourios#135: Related through the movement of SharedPipeline type alias from receiver/http.rs to receiver/pipeline.rs as the canonical shared-state definition.

Poem

🐰 Tonic logs now flow through gRPC's gleaming gate,
A shared pipeline embraces HTTP and gRPC's fate,
Tenant failures sing INVALID_ARGUMENT's refrain,
While N concurrent exports dance the WAL's durable chain—
From red-gated stubs to green, the fixtures celebrate!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: adding the OTLP/gRPC LogsService listener and implementing RFC0003.11/.15.
Description check ✅ Passed The description is comprehensive and addresses all required template sections: What (detailed feature explanation), Related (RFC references), and Verification (test results and checks).
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/otlp-receiver-grpc

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 the OTLP/gRPC LogsService receiver for the ingester, wiring the existing WAL-before-ack IngestPipeline behind a tonic service implementation and turning previously ignored RFC0003.11/.15 scenarios into green tests.

Changes:

  • Introduces receiver::grpc::LogsReceiver implementing logs_service_server::LogsService, offloading ingest to spawn_blocking and mapping ReceiveError to gRPC Status.
  • Enables tonic/opentelemetry-proto codegen needed for the generated LogsService trait and updates crate docs to include the new transport.
  • Adds/updates RFC0003 acceptance tests for gRPC error mapping and concurrent WAL durability under concurrent Export calls.

Reviewed changes

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

Show a summary per file
File Description
crates/ourios-ingester/src/receiver/grpc.rs New OTLP/gRPC LogsService implementation over the shared WAL-before-ack pipeline.
crates/ourios-ingester/src/receiver.rs Exposes the new grpc module and updates receiver module docs accordingly.
crates/ourios-ingester/src/lib.rs Updates crate-level docs to include the gRPC listener as landed.
crates/ourios-ingester/Cargo.toml Switches opentelemetry-proto to gen-tonic and adds tonic (codegen-only) dependency.
crates/ourios-ingester/tests/rfc0003_11_transport_errors.rs Converts RFC0003.11 gRPC transport-error expectations into executable tests.
crates/ourios-ingester/tests/rfc0003_15_concurrent_wal_before_ack.rs Converts RFC0003.15 concurrency/durability expectations into a multi-threaded tokio test.
crates/ourios-ingester/tests/ingest_support/mod.rs Adds helper to build a shared pipeline over a real WAL for concurrency/durability assertions.
Cargo.lock Locks the new tonic dependency.

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

Comment on lines +22 to +23
use crate::receiver::http::SharedPipeline;
use crate::receiver::pipeline::ReceiveError;

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 — SharedPipeline moved to receiver::pipeline (alongside IngestPipeline) and re-exported from receiver; grpc no longer depends on http for it. Both transports import it from pipeline.

Comment on lines +63 to +65
// A WAL failure (or a panic in the blocking task) is
// server-side; the batch was not acked (§3.4).
Ok(Err(_)) | Err(_) => Err(Status::internal("ingest failed")),

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 INTERNAL arm now distinguishes: a WAL append/sync failure surfaces its Display detail via Status::internal(e.to_string()) (AppendError/SyncError gained Display in #134), and a blocking-task join failure (panic/cancel) is a separate Status::internal("ingest task failed"). TenantResolution stays INVALID_ARGUMENT.

…C INTERNAL

Review fixes on the gRPC slice:
- `SharedPipeline` lived in `receiver::http`, so `receiver::grpc` imported
  the HTTP module just for it — a wrong dependency direction. Moved the
  alias to `receiver::pipeline` (where `IngestPipeline` lives) and
  re-export it from `receiver`; both transports import it from there.
- The gRPC `export` collapsed WAL-append, WAL-sync, and join (panic/cancel)
  failures into one opaque `INTERNAL("ingest failed")`. Now a WAL failure
  surfaces its Display detail via `Status::internal(e.to_string())`, and a
  blocking-task join failure is a distinct `INTERNAL("ingest task failed")`.

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 2 comments.

Comment on lines +70 to +77
// Assert: a controlled INVALID_ARGUMENT naming the attribute — not a
// panic — and nothing appended.
assert_eq!(status.code(), Code::InvalidArgument);
assert!(
status.message().contains("service.name"),
"the Status names the missing attribute, got {:?}",
status.message(),
);

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 — rfc0003_11 now also asserts the Status message contains ResourceLogs[0] (the failing index), covering the full RFC0003.4/.11 contract (index + attribute), not just the attribute.

Comment on lines +65 to +66
// The blocking ingest task panicked or was cancelled.
Err(_) => Err(Status::internal("ingest task failed")),

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 join-error arm now includes the JoinError Display: Status::internal(format!("ingest task failed: {join}")). (For spawn_blocking a JoinError is always a panic — blocking tasks aren't cancellable — but the Display still distinguishes it clearly.)

…surface JoinError

Review follow-ups: rfc0003_11 now asserts the INVALID_ARGUMENT message names
the failing ResourceLogs index (RFC0003.4/.11), not just the attribute; the
gRPC join-error arm includes the (short, safe) JoinError Display in the
INTERNAL message instead of dropping it.

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 3 comments.

Comment on lines +65 to +66
// The blocking ingest task panicked or was cancelled;
// `JoinError`'s Display is short and safe to surface.

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 f4e1c86 — the comment now says the spawn_blocking task panicked (such tasks cannot be cancelled, so a JoinError here is always a panic).

Comment thread crates/ourios-ingester/Cargo.toml Outdated
Comment on lines +44 to +47
# (RFC0003.6, lands next). Live gRPC/HTTP listeners (`tonic`/`axum`) are
# a later transport slice — this slice is the pure decode layer.
opentelemetry-proto = { version = "0.32", default-features = false, features = ["gen-tonic-messages", "logs", "with-serde"] }
# `gen-tonic` (not just `-messages`) for the `logs_service_server`
# `LogsService` trait the gRPC listener implements (RFC0003.11/.15).

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 f4e1c86 — dropped the stale "later transport slice / pure decode layer" wording; the comment now just explains gen-tonic supplies the LogsService trait the gRPC listener implements.

Comment on lines 23 to +27
//! - [`http`] — the OTLP/HTTP listener ([`http::router`]) wrapping the
//! pipeline: `Content-Type`/`Content-Encoding` dispatch, controlled
//! transport errors, configurable path (RFC0003.11 HTTP arms / .13 /
//! .14). The gRPC listener (`tonic`) follows.
//! .14).
//! - [`grpc`] — the OTLP/gRPC `LogsService` ([`grpc::LogsReceiver`])

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 f4e1c86 — the decode bullet no longer says "No live tonic/axum listener yet"; it now notes the http + grpc transports hand their decoded payload to the shared decode layer.

Three review-surfaced doc/comment staleness fixes: the gRPC join-error
comment now says the spawn_blocking task panicked (such tasks cannot be
cancelled); the opentelemetry-proto dep comment no longer calls listeners
a "later slice"; the receiver decode bullet no longer says "No live
tonic/axum listener yet" (http + grpc modules now exist).

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 2 comments.

Comment on lines +3 to +4
//! Implements `opentelemetry-proto`'s `LogsService` over the shared
//! [`IngestPipeline`]: `export` hands the (already tonic-decoded)

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 01c6c3f — these intra-doc links did not resolve from receiver::grpc (it imports SharedPipeline, not IngestPipeline). Made them plain code spans (the http module doc had the same broken link after the SharedPipeline move; fixed there too). cargo doc is now warning-free.


use crate::receiver::pipeline::{ReceiveError, SharedPipeline};

/// The gRPC `LogsService` over a shared [`IngestPipeline`].

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 01c6c3f — these intra-doc links did not resolve from receiver::grpc (it imports SharedPipeline, not IngestPipeline). Made them plain code spans (the http module doc had the same broken link after the SharedPipeline move; fixed there too). cargo doc is now warning-free.

The grpc + http module docs linked `[`IngestPipeline`]`, which does not
resolve from those modules (they import SharedPipeline, not the type).
Use plain code spans. 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 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