Skip to content

feat: route modified-OTLP sketch metrics through precompute engine (PR B) - #6

Merged
zzylol merged 1 commit into
mainfrom
feat/decode-modified-otel-sketches
Apr 14, 2026
Merged

zzylol merged 1 commit into
mainfrom
feat/decode-modified-otel-sketches

Conversation

@zzylol

@zzylol zzylol commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

PR B in the end-to-end sketch path roadmap (Phase 1). Builds on PR #5 (PR A, vendoring the modified OTLP proto).

Replaces the debug-log stub arms PR A added with a real routing function that walks the five first-class sketch metric variants (Metric.data = DDSketch / KLLSketch / CountSketch / CountMinSketch / HLLSketch) and dispatches each *SketchDataPoint through the precompute engine via WorkerMessage::AccumulatorInput.

What lands

  1. route_modified_otlp_sketches_to_precompute(req, ingest_state) in drivers/ingest/otel.rs. Walks resource→scope→metric and flattens the per-variant data points into a uniform ModifiedOtlpSketchDp struct. Same label fan-in (scope_attrs ∪ resource_attrs ∪ dp.attributes), same (agg_id, group_key) matching against ingest_state.agg_configs, same WorkerMessage::AccumulatorInput routing as the existing raw-metrics path.

  2. decode_modified_otlp_sketch_bytes(kind, encoding, bytes) — per-variant decoder dispatcher. Today:

    • Accepts only encoding = ENCODING_PROTO (= 1). Delta variants (PR C) and msgpack variants (PR I) return Err.
    • SketchKind::CountMin → calls CountMinSketchAccumulator::from_sketchlib_proto_bytes — fully wired end-to-end.
    • SketchKind::Kll / DdSketch / CountSketch / HllErr("decoder not yet implemented (tracked in PR C, task #8)"). Caller falls through to the §5.2 forwarding adapter so the user still gets a correct answer.
  3. CountMinSketchAccumulator::from_sketchlib_proto_bytes(buf) — decodes asap_sketchlib::proto::sketchlib::CountMinState, validates dims, picks counts_int or counts_float based on the counter_type enum, reshapes flat counts into Vec<Vec<f64>>, and constructs via CountMinSketch::from_legacy_matrix. INT128 counter type is rejected with a PR C TODO (rare in practice; uses interleaved hi/lo encoding).

  4. Wiring: both MetricsServiceImpl::export (gRPC) and handle_otlp_http (HTTP) call the new function immediately after the existing route_otlp_to_precompute.

Tests

Four new unit tests in count_min_sketch_accumulator.rs covering int64 round-trip, float64 round-trip, dimension mismatch, and zero-dim rejection. All pass.

