Skip to content

feat(rfc0016): query endpoint handler — POST /v1/query (.1-.4) - #283

Merged
jensholdgaard merged 6 commits into
mainfrom
rfc0016-green-handler
Jun 22, 2026
Merged

feat(rfc0016): query endpoint handler — POST /v1/query (.1-.4)#283
jensholdgaard merged 6 commits into
mainfrom
rfc0016-green-handler

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 22, 2026

Copy link
Copy Markdown
Owner

RFC 0016 green — query endpoint handler (.1.4)

The querier role's transport + dispatch (RFC 0016 §3.2–§3.5). Its RFC 0017 dependency (the typed-row payload) is green in main, so this returns actual rendered rows.

Changes

  • [lib] target (src/lib.rs) exposing pub mod querier so serve/router are testable in-process; the binary (main.rs) is unchanged this slice.
  • querier::serve(QuerierConfig) → QuerierHandle on the receiver's bind → axum::serve(...).with_graceful_shutdown(watch) topology (RFC 0003 mirror); router() split out for tower::oneshot tests.
  • POST /v1/query: required X-Ourios-Tenant (missing/empty → 400 before the engine); Content-Type dispatch (text/plainparse_statement; application/json{"query":…} wrapper or structured-IR JSON); Statement::Logs → run_query, Drift → run_drift; now = wall clock + server default window.
  • 200 JSON Ourios-owned response DTOs — no engine type crosses (H6). Attributes / structured bodies are proto3-JSON via ourios-core's canonical codec (no opentelemetry-proto serde feature); rendered line as UTF-8 text; trace/span ids hex.
  • Limit (§7): the DSL limit caps records and is clamped to MAX_LIMIT; a query with none gets DEFAULT_LIMIT so the endpoint returns rows by design (RFC 0017 only populates records when a limit is present).
  • Error model (H6): DSL/compile → 400 {error:{kind,message}}; Storage500 with the engine's already-scrubbed Display.

Tests

  • RFC0016.1/.2/.3/.4 green via router + tower::oneshot over a real RFC 0005 store (logs end-to-end, tenant scoping + no-header-400, drift routing, malformed→400 with an H6 denylist guard).
  • .5/.6/.7 stay #[ignore]d — next slices: main.rs env-gating + compose (.5/.7), OTel query metrics (.6, will consult the OpenTelemetry MCP for semconv naming).

fmt + clippy -D warnings clean across the server crate.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an HTTP API endpoint (POST /v1/query) for Logs and Drift queries with JSON responses.
    • Enforced tenant scoping via a required X-Ourios-Tenant header.
    • Supports multiple request formats (raw DSL, {"query": ...} wrapper, and structured JSON).
    • Applies consistent limit behavior (defaulting and clamping), returns query statistics, and rejects oversized requests (413).
  • Tests

    • Replaced ignored placeholders with end-to-end integration tests covering tenant isolation, request parsing modes, drift queries, limit normalization, safe error messaging, and oversized-body handling.

@jensholdgaard
jensholdgaard requested a review from Copilot June 22, 2026 10:41
@coderabbitai

coderabbitai Bot commented Jun 22, 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 56 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

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

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

🚦 How do rate limits work?

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

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, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

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: 02e67681-a466-4e8c-8c33-254db6453010

📥 Commits

Reviewing files that changed from the base of the PR and between 72fd2d9 and fe476da.

📒 Files selected for processing (1)
  • crates/ourios-server/src/querier.rs
📝 Walkthrough

Walkthrough

Adds the ourios-server querier HTTP role: a POST /v1/query endpoint backed by axum that enforces X-Ourios-Tenant, parses DSL text or JSON bodies, clamps query limits, dispatches to Querier::run_query/run_drift, and returns owned JSON DTOs with scrubbed errors. RFC0016 integration test scenarios 1–4 are activated.

Changes

RFC0016 /v1/query querier endpoint

Layer / File(s) Summary
Crate setup: dependencies and module export
crates/ourios-server/Cargo.toml, crates/ourios-server/src/lib.rs
Adds ourios-querier, serde, serde_json runtime dependencies and tower dev-dependency; introduces the crate root with the public querier module export.
Config, handle, state, and server wiring
crates/ourios-server/src/querier.rs
Defines QuerierConfig, QuerierHandle with shutdown(), QuerierState shared state, router() wiring for /v1/query, and serve() that binds TcpListener and drives axum::serve with watch-channel graceful shutdown.
/v1/query handler, parsing, and limit utilities
crates/ourios-server/src/querier.rs
Implements the handler that validates X-Ourios-Tenant, parses text/plain DSL or application/json bodies (including {"query":"..."} wrapper), clamps/inserts Limit stages via apply_limit, dispatches to run_query or run_drift, and serializes results or errors as JSON.
Response/error DTO layer and JSON encoding
crates/ourios-server/src/querier.rs
Defines owned response DTOs (LogRowDto, LogBodyDto, stats, query/drift wrappers), From conversions from engine types, canonical attribute/body encoding helpers, json_ok/error_response/query_error_response constructors mapping QueryError to 400/500 with {error:{kind,message}} shape, QueryWrapper for JSON body deserialization, and unit tests for tenant/limit/parse behavior.
Integration test helpers and utilities
crates/ourios-server/tests/rfc0016_query_endpoint.rs
Introduces HUGE_WINDOW and recent_ns() for deterministic fixture timestamps, mined()/write_records() for Parquet log data fixtures, widened() for audit-event fixtures, and async post() helper for in-process router testing via tower::ServiceExt::oneshot.
RFC0016 integration test scenarios (1–4 and oversize body)
crates/ourios-server/tests/rfc0016_query_endpoint.rs
Activates scenarios 1 (basic query + stats), 2 (tenant scoping + missing-header 400), JSON request modes (wrapper and structured-IR JSON), 3 (drift result shape), 4 (malformed DSL → 400 without engine internals), and oversize body rejection (413). Scenarios 5–7 remain ignored stubs.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant query_handler as POST /v1/query
  participant parse_body
  participant apply_limit
  participant Querier

  Client->>query_handler: POST /v1/query<br/>(X-Ourios-Tenant, Content-Type, body)
  query_handler->>query_handler: tenant_from_headers()
  alt missing / empty tenant
    query_handler-->>Client: 400 {error:{kind:"missing_tenant",...}}
  end
  query_handler->>parse_body: bytes + Content-Type
  alt parse error
    parse_body-->>query_handler: Err(dsl_message)
    query_handler-->>Client: 400 {error:{kind:"dsl_error",...}}
  end
  query_handler->>apply_limit: clamp Limit stage to MAX_LIMIT
  alt Logs statement
    query_handler->>Querier: run_query(tenant, statement, now_ns)
  else Drift statement
    query_handler->>Querier: run_drift(tenant, statement, now_ns)
  end
  alt success
    Querier-->>query_handler: Ok(QueryResult / DriftResult)
    query_handler-->>Client: 200 {rows, records, stats}
  else QueryError::Dsl / Validation
    Querier-->>query_handler: Err(dsl/validation error)
    query_handler-->>Client: 400 {error:{kind, message}}
  else QueryError::Execution / Storage
    Querier-->>query_handler: Err(engine error)
    query_handler-->>Client: 500 {error:{kind:"internal",...}}
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • jensholdgaard/ourios#282: Directly replaces the #[ignore]d todo!() stubs in tests/rfc0016_query_endpoint.rs with active integration tests for RFC0016 scenarios.
  • jensholdgaard/ourios#86: The ourios-server HTTP /v1/query implementation depends on the ourios-querier crate newly exposed by this PR.
  • jensholdgaard/ourios#277: The /v1/query handler's apply_limit and limit normalization logic directly builds on the row-materialization changes (QueryResult.records, LogRow) returned by ourios-querier::run_query.

Poem

🐇 A query hops in, tenant checked with care,
DSL or JSON — the endpoint's aware.
Limits are clamped, no engine secrets spill,
Logs and drift return with measured skill.
The rabbit rejoices: four scenarios pass,
RFC0016 blooms at last! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing the query endpoint handler (POST /v1/query) for RFC 0016, covering phases .1-.4.
Description check ✅ Passed The description covers all required sections: detailed summary of changes, related RFC references, and comprehensive implementation details. However, the checklist items are not explicitly marked as completed.
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 rfc0016-green-handler

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.

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 RFC 0016 querier role’s HTTP transport layer: an in-process-testable Axum router() and a serve() wrapper that binds a listener and supports graceful shutdown, exposing POST /v1/query with tenant enforcement, content-type dispatch, and Ourios-owned JSON response/error DTOs.

Changes:

  • Add ourios_server::querier::{router, serve, QuerierConfig, QuerierHandle} with POST /v1/query handler (tenant header, text/plain vs application/json parsing, limit clamping, error mapping).
  • Convert RFC0016 .1.4 integration tests from ignored stubs into tower::oneshot router-driven green tests using a real RFC 0005 store.
  • Introduce a library target (src/lib.rs) and add dependencies (ourios-querier, serde(_json), tower).

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/ourios-server/tests/rfc0016_query_endpoint.rs Turns RFC0016 .1.4 into end-to-end in-process handler tests (store fixtures + oneshot requests).
crates/ourios-server/src/querier.rs New querier role implementation: router, server lifecycle, request parsing, query dispatch, and JSON DTOs/errors.
crates/ourios-server/src/lib.rs Adds ourios-server library surface exporting pub mod querier for in-process tests/reuse.
crates/ourios-server/Cargo.toml Adds querier/serde/tower dependencies needed for the new HTTP query endpoint + tests.
Cargo.lock Locks new transitive dependencies.

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

Comment thread crates/ourios-server/src/querier.rs Outdated
Comment thread crates/ourios-server/src/querier.rs
Comment thread crates/ourios-server/src/querier.rs

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

🧹 Nitpick comments (1)
crates/ourios-server/tests/rfc0016_query_endpoint.rs (1)

127-130: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Cap response body size in the in-process post helper.

Using usize::MAX here makes test runs vulnerable to unbounded buffering if a regression returns an unexpectedly large payload. A fixed upper bound fails fast and keeps CI stable.

Proposed patch
@@
 const TS0: u64 = 1_775_127_480_000_000_000;
 /// A default window so wide that fixed-past fixtures always fall inside the
 /// no-`range` look-back (≈100 years of nanos, well under `u64::MAX`).
 const HUGE_WINDOW: u64 = 100 * 365 * 24 * 60 * 60 * 1_000_000_000;
+const MAX_TEST_RESPONSE_BYTES: usize = 2 * 1024 * 1024;
@@
-    let bytes = to_bytes(response.into_body(), usize::MAX)
+    let bytes = to_bytes(response.into_body(), MAX_TEST_RESPONSE_BYTES)
         .await
         .expect("read body");
🤖 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-server/tests/rfc0016_query_endpoint.rs` around lines 127 - 130,
The to_bytes call in the post helper is using usize::MAX as the buffer limit,
which can cause unbounded memory buffering if a regression returns an
unexpectedly large response payload. Replace the usize::MAX argument in the
to_bytes invocation with a reasonable fixed upper bound value that will fail
fast and keep tests stable. Choose a limit appropriate for the expected response
size in your tests, such as a specific megabyte value or a defined constant.
🤖 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-server/src/querier.rs`:
- Around line 195-255: Add a `#[cfg(test)]` module at the end of the file to
include unit tests for the three core API contract helpers:
`tenant_from_headers`, `parse_body`, and `apply_limit`. For
`tenant_from_headers`, test valid tenant IDs, missing headers, non-UTF-8 values,
and empty strings. For `parse_body`, test the different Content-Type scenarios
(text/plain, application/json with query wrapper, and structured-IR JSON), UTF-8
validation, and error cases. For `apply_limit`, test scenarios with existing
limits, missing limits using defaults, clamping to cap, and ensure exactly one
Limit stage remains in the output.
- Around line 103-193: The query request path (router, serve, and handle_query
functions) lacks observability integration required by coding guidelines. Add
Prometheus metrics to track query operations in the router function, instrument
the serve function with structured Ourios logs for startup and shutdown events,
and add comprehensive observability to the handle_query function including
metric counters for successful and failed queries, structured Ourios logs for
query execution with tenant and statement details, and distributed tracing spans
that wrap the entire request handling to trace both the Logs and Drift statement
execution paths.

In `@crates/ourios-server/tests/rfc0016_query_endpoint.rs`:
- Around line 169-181: The assertion in the post request block only validates
that one row is returned when scoped to the acme tenant, but does not verify
that the returned row actually belongs to the acme tenant rather than the other
tenant. Add an additional assertion that checks a tenant-identifying field in
the returned response data (accessed through json["rows"] or the response
structure) to confirm the row belongs to acme and not other, ensuring the
tenant-scoping filter is working correctly and not just truncating results.

---

Nitpick comments:
In `@crates/ourios-server/tests/rfc0016_query_endpoint.rs`:
- Around line 127-130: The to_bytes call in the post helper is using usize::MAX
as the buffer limit, which can cause unbounded memory buffering if a regression
returns an unexpectedly large response payload. Replace the usize::MAX argument
in the to_bytes invocation with a reasonable fixed upper bound value that will
fail fast and keep tests stable. Choose a limit appropriate for the expected
response size in your tests, such as a specific megabyte value or a defined
constant.
🪄 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: e9660fdf-c269-471b-9873-40a6c9d1d58f

📥 Commits

Reviewing files that changed from the base of the PR and between 2fb278b and ce6c286.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • crates/ourios-server/Cargo.toml
  • crates/ourios-server/src/lib.rs
  • crates/ourios-server/src/querier.rs
  • crates/ourios-server/tests/rfc0016_query_endpoint.rs

Comment thread crates/ourios-server/src/querier.rs
Comment thread crates/ourios-server/src/querier.rs
Comment thread crates/ourios-server/tests/rfc0016_query_endpoint.rs Outdated
jensholdgaard and others added 2 commits June 22, 2026 14:03
The querier role's transport + dispatch (RFC 0016 §3.2-§3.5), green for the
handler scenarios. Adds a [lib] target so serve/router are testable
in-process; main.rs wiring (env-gating, compose) + OTel metrics are the
next slices.

- querier::serve(QuerierConfig) -> QuerierHandle on the receiver's
  bind + axum::serve(...).with_graceful_shutdown(watch) topology; router()
  split out for in-process (tower oneshot) tests.
- POST /v1/query: required X-Ourios-Tenant header (missing/empty → 400
  before the engine); Content-Type dispatch (text/plain → parse_statement;
  application/json → {"query":…} wrapper or structured-IR JSON);
  Statement::Logs → run_query, Drift → run_drift; now = wall clock,
  server-supplied default window.
- 200 JSON: Ourios-owned response DTOs (no engine type crosses — H6).
  Attributes/structured bodies encoded as proto3-JSON via ourios-core's
  canonical codec (no opentelemetry-proto serde feature); rendered line as
  UTF-8 text; trace/span ids hex. The query's limit caps records only and
  is clamped to MAX_LIMIT; a query with none gets DEFAULT_LIMIT so the
  endpoint returns rows (RFC 0016 §7).
- Error model (H6): DSL/compile failure → 400 {error:{kind,message}};
  Storage → 500 with the engine's already-scrubbed Display.

Fills RFC0016.1/.2/.3/.4 (router via tower oneshot over a real store);
.5/.6/.7 stay #[ignore]d for the main-wiring + observability slices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback on the query endpoint handler:
- QueryWrapper is now #[serde(deny_unknown_fields)] so a structured-IR
  object (predicate/stages) can't be mis-dispatched as the {"query":…}
  wrapper (Copilot).
- Correct the reconstruction-marker comment to match the wire value
  "retained_verbatim" (Copilot).
- Add colocated #[cfg(test)] unit tests for tenant_from_headers /
  apply_limit / parse_body (text-plain + JSON wrapper + structured-IR +
  malformed), and a handler test for both application/json request modes
  (Copilot + CodeRabbit).
- Strengthen the tenant-isolation test to be tenant-identifying: acme has
  template 1, other has template 2; querying template_id==2 as acme
  returns 0 rows (proves isolation, not truncation) (CodeRabbit).

Read-path metrics / structured logs / tracing (CodeRabbit) are deferred to
the RFC0016.6 observability slice (the querier has no metric surface yet).

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

Comment thread crates/ourios-server/src/querier.rs
Comment thread crates/ourios-server/src/querier.rs
- Apply DefaultBodyLimit (MAX_BODY_BYTES = 1 MiB) to the query router so an
  oversized POST body is rejected 413 rather than read whole into memory
  via the Bytes extractor (Copilot — DoS). + an oversize-body test.
- apply_limit now clamps the effective (last) Limit stage in place instead
  of removing all Limit stages and re-appending, which reordered the
  pipeline; stage order is DSL semantics (RFC 0002) and reordering is a
  footgun once more stages execute (Copilot).

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

Comment thread crates/ourios-server/tests/rfc0016_query_endpoint.rs Outdated
Comment thread crates/ourios-server/src/querier.rs
- Tests no longer hard-code a 2026 timestamp: fixtures are stamped an hour
  before the request's now (recent_ns), so the no-range look-back window
  always covers them regardless of the machine's wall clock (Copilot).
- apply_limit now normalizes to exactly one Limit (the clamped last, in its
  position relative to non-limit stages; earlier shadowed limits dropped) —
  matching its doc and removing the multi-limit footgun. Added a
  multi-limit unit case (Copilot).

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 4 out of 5 changed files in this pull request and generated 1 comment.

Comment thread crates/ourios-server/src/querier.rs
Copilot: apply_limit's no-existing-limit branch appended `default` without
clamping, so a misconfigured `default > cap` would violate the 'no larger
than cap' contract. Append `default.min(cap)`.

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 4 out of 5 changed files in this pull request and generated 1 comment.

Comment thread crates/ourios-server/src/querier.rs Outdated
Match `&stages[last]` and copy the `u64` out of `Stage::Limit` rather
than matching `stages[last]` by value. The prior form compiled (binding
a `Copy` field is a partial copy, not a move of the non-`Copy` `Stage`),
but the borrow makes the no-move intent explicit.

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

@jensholdgaard
jensholdgaard merged commit 70b1f7a into main Jun 22, 2026
21 checks passed
@jensholdgaard
jensholdgaard deleted the rfc0016-green-handler branch June 22, 2026 14:02
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