Skip to content

otap: merge into one crate, real OtapPdata direct from SketchEnvelope, no staging required - #9

Open
zzylol wants to merge 6 commits into
mainfrom
feat/verify-otap-bridge-against-real-workspace
Open

otap: merge into one crate, real OtapPdata direct from SketchEnvelope, no staging required#9
zzylol wants to merge 6 commits into
mainfrom
feat/verify-otap-bridge-against-real-workspace

Conversation

@zzylol

@zzylol zzylol commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Why

Started from otap_bridge.rs (the real OtapPdata <-> OtapMetricRecords binding) being written entirely from reading upstream source, never compiled. Along the way, review surfaced three deeper architectural questions worth answering for real rather than by assumption:

  1. Does sketch traffic riding a generic OTLP metric need its own dictionary/schema-reuse economics, or does OTAP's real Arrow encoding already provide that?
  2. Why does otap-patch/ need to be a separate overlay staged into a checked-out OTAP Dataflow workspace at all — why not just depend on the real crates directly?
  3. Given (2), why keep OtapMetricRecords/flat-RecordBatch as intermediate hops between SketchEnvelope and OtapPdata instead of encoding straight to the real type?

This PR answers all three with real, verified code, not reasoning.

What

One crate, not two. otap-patch/ is gone. asap-precompute-rs now depends directly on the real otel-arrow-dfe-* crates via a plain git dependency (same mechanism already used for asap_sketchlib), behind a new otap-engine Cargo feature. No more staging files into a separately-checked-out OTAP Dataflow workspace to compile — cargo build --features otap-engine just works, fetching from git like any other dependency.

One transport. AsapSketchesProcessor sends only via effect_handler.send_message_with_source_node. An earlier revision on this branch added a direct-TCP "wire lane" as a second transport (otap_wire.rs + a standalone AsapSketchesReceiver receiver node) — that's been removed.

One encoding, no intermediate hops. The wire lane existed to give sketch traffic dictionary/schema-reuse economics a generic OTLP metric didn't have (the legacy SeriesDictionary SCHEMA/DICTIONARY/RECORD tiering). Turns out unnecessary: OTAP's real Arrow encoder (encode_metrics_otap_batch) already dictionary-encodes the metric name and every string-valued attribute key/value by construction — confirmed against the real workspace and guarded by a permanent test:

=== payload_type UnivariateMetrics ===
  name : Dictionary(UInt8, Utf8)
=== payload_type NumberDpAttrs ===
  parent_id : Dictionary(UInt8, UInt32)
  key       : Dictionary(UInt8, Utf8)
  str       : Dictionary(UInt16, Utf8)

That's the same "send it once, reference it after that" shape SeriesDictionary was reinventing at the application layer, done at the columnar/Arrow-IPC level instead — no dictionary state for this adapter to track, no second wire protocol, none of the "must be one ordered, single-consumer stream" correctness constraint a hand-rolled series_id scheme carries.

Once that was settled, the old encode_batch (→ flat RecordBatch) → lift (→ OtapMetricRecords) → otap_bridge (→ OtapPdata) chain was also revisited: those three representations for one job only existed to serve a second adapter (Telegraf/Vector) that was never actually built in this repo. The new otap::codec module implements OTAP's own MetricsView trait family directly over &[SketchEnvelope] — one hop, SketchEnvelope straight to real OtapPdata, and back.

encode_batch/decode_batch/records::{flatten,lift} still exist, still tested — they now exclusively back the legacy SeriesDictionary/otap::wire transport and its standalone example binaries, not the otap-engine path.

How

A real bug found and fixed while rewriting the encoder: encode_batch unconditionally sets _asap_envelope for every row, including estimate-mode envelopes (empty payload, the gauge value rides in value instead). arrow_array::BinaryArray treats Some(&[]) as a present, empty value, not null — confirmed empirically (BinaryArray::from_opt_vec(vec![Some(&[])]).is_null(0) == false). So an estimate-mode row would silently misroute through the envelope decode path with an empty payload instead of the scalar path. otap::codec's direct encoder only attaches _asap_envelope (and its sibling _asap_* attributes) when the payload is actually non-empty, with a defensive .filter(|b| !b.is_empty()) on decode too. New regression test: estimate_mode_envelope_round_trips_as_a_scalar_not_an_empty_envelope. The legacy encode_batch/decode_batch pair keeps the original behavior untouched — fixing it there was out of scope here.