Validation

  • cargo check -p query_engine_rust --all-targets: clean
  • cargo clippy -p query_engine_rust --all-targets -- -D warnings: clean
  • cargo fmt --check: clean
  • cargo test -p query_engine_rust --lib: 439 passed, 0 failed, 5 ignored (4 new tests on top of PR A's 435)

End-to-end value with PR A + PR B merged

A DataCollector countminsketchprocessor emitting via the modified OTLP wire now flows hot end-to-end:

OTLP gRPC/HTTP receiver
  → route_modified_otlp_sketches_to_precompute
  → decode_modified_otlp_sketch_bytes (CountMin path)
  → CountMinSketchAccumulator::from_sketchlib_proto_bytes
  → WorkerMessage::AccumulatorInput
  → precompute engine sketch_panes merge
  → SimpleMapStore
  → SimpleEngine PromQL queries answer hot

The other four sketch types (KLL / DDSketch / CountSketch / HLL) fall through to the §5.2 forwarding adapter today; their decoders land in PR C (task #8). PR D (task #9) adds an end-to-end integration test against a real precompute engine instance.

What this PR does NOT do

  • Does not delete the legacy SketchEnvelopeAccumulator / attribute-bytes path (transitional hatch for standard-OTLP clients that sideband sketches in DataPoint attributes; can be removed after PR D validates the new path).
  • Does not handle delta transmission (ENCODING_PROTO_DELTA). PR C adds per-series baseline tracking.
  • Does not wire any of the four other sketch types end-to-end (PR C).
  • Does not implement series_id-based descriptor lookup (PR G).

🤖 Generated with Claude Code

…R B)

Replaces the debug-log stub arms PR A added in `otlp_to_metric_points_and_sketches`
with a real routing function that walks the five first-class sketch
metric variants (`Metric.data = DDSketch / KLLSketch / CountSketch /
CountMinSketch / HLLSketch`) and dispatches each `*SketchDataPoint`
through the precompute engine via `WorkerMessage::AccumulatorInput`.

This is **PR B** in the implementation plan tracked in
`docs/pipeline-query-catalog.md` §10 in DataCollector#153.

What's wired
------------
1. **`route_modified_otlp_sketches_to_precompute(req, ingest_state)`**
   in `asap-query-engine/src/drivers/ingest/otel.rs`. Walks
   `request.resource_metrics → scope_metrics → metrics → metric.data`
   for the five new sketch variants, flattens each per-variant data
   point into a `ModifiedOtlpSketchDp { kind, attrs, time_unix_nano,
   sketch, encoding }`, and dispatches:
   - **labels**: `merge_point_attributes(base_labels, dp.attributes)`
     where `base_labels = scope_attrs ∪ resource_attrs` — same label
     fan-in as the existing raw-metrics path.
   - **timestamp**: `dp.time_unix_nano / 1_000_000` (ms).
   - **per-config matching**: same as `route_otlp_to_precompute` —
     match against `ingest_state.agg_configs` by metric name (or
     `spatial_filter` / `spatial_filter_normalized`), extract the
     group key via `IngestState::extract_group_key_for(series_key, config)`,
     and emit one `WorkerMessage::AccumulatorInput { agg_id,
     group_key, timestamp_ms, accumulator, ingest_received_at }` per
     matching config (clone via `clone_boxed_core` so one accumulator
     can fan out to multiple agg configs).

2. **`decode_modified_otlp_sketch_bytes(kind, encoding, bytes)`** —
   the per-variant decoder dispatcher. Today:
   - Accepts only `encoding = ENCODING_PROTO (= 1)`. `ENCODING_PROTO_DELTA`
     (delta transmission, tracked in PR C) and `ENCODING_MSGPACK` /
     `ENCODING_MSGPACK_DELTA` (PR I) return `Err` with a clear message.
   - For `SketchKind::CountMin` calls the new
     `CountMinSketchAccumulator::from_sketchlib_proto_bytes(buf)`
     constructor (see below) — fully wired end-to-end.
   - For `SketchKind::Kll / DdSketch / CountSketch / Hll` returns
     `Err("decoder for {kind:?} not yet implemented (tracked in PR C, task #8)")`.
     Caller drops the data point and falls through to the §5.2
     forwarding adapter so the user still gets a correct answer.

3. **`CountMinSketchAccumulator::from_sketchlib_proto_bytes(buf)`** in
   `precompute_operators/count_min_sketch_accumulator.rs`. Decodes the
   `asap_sketchlib::proto::sketchlib::CountMinState` message and
   constructs the accumulator via `CountMinSketch::from_legacy_matrix`:
   - decode `CountMinState` via `prost::Message::decode`
   - reject zero rows/cols
   - read `counter_type` enum and pick `counts_int` or `counts_float`
   - validate `len() == rows * cols`
   - reshape flat counts into `Vec<Vec<f64>>` row-major
   - `INT128` counter type rejected with "not yet supported (PR C)" —
     it stores `(hi, lo)` interleaved pairs and is rare in practice
   - returns `Result<Self, Box<dyn std::error::Error>>` so the
     dispatcher can fall through to fallback on any error

4. **Wiring in both ingest paths**: `MetricsServiceImpl::export` (gRPC)
   and `handle_otlp_http` (HTTP) both call
   `route_modified_otlp_sketches_to_precompute` immediately after the
   existing `route_otlp_to_precompute` so raw metrics and sketches
   travel side-by-side through the same precompute engine.

5. **Per-variant counters + summary log**: the routing function counts
   `routed`, `decoded_failed` (fall-through to fallback), and
   `unconfigured` (no matching agg config) and emits a single
   `debug!` line per request with the totals — matches the style of
   the existing `route_otlp_to_precompute` summary.

Tests
-----
Four new unit tests in `count_min_sketch_accumulator.rs`:

- `test_from_sketchlib_proto_bytes_int64` — round-trips a 2x3 matrix
  through `CountMinState{counter_type=INT64, counts_int=[1..6]}` and
  asserts the accumulator's inner sketch matrix matches.
- `test_from_sketchlib_proto_bytes_float64` — same for a 2x2 matrix
  with `FLOAT64` counters.
- `test_from_sketchlib_proto_bytes_dimension_mismatch` — `counts_int`
  length doesn't match `rows * cols`; expect `Err` mentioning
  "counts_int".
- `test_from_sketchlib_proto_bytes_zero_dims_rejected` — default
  `CountMinState` (rows=0, cols=0); expect `Err` mentioning
  "zero dims".

Validation
----------
- `cargo check -p query_engine_rust --all-targets`: clean
- `cargo clippy -p query_engine_rust --all-targets -- -D warnings`: clean
- `cargo fmt --check`: clean
- `cargo test -p query_engine_rust --lib`: **439 passed**, 0 failed,
  5 ignored (was 435 in PR A — 4 new tests above)

What this delivers end-to-end
-----------------------------
With PR A + PR B merged, a DataCollector `countminsketchprocessor`
that emits via the modified OTLP wire (`Metric.data = CountMinSketch{
data_points: [CountMinSketchDataPoint{...sketch=<CountMinState
proto bytes>, encoding=COUNT_MIN_SKETCH_ENCODING_PROTO}] }`) now
flows end-to-end through:

    OTLP gRPC/HTTP receiver
      → route_modified_otlp_sketches_to_precompute
      → decode_modified_otlp_sketch_bytes (CountMin path)
      → CountMinSketchAccumulator::from_sketchlib_proto_bytes
      → WorkerMessage::AccumulatorInput
      → precompute engine sketch_panes merge
      → SimpleMapStore
      → SimpleEngine PromQL queries answer hot

The other four sketch types (KLL, DDSketch, CountSketch, HLL) still
fall through to the §5.2 forwarding adapter today; their decoders
land in PR C (task #8). PR D (task #9) adds an end-to-end integration
test that exercises the CountMin hot path against a real precompute
engine instance.

What this PR does NOT do
------------------------
- Does not delete the legacy `SketchEnvelopeAccumulator` / attribute-bytes
  path. Modern DataCollector processors don't use it, but it stays
  as a transitional hatch for standard-OTLP clients that sideband
  sketches in DataPoint attributes. Removal can happen after PR D
  validates the new path on a real workload.
- Does not handle delta transmission (`ENCODING_PROTO_DELTA`). PR C
  will add per-series baseline tracking and delta application.
- Does not wire any of the four other sketch types end-to-end.
  Those decoders are PR C territory.
- Does not implement `series_id`-based descriptor lookup (PR G, where
  the controller starts minting them).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request Apr 14, 2026
Adds the first real end-to-end correctness test for the modified-OTLP
sketch path landed in PR A (#5, vendoring) + PR B (#6, routing +
CountMin decoder).

The test exercises every public surface in the Phase 1 hot path:

  1. Build a `StreamingConfig` with a single `AggregationConfig` for
     `http_requests_total` with `aggregation_type=CountMinSketch`,
     `grouping_labels=[service]`, 1-second tumbling windows.
  2. Spawn a real `PrecomputeEngine` (workers + router + ingest state).
  3. Spawn a real `OtlpReceiver::with_ingest_state(...)` wired to the
     same engine — modeling the production wiring `main.rs` does.
  4. POST a real protobuf-encoded `ExportMetricsServiceRequest` over
     OTLP HTTP at `/v1/metrics`. The request carries a single
     `Metric.data = CountMinSketch{ data_points: [
        CountMinSketchDataPoint { sketch: <CountMinState bytes>,
        encoding: COUNT_MIN_SKETCH_ENCODING_PROTO, attributes:
        [{service: "auth"}], time_unix_nano: 100ms } ] }` payload
     with a known 2x4 matrix (counts: row 0 = [1,2,3,4],
     row 1 = [5,6,7,8]).
  5. POST a second OTLP request timestamped 2 s past epoch (past the
     1 s window end) so the precompute engine's watermark advances
     and closes window 0.
  6. Wait for the periodic flush (100ms interval) to fire.
  7. Drain the `CapturingOutputSink`, find the entry for window 0,
     downcast the `Box<dyn AggregateCore>` to
     `CountMinSketchAccumulator`, read `inner.sketch()`, and assert
     each row matches the expected `Vec<f64>`.

What this validates end-to-end:

- PR A: the vendored `asap_otel_proto` crate's tonic bindings expose
  `Metric.data::Countminsketch` and the typed `CountMinSketchDataPoint`
  fields, and the prost build pipeline produces decoders that the
  test crate can use directly to construct a request.
- PR B routing: `route_modified_otlp_sketches_to_precompute` walks
  the `Metric.data` oneof, flattens each per-variant data point into
  `ModifiedOtlpSketchDp`, matches the metric against
  `ingest_state.agg_configs` by name, computes the group key from
  the `service` attribute via `IngestState::extract_group_key_for`,
  and emits `WorkerMessage::AccumulatorInput` correctly.
- PR B decoder: `CountMinSketchAccumulator::from_sketchlib_proto_bytes`
  decodes the `CountMinState` proto, picks the int64 counter path,
  reshapes the flat counts into `Vec<Vec<f64>>`, and constructs the
  underlying `CountMinSketch` via `from_legacy_matrix` — round-trip
  matches the original matrix exactly.
- Precompute engine sketch-pane merge: the `WorkerMessage::AccumulatorInput`
  reaches the right worker via the (agg_id, group_key) hash, lands
  in the `sketch_panes` of the matching `GroupState`, and survives
  the watermark-driven window close to be emitted via the
  `OutputSink`.
- Window close + sink: after the watermark advances past the 1 s
  window end, the worker's flush emits a `(PrecomputedOutput,
  Box<dyn AggregateCore>)` tuple with `start_timestamp=0` and
  `end_timestamp=1000` to the `CapturingOutputSink`.
- Round-trip correctness: the matrix that comes out of the sink is
  bit-identical to what went in over OTLP, proving there is no
  data loss in the routing/decoder/merge path for a single-input
  window.

Test layout
-----------
- New file `asap-query-engine/tests/e2e_modified_otlp_sketch_path.rs`
- Helpers:
    `make_count_min_agg_config(...)` — builds a tumbling-window
      CountMinSketch `AggregationConfig` with a `service` group
    `engine_config(...)` — builds a `PrecomputeEngineConfig` with a
      100ms flush interval so the test does not have to wait long
    `build_count_min_state(...)` — builds a `CountMinState` proto
      from a known matrix
    `build_export_request(...)` — wraps a `CountMinSketchDataPoint`
      in a fully-formed `ExportMetricsServiceRequest`
    `post_otlp_http(...)` — sends a protobuf body to the OTLP HTTP
      endpoint at `localhost:port/v1/metrics` and asserts 2xx

Validation
----------
- `cargo check -p query_engine_rust --tests`: clean
- `cargo clippy -p query_engine_rust --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `cargo test -p query_engine_rust --test e2e_modified_otlp_sketch_path`:
    `test e2e_count_min_sketch_modified_otlp_path ... ok`
    `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured`
- `cargo test -p query_engine_rust --lib`: 439 passed, 0 failed, 5 ignored

What's still PR C territory
---------------------------
- KLL / DDSketch / CountSketch / HLL e2e coverage. PR C (task #8) is
  re-scoped as smaller per-sketch-type follow-ups (PR C-CountSketch,
  PR C-HLL, PR C-KLL, PR C-DDSketch); each adds a per-type decoder
  and an analogous test arm to this file.
- Delta transmission (`*_ENCODING_PROTO_DELTA`). PR C-delta adds
  per-series baseline tracking and a delta-merge codepath; this test
  file gets a `delta` test case once that lands.
- MessagePack encoding parity (`*_ENCODING_MSGPACK` variants).
  PR I (task #14) adds those; this test file gets a four-way
  `(format, mode)` correctness assertion at that point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit c06353c into main Apr 14, 2026
5 of 6 checks passed
@zzylol
zzylol deleted the feat/decode-modified-otel-sketches branch April 14, 2026 17:00
zzylol added a commit that referenced this pull request May 13, 2026
Post-M2.3 reorg #5 of 8. Creates a new `sketch_db::lifecycle` module
and moves `SchemaEvictionService` (and friends) into
`lifecycle/eviction.rs`. The sid-level lifecycle FIELDS and
methods on `SketchInstanceMetadata` / `SketchStore` stay in
`index/` next to the data they gate — only the schedule-driven
*service* moves here.

`schema/mod.rs` re-exports the eviction types under their legacy
path (`sketch_db::schema::SchemaEvictionService`) so existing
consumers compile unchanged. Canonical home is now
`sketch_db::lifecycle::*`.

This is the structural skeleton for the upcoming sub-PRs:
- #6 will add `lifecycle::reconcile_from_streaming_config`,
  reimplementing schema/'s reconcile semantics over the sid catalog.
- #7 will then delete `schema/` once the only resident is the
  thin re-export.

783/783 lib tests pass.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 13, 2026
…_db/query/ (#182)

Post-M2.3 reorg #6 of 8. Moves
`query_engines/{timeline_dispatch.rs, window_merger.rs}` →
`storage_engines/sketch_db/query/`.

Both files are warm-tier driver primitives — `timeline_dispatch`
combines per-segment results across reconfigure boundaries that
only exist *within* a SketchStore; `window_merger` stitches per-
window results from the same. Neither belongs to the
backend-agnostic orchestration layer (`query_engines/`).

`query_engines/mod.rs` re-exports both modules under their legacy
paths via `pub use crate::storage_engines::sketch_db::query::*`,
so existing consumers (in `engine.rs`, `http.rs`, schema docs)
compile unchanged.

783/783 lib tests pass.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant