Skip to content

controller: emit 5-sketch routing-connector pipeline (DDSketch/KLL/HLL/CountSketch/CountMinSketch) - #340

Merged
zzylol merged 1 commit into
mainfrom
mvp/stage-config-emit-5-sketch-routing
May 8, 2026
Merged

zzylol merged 1 commit into
mainfrom
mvp/stage-config-emit-5-sketch-routing

Conversation

@zzylol

@zzylol zzylol commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Rewrites the typed L5 edge YAML emitter (controller/src/config/stage_config.rs::emit_edge_yaml) so the agent loads all 5 sketch processors and the OTel routing connector (NOT the deprecated routing processor) dispatches per metric to the right sketch family — the canonical wire shape for issue #46.

Bugfix call-out: routing is a CONNECTOR, not a processor

OTel collector v0.106+ removed routingprocessor and re-shipped the routing component as a connector under a top-level connectors: block. The legacy emit path placed routing under processors:, which fails confmap validation at agent boot:

error decoding 'processors': unknown type: "routing"

The asap-otel binary's builder-config.yaml registers routingconnector. This PR emits the connector-form layout that build expects.

Canonical YAML shape

receivers:
  otlp: ...
processors:
  ddsketchprocessor: ...
  kllprocessor: ...
  hllprocessor: ...
  countsketchprocessor: ...
  countminsketchprocessor: ...
  gorillas3: ...     # when archive_tier_metrics non-empty
  batch: ...
connectors:
  routing:
    default_pipelines: [metrics/raw_passthrough]
    table:
      - statement: 'route() where metric.name == "http_latency_ms"'
        pipelines: [metrics/ddsketch_path]
      - statement: 'route() where metric.name == "request_size_bytes"'
        pipelines: [metrics/kll_path]
      - statement: 'route() where metric.name == "unique_users_per_min"'
        pipelines: [metrics/hll_path]
      - statement: 'route() where metric.name == "top_endpoint_qps"'
        pipelines: [metrics/countsketch_path]
      - statement: 'route() where metric.name == "endpoint_request_freq"'
        pipelines: [metrics/countminsketch_path]
exporters:
  otlp/backend: ...
service:
  pipelines:
    metrics:                              # entry
      receivers: [otlp]
      exporters: [routing]
    metrics/raw_passthrough:              # default
      receivers: [routing]
      processors: [gorillas3?, batch]
      exporters: [otlp/backend]
    metrics/ddsketch_path:
      receivers: [routing]
      processors: [gorillas3?, ddsketchprocessor, batch]
      exporters: [otlp/backend]
    metrics/kll_path: ...                 # same shape
    metrics/hll_path: ...
    metrics/countsketch_path: ...
    metrics/countminsketch_path: ...

The 6 named pipelines

  1. metrics — entry (receiver otlp, exporter routing)
  2. metrics/raw_passthrough — default fall-through (e.g. http_requests_total, freshness probes)
  3. metrics/ddsketch_path
  4. metrics/kll_path
  5. metrics/hll_path
  6. metrics/countsketch_path
  7. metrics/countminsketch_path

(Plus metrics/prometheus_archive when Mode-3 metrics are also configured.)

EdgeStageConfig field

Adds metric_to_family: HashMap<String, SketchKind> to EdgeStageConfig (agreed convention with the parallel planner agent). Empty map ⇒ legacy single-pipeline / Mode-3 / warm-passthrough emit shapes preserved verbatim.

gorillas3 invariant

gorillas3 runs FIRST in every per-sketch pipeline so the cold-tier write happens BEFORE the family processor mutates / suffix-renames the stream — same invariant the legacy emit path enforces, pinned by mvp46_per_sketch_pipelines_have_gorillas3_first_when_archive_declared.

Freshness-probe routing (PR #333)

warm_passthrough_metrics route to metrics/raw_passthrough so the metric name is preserved end-to-end (no DDSketch _quantile suffix). Pinned by mvp46_warm_passthrough_routes_to_raw_passthrough_pipeline.

Test plan

  • All 99 config::* tests pass
  • 11 new MVP §46 tests pin the new wire shape:
    • mvp46_emit_loads_all_5_sketch_processors
    • mvp46_routing_lives_in_connectors_not_processors (the bugfix)
    • mvp46_emits_all_6_named_pipelines
    • mvp46_entry_pipeline_routes_to_connector_not_processor
    • mvp46_per_sketch_pipelines_use_routing_as_receiver
    • mvp46_per_sketch_pipelines_have_gorillas3_first_when_archive_declared
    • mvp46_routing_table_dispatches_per_metric_to_correct_family
    • mvp46_default_pipeline_is_raw_passthrough
    • mvp46_warm_passthrough_routes_to_raw_passthrough_pipeline
    • mvp46_empty_metric_to_family_falls_back_to_legacy_emit
    • mvp46_composes_with_prometheus_archive_mode3
  • Pre-existing test failures on main (analyzer / intent_algebra / planner cost_model / opamp / api_tests — 10 total) are untouched

Refs: #46

🤖 Generated with Claude Code

…L/CountSketch/CountMinSketch)

Rewrites the typed L5 edge YAML emitter so the agent loads all 5
sketch processors and the OTel `routing` *connector* (NOT the
deprecated routing processor) dispatches per metric to the right
sketch family — the canonical wire shape for MVP issue #46.

## Bugfix call-out: routing is a CONNECTOR, not a processor

OTel collector v0.106+ removed `routingprocessor` and re-shipped
the routing component as a connector under the top-level
`connectors:` block. The legacy emit path placed `routing` under
`processors:`, which fails confmap validation at agent boot:
  `error decoding 'processors': unknown type: "routing"`.