Real trait-signature drift the compiler caught during the rewrite (uninhabited-placeholder MetricsView impls, copied from the old file and re-typed by hand): ResourceView::AttributesIter (not AttributeIter), ResourceMetricsView::schema_url() -> Option<Str> (not bare Str), HistogramDataPointView/BucketsView needing explicit BucketCountIter/ExplicitBoundsIter associated types, ExponentialHistogramDataPointView::zero_threshold(). All caught by cargo build, none guessable from memory.

Why a git dependency works without workspace staging: open-telemetry/otel-arrow's own crates (otel-arrow-dfe-engine etc.) declare their own sibling dependencies as { workspace = true }. A plain { git = "...", package = "otel-arrow-dfe-engine" } dependency still resolves correctly because Cargo clones the whole repo and treats that clone's workspace root as the resolution context for the named package's own deps — this crate doesn't need to be a member of that workspace itself. Verified directly with a scratch crate before committing to this design.

Output

$ cargo build --features otap-engine && cargo test --features otap-engine
...
test result: ok. 113 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out   (unit tests, incl. otap::codec + otap::processor)
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out    (tests/api_surface.rs)
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out     (tests/otap_codec.rs)
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out     (tests/otap_lifecycle.rs)
test result: ok. 32 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out    (tests/runtime.rs)

$ cargo clippy --features otap-engine --all-targets -- -D warnings   # clean
$ cargo fmt --check                                                   # clean

169 tests total at the otap-engine level. Also re-verified otap (100+11+4+9+32 = clean, minus the 13 otap-engine-only tests) and default (no-feature) builds are unaffected — all via this repo's own cargo build/cargo test, nothing staged externally.

Not done here

  • No test exercises AsapSketchesProcessor inside a real running pipeline end to end (no Message::PData sent through process() against a live df_engine).
  • The receiver role — ingesting another asap_sketches node's legacy SketchStreamBatch output — isn't addressed by this adapter; that's a different, ASAP-native hop with its own standalone example binaries.
  • Cross-host byte-parity (Phase E) is unstarted.

🤖 Generated with Claude Code

zzylol added 2 commits August 24, 2026 12:04
…eal OTAP workspace

Per request: actually get an OTAP Dataflow checkout and compile/test
against it, rather than leaving otap_bridge.rs's binding as "read from
source, never compiled." Staged both files into a real clone of
open-telemetry/otel-arrow @ 3e85c3460361446ebfce99e9f35fffd2dd5ab740
(2026-08-24) as a `crates/asap-sketches-registry` workspace member
(path-depping back to asap-precompute-rs exactly as Cargo.toml's own
doc describes) and ran the real cargo build/clippy/fmt/test there.

## Bugs the real compiler caught (none of these were guessable from
## source reading alone)

- `ProcessorFactory::create`'s function-pointer type gained a fifth
  parameter, `capabilities: &capability::registry::Capabilities`
  ("per-node, one-shot view of extension capabilities... factories
  that don't depend on any extension can ignore the parameter") —
  not present in whatever version this adapter was originally read
  against. Added as an unused parameter to
  create_asap_sketches_processor.
- Two borrow-checker errors in the OtapPdata decode path: `if let
  Some(gauge) = data.as_gauge() { gauge.data_points().collect() }`
  doesn't compile because the collected Vec's items borrow from
  `gauge`, which is dropped at the end of the `if let` arm. Fixed by
  factoring the per-data-point body into a `DecodeAccumulator` struct
  with a generic `push_data_point<D: NumberDataPointView>` method,
  called inline from each of the Gauge/Sum branches instead of trying
  to unify their different concrete NumberDataPointView types into
  one Vec first.
- One unused import (`TryFromWithOptions`) clippy caught immediately.

## What's now genuinely verified (not just "compiles")

Added 3 new tests to otap_bridge.rs exercising the real
OtapArrowRecords::Metrics / OtapPdata / encode_metrics_otap_batch /
OtapMetricsView machinery end to end:
- encode_then_decode_round_trips_a_scalar_metric
- encode_then_decode_round_trips_a_sketch_envelope_carried_as_a_metric_attribute
  (the "self-describing sketch binary inside an OTAP metric" case --
  an `_asap_envelope` Bytes attribute survives the round trip
  byte-for-byte)
- decode_returns_none_records_for_zero_rows

All 10 tests in the staged crate pass (7 pre-existing config-shape
tests + 3 new), `cargo clippy -D warnings` and `cargo fmt --check`
both clean, against the real workspace.

## Not changed

Applied `cargo fmt`'s reformatting (this workspace's rustfmt config
differs slightly from what the file was originally written against --
import grouping mainly) back onto the canonical otap-patch/ copies.
Updated mod.rs's, otap_bridge.rs's, and README.md's doc comments from
"unverified" to reflect what's actually confirmed now, and precisely
what "verified" means here: staged into a *separate*, temporary
checkout of the real workspace, not something this repo's own build
wires up -- otap-patch/ itself still has no standalone build in this
repo.

Added .gitignore entry for /otel-arrow/ (the temporary checkout used
for this verification, not committed).
…ncoding

Per request: merge the two paths into one, with the wire lane
genuinely supporting the same dictionary/reuse economics. New
otap_wire.rs carries a real OtapArrowRecords::Metrics (built via
otap_bridge::otap_metric_records_to_pdata -- the exact same encoding
the generic-pipeline path already uses) directly over a persistent TCP
connection, instead of ASAP's own SCHEMA/DICTIONARY/RECORD
SketchStreamBatch protocol (asap_precompute_rs::otap::{wire,dictionary}).
One encoding, used identically whether a producer/receiver pair is
directly connected or routed through other OTAP pipeline components.

## What changed

- otap-patch/all/otap_wire.rs (new): OtapWireWriter/OtapWireReader,
  reusing the persistent-per-connection Arrow IPC design
  asap_precompute_rs::otap::wire::{WireWriter,WireReader} already
  validates (each payload type's Schema message sent once per
  connection, not once per window) -- generalized from
  SketchStreamBatch's fixed 4 roles to however many ArrowPayloadTypes
  a given OtapArrowRecords::Metrics actually populates (a real
  OtapPdata can carry up to 19 different payload types; ASAP's encode
  path only ever populates a handful, so roles are tracked in a
  BTreeMap keyed by ArrowPayloadType rather than 4 named fields).
- Duplicated (not shared) with asap-precompute-rs's design on purpose:
  asap-precompute-rs deliberately has no dependency on the OTAP
  Dataflow crates this needs, and pulling one in would break that
  crate's "builds standalone" property -- see the module's own doc.
- otap-patch/all/Cargo.toml: added arrow-ipc, tokio deps (both already
  pinned at the OTAP workspace level, so no new version to reconcile).

## Verification

Same workflow as the previous commit on this PR: staged into a real
open-telemetry/otel-arrow checkout (3e85c3460361446ebfce99e9f35fffd2dd5ab740,
2026-08-24) as the asap-sketches-registry workspace member. Two new
tests genuinely round-trip a real OtapPdata over an actual TCP
loopback socket:
- round_trips_a_real_otap_pdata_over_a_tcp_loopback_socket
- round_trips_multiple_windows_over_one_persistent_connection (two
  different metric values sent over one persistent connection, both
  decoded correctly on the far end -- the scenario that actually
  proves "one path" rather than just "compiles")

All 12 tests in the staged crate pass (10 from the previous commit +
these 2), cargo clippy -D warnings and cargo fmt --check both clean.

## Not done here

otap_wire.rs has no consumer inside this crate yet -- it's a complete,
tested transport module, not baked into AsapSketchesProcessor's
runtime behavior (documented honestly via #![allow(dead_code)] with
an explanation, not silently hidden). Wiring a config-driven choice of
transport into AsapSketchesProcessor itself (a peer_addr option,
connection lifecycle, reconnect behavior) is real follow-up work --
see the module's own "Not wired into AsapSketchesProcessor yet"
doc section for the two shapes that follow-up could take.
@zzylol zzylol changed the title otap: build/lint/test-verify OtapPdata binding against a real OTAP workspace otap: unify wire lane + metric lane onto one real OtapPdata encoding, verified against a real OTAP workspace Aug 24, 2026
…receiver nodes

Both otap_wire.rs's transport (OtapWireWriter/OtapWireReader) and the
wire lane's receive side had no consumer inside the crate — this closes
that: real OTAP nodes now actually drive them at runtime, not just tests.

- New otap_receiver.rs: AsapSketchesReceiver, a real
  local::Receiver<OtapPdata> (not local::Processor — that trait is purely
  reactive and can't independently accept a TCP connection). Listens on a
  configured address, decodes each connection with OtapWireReader, pushes
  into the pipeline via effect_handler.send_message. Registered under
  urn:asap:receiver:asap_sketches_wire via OTAP_RECEIVER_FACTORIES.
- AsapSketchesProcessor gains an optional peer_addr config field:
  emit_envelopes now forwards over a lazily-connected, persistent
  OtapWireWriter connection to that peer when set, falling back to the
  existing generic pipeline hop otherwise. A send failure drops the
  window and resets the connection so the next window reconnects.
- otap_wire.rs's #![allow(dead_code)] is gone — both OtapWireWriter and
  OtapWireReader now have real, non-test callers.
- New end-to-end test: the real AsapSketchesReceiver::start() bound to a
  real loopback TCP socket, fed by a real OtapWireWriter::send from a
  connected client, decoded and pushed through the actual pipeline
  machinery (OTAP's own TestRuntime harness) — not a mock.

Verified against the same staged open-telemetry/otel-arrow checkout
(3e85c3460361446ebfce99e9f35fffd2dd5ab740, 2026-08-24): build, clippy
-D warnings, fmt --check, and test all clean; 16/16 tests passing
(up from 12/12), including the new end-to-end receiver test.
asap-precompute-rs's own suite: 156/156, unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol zzylol changed the title otap: unify wire lane + metric lane onto one real OtapPdata encoding, verified against a real OTAP workspace otap: wire lane + metric lane unified onto one real OtapPdata encoding, now driving real processor/receiver nodes Aug 24, 2026
… already gives us dictionary economics

The direct-TCP wire lane added in the previous commit (otap_wire.rs +
otap_receiver.rs's AsapSketchesReceiver, peer_addr config) is gone.
There is now exactly one transport: effect_handler.send_message_with_source_node,
i.e. whatever pipeline this node is already wired into.

Why: the wire lane existed to give sketch traffic real dictionary/
schema-reuse economics (asap_precompute_rs::otap::dictionary's SCHEMA/
DICTIONARY/RECORD tiering, from PR #5/#6) that riding a generic OTLP
metric didn't have. But that's solving a problem OTAP's real Arrow
encoding already solves — otap_bridge's encode_metrics_otap_batch
dictionary-encodes the metric name and every string-valued attribute
key/value by construction. Verified against the real staged workspace:

  === payload_type UnivariateMetrics ===
    name : Dictionary(UInt8, Utf8)
  === payload_type NumberDpAttrs ===
    parent_id : Dictionary(UInt8, UInt32)
    key       : Dictionary(UInt8, Utf8)
    str       : Dictionary(UInt16, Utf8)

That's the same "send the dictionary once, reference it after that"
shape SeriesDictionary was reinventing at the application layer, done
instead at the columnar/Arrow-IPC level — with no dictionary state for
this adapter to track, no second wire protocol, and none of the
"must be one ordered, single-consumer stream or the series_id
reference dangles" correctness constraint a hand-rolled scheme has.

Added a permanent regression test guarding this fact
(otap_bridge::tests::real_otap_encoding_dictionary_encodes_metric_name_and_string_attributes)
so a silent upstream schema change surfaces loudly rather than being
rediscovered from scratch. asap_precompute_rs::otap::dictionary
(SeriesDictionary / SketchStreamBatch) stays in the tree, tested, and
still backs the legacy asap_precompute_rs::otap::wire example
binaries — it's just not part of this adapter's path.

Re-verified against the same staged open-telemetry/otel-arrow checkout
(3e85c3460361446ebfce99e9f35fffd2dd5ab740, 2026-08-24): build, clippy
-D warnings, fmt --check, and test all clean; 11/11 tests passing.
asap-precompute-rs's own suite: 156/156, unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol zzylol changed the title otap: wire lane + metric lane unified onto one real OtapPdata encoding, now driving real processor/receiver nodes otap: real OtapPdata binding, verified against a real OTAP workspace Aug 24, 2026
…e, real OtapPdata everywhere

Full merge, per explicit direction: no more otap-patch/ overlay
requiring manual staging into a checked-out OTAP Dataflow workspace to
compile. asap-precompute-rs is now the ONE crate, and depends directly
on the real otel-arrow-dfe-* crates (plain git dependency, pinned to
3e85c3460361446ebfce99e9f35fffd2dd5ab740, same pattern already used
for asap_sketchlib) behind a new `otap-engine` Cargo feature.
`cargo build --features otap-engine` just works — no staging script,
no temporary checkout, no copying files by hand.

New modules (moved + rewritten from otap-patch/all/{mod.rs,otap_bridge.rs}):

- otap::processor — AsapSketchesProcessor, the real
  local::Processor<OtapPdata> node, linkme-registered under
  urn:asap:processor:asap_sketches.
- otap::codec (renamed from the working name `bridge` — it's not
  bridging two representations anymore, see below) — the real
  SketchEnvelope <-> OtapPdata binding.

The bigger change is what codec.rs does differently from the old
otap_bridge.rs it replaces: it builds/reads a real OtapPdata *directly*
from/to &[SketchEnvelope], skipping the intermediate flat RecordBatch
(encode_batch) and OtapMetricRecords two-batch family (lift) hops
entirely. Those three representations for one job only existed because
of speculative multi-adapter generality (Telegraf/Vector) that never
materialized in this repo, which only ever ships to OTAP -- so codec.rs
implements OTAP's own MetricsView trait family straight over envelope
slices instead. encode_batch/decode_batch/records::{flatten,lift}
still exist, still tested, and still back the legacy
SeriesDictionary/otap::wire transport and its standalone example
binaries -- they're just not part of the otap-engine path anymore.

A real bug found and fixed along the way: encode_batch unconditionally
sets `_asap_envelope` even for estimate-mode envelopes (empty
payload), and arrow_array::BinaryArray treats `Some(&[])` as present-
not-null -- confirmed empirically -- so an estimate-mode gauge would
misroute through the envelope decode path with an empty payload
instead of the scalar path. codec.rs's direct encoder only attaches
_asap_envelope (and its sibling attributes) when the payload is
actually non-empty, with a defensive filter on decode too. New test:
estimate_mode_envelope_round_trips_as_a_scalar_not_an_empty_envelope.
The legacy encode_batch/decode_batch pair keeps the original behavior
untouched -- fixing it there was out of scope for this rewrite.

Also moved plugins/asap_sketches/{README.md,sample.toml} to
asap-precompute-rs/plugins/asap_sketches/ (content updated to match:
no more Phase D/"deliberately deferred" language, since it's done),
dropped the empty src/mod.rs placeholder it predated, and rewrote the
crate-root and repo-root README/module docs throughout to describe the
one-crate reality instead of the old Layer A/otap-patch split.

Verified via this repo's own `cargo build/test/clippy/fmt` at all
three feature levels (default, otap, otap-engine) -- no staging into
any external checkout: 100/113/169 tests pass respectively (default/
otap/otap-engine), clippy -D warnings clean, fmt clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol zzylol changed the title otap: real OtapPdata binding, verified against a real OTAP workspace otap: merge into one crate, real OtapPdata direct from SketchEnvelope, no staging required Aug 24, 2026
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