This PR emits the connector-form layout the asap-otel binary's
builder-config registers:

  receivers:  { otlp }
  processors: { gorillas3?, batch,
                ddsketchprocessor, kllprocessor, hllprocessor,
                countsketchprocessor, countminsketchprocessor }
  connectors: { routing: { default_pipelines: [...],
                           table: [...route() statements...] } }
  exporters:  { otlp/backend, otlphttp/prometheus? }

  service.pipelines:
    metrics:                            (entry — receivers: [otlp],
                                          exporters: [routing])
    metrics/raw_passthrough             (default — receivers: [routing],
                                          processors: [gorillas3?, batch],
                                          exporters: [otlp/backend])
    metrics/ddsketch_path
    metrics/kll_path
    metrics/hll_path
    metrics/countsketch_path            (per-family — receivers: [routing],
    metrics/countminsketch_path           processors: [gorillas3?,
                                                       <family>processor,
                                                       batch],
                                          exporters: [otlp/backend])

`gorillas3` runs FIRST in every per-sketch pipeline (when an
archive tier is declared) so the cold-tier write happens BEFORE
the family processor mutates / suffix-renames the stream — same
invariant the legacy emit path enforces.

## EdgeStageConfig field

Adds `metric_to_family: HashMap<String, SketchKind>` to
`EdgeStageConfig` (agreed convention with the parallel planner
agent). Empty map ⇒ legacy single-pipeline / Mode-3 /
warm-passthrough emit shapes are preserved verbatim
(backward-compat).

When non-empty, the new `emit_edge_yaml_5sketch_routing` helper
emits the canonical 6-pipeline layout. Metrics in the map dispatch
per-family; metrics absent fall through to `metrics/raw_passthrough`
(this is the home for `http_requests_total` and the freshness
probes from PR #333's warm-passthrough routing).

## Field plumbing

Updates every `EdgeStageConfig` constructor in `controller/src/`
to initialise `metric_to_family: HashMap::new()` so existing
callers keep producing the legacy shape.

Adds `connectors: HashMap<String, Value>` to the structural
`CollectorYaml` type with `skip_serializing_if = "is_empty"` so
the legacy single-pipeline shape doesn't gain an empty
`connectors: {}` block.

## Test coverage

11 new tests pin the new wire shape:
  * mvp46_emit_loads_all_5_sketch_processors
  * mvp46_routing_lives_in_connectors_not_processors  (the bugfix)
  * mvp46_emits_all_6_named_pipelines
  * mvp46_entry_pipeline_routes_to_connector_not_processor
  * mvp46_per_sketch_pipelines_use_routing_as_receiver
  * mvp46_per_sketch_pipelines_have_gorillas3_first_when_archive_declared
  * mvp46_routing_table_dispatches_per_metric_to_correct_family
  * mvp46_default_pipeline_is_raw_passthrough
  * mvp46_warm_passthrough_routes_to_raw_passthrough_pipeline
  * mvp46_empty_metric_to_family_falls_back_to_legacy_emit
  * mvp46_composes_with_prometheus_archive_mode3

All 99 `config::*` tests pass; the 10 pre-existing failures on
`main` (analyzer, intent_algebra, planner cost_model, opamp,
api_tests) are untouched by this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 8bcc6f4 into main May 8, 2026
zzylol added a commit that referenced this pull request May 8, 2026
Activates the typed-stage-split path for every compose invocation that
includes base.yml, so the demo (and any ad-hoc `docker compose up
controller` against base alone) routes through the typed pipeline:
bootstrap GET (#329), OpAMP-on-connect replan (#334), and per-metric
5-sketch family routing (#339 + #340 + the in-flight stitching PR).

The mvp-multi-stage.yml overlay already sets the same default; this
just promotes the gate to the base layer so it is on without the
overlay.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 8, 2026
…_family map (#46) (#342)

PR #339 (planner: per-metric SketchExpr) and PR #340 (emitter:
metric_to_family-driven 5-sketch routing) landed in parallel, but
nothing populated `EdgeStageConfig.metric_to_family`, so the new
routing-connector wire path stayed dormant — every bootstrap and
OpAMP-push emitted single-pipeline DDSketch even with the six
contract metrics declared in `workloads.yaml`.

This stitches them: a new `config::collect_metric_to_family` walks
the workload registry, runs `bind_workload_typed` per metric, and
collects committed sketch families into the HashMap. Metrics that
decline binding (`http_requests_total` raw passthrough,
exact-required, multi-intent) are skipped — the emitter routes
those to `metrics/raw_passthrough` by default.

Wired into both:
  * `main::emit_bootstrap_typed` — also generalised the candidate
    resolution so a registry whose first agent-role entry is raw
    (e.g. `http_requests_total`) walks to the next binding metric
    for the edge_cfg shape rather than falling back to legacy.
  * `replan::Replanner::try_emit_typed_edge_yaml_for_workload` —
    same stitch so OpAMP-pushed YAML on reconnect / replan also
    activates the routing connector.

Acceptance test (`api_tests::bootstrap_emits_5sketch_routing_for_six_contract_metrics`):
loads all six MVP §46 contract metrics into the registry, hits the
bootstrap GET endpoint with `USE_TYPED_STAGE_SPLIT=1`, and asserts
the emitted YAML contains all 5 sketch processors, `routing` under
`connectors:` (not `processors:`), all 6 named pipelines, and a
`route() where metric.name == "..."` rule for each sketched metric.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol deleted the mvp/stage-config-emit-5-sketch-routing branch May 9, 2026 18:00
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