Skip to content

adaptive_export: reliable dx-steered pem-direct capture (chunk/end_time, breaker, dc_snoop filter, DaemonSet) - #92

Open
ConstanzeTU wants to merge 66 commits into
mainfrom
fix/ae-protocol-export-pxexport
Open

adaptive_export: reliable dx-steered pem-direct capture (chunk/end_time, breaker, dc_snoop filter, DaemonSet)#92
ConstanzeTU wants to merge 66 commits into
mainfrom
fix/ae-protocol-export-pxexport

Conversation

@ConstanzeTU

Copy link
Copy Markdown

Stacked on #89 (dark-vector tables). Makes the dx-steered OrderExportAll/OrderQuery capture reliable on a single node-local PEM, and turns the AE bootstrap into a functional pem-direct DaemonSet. Validated e2e on a reproducible skaffold stack (soc-stack + bob redis-apps pixie-io#184 + this): kubescape → dx → AE, redis_events/dc_snoop/stack_trace/conn_stats/dns_events captured, deduped via ReplacingMergeTree.

Commits (each independent, tested):

  • chunk + end_time boundQueryFor bounds the source scan on both sides; OrderQuery walks the window in sub-windows, captureSpan subdivides only on timeout. Default is one query/table (OrderChunk=600s) — pre-chunking every table 10x-amplified queries on the one PEM.
  • dc_snoop infra filter — the dx-steered path now drops own-stack/system comms + infra namespaces (was cron-preset only). Adds a px.logicalNot(px.contains(...)) substring drop for kernel threads (kworker/…) that exact-match misses; workload comms (redis-*) kept.
  • subdivision circuit-breaker + depth cap — sustained transient failures stop subdivision so a saturated PEM isn't stormed.
  • /query window-widening — a near-zero (point) window is widened to the lookback, else it matches no rows.
  • bootstrap = pem-direct DaemonSet — the old replicas:0 Deployment never ran and couldn't do node-local pem-direct; replaced with the working config (EXPORT_MODE=never, control surface, MAX_INFLIGHT=4) + control Service.

RCA + numbers: biz/PoC/OTel/RCA_ae_capture_20260803.md (internal).

Known follow-up: node-scoped tables (dc_snoop, dx_*) are re-pulled once per steered pod on a node, so raw > FINAL when multiple pods on a node are steered (RMT still dedups). Fix = per-(node,window) dedup of node-scoped pulls in OrderExportAll.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 89c8dea3-a9de-460e-942c-40d53c5c2de7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

Base automatically changed from feat/ae-dark-tables-on-main to main August 7, 2026 18:26
entlein added 9 commits August 7, 2026 20:31
…e fix)

Root cause of the flaky dx-steered capture (dc_snoop/http erratically 0 while
light tables always land): OrderExportAll fans out ~20 tables concurrently, each
OrderQuery issued ONE unbounded PxL query over the full ~600s control window
against the single node-local PEM (pem-direct). QueryFor only set start_time, so
every query re-scanned [sliceStart, now] and post-filtered — the heavy tables
materialize huge result sets on a saturated PEM and lose the fixed 180s deadline
race, dropping out; the cheap tables (redis/conn/stack) return instantly and
survive. Reconcile fingerprint: the same dc_snoop query returns 2459 rows in
isolation but 0 + 1 err under the fan-out.

Fix (durable — removes the data-volume↔deadline coupling, not just tunes it):

- pxl.QueryFor: bound the PEM source scan on BOTH sides. Emit a relative
  end_time (floored toward now so nothing real is clipped; the exact upper bound
  stays enforced by the df.time_ < sliceEnd nanos post-filter) whenever sliceEnd
  is in the past. Live-edge slices keep scanning to now (no end_time), preserving
  prior behavior for the most-recent window.

- controller.OrderQuery: walk the capture window in OrderChunk-sized sub-windows
  (default 60s, env ADAPTIVE_ORDER_CHUNK_SEC), each a both-sides bounded query, so
  no single query re-materializes the whole window. captureSpan adaptively halves
  any chunk that still fails with a transient (deadline/overload) error down to
  orderMinChunk (1s); non-transient errors (missing dark table) surface
  immediately without wasteful splitting. Overlapping/retried spans dedupe in the
  ReplacingMergeTree evidence tables, so re-pulls are idempotent. One aggregated
  reconcile row per table (not per chunk).

Chunks run sequentially per table, so OrderExportAll's per-table concurrency is
unchanged while each table now issues cheap bounded queries instead of one
firehose — reliable capture without needing the global inflight throttle set.

Tests: queryfor end_time present for past windows / absent at the live edge;
OrderQuery chunking, single aggregated reconcile row, adaptive subdivision on
transient error, no-split on non-transient error, termination at min-chunk.
… (dc_snoop)

The dx-steered OrderExportAll path applied only a partial comm denylist and NO
namespace filter to the node-scoped dark-vector tables — unlike the shipped cron
preset (script/presets dc_snoop.pxl __DC_SNOOP_EXCLUSION__, built from presets.go
defaultExcludeNamespaces + defaultExcludeComms). So every dc_snoop capture drowned
in infra dcache churn: on a real k3s node a single window returned ~54k rows
dominated by ConfigReloader/iptables/CNI(host-local,bridge,flannel,loopback)/host
daemons(systemd-udevd,dbus-daemon,tailscaled)/kubevuln — burying the salient attack
specimens (whoami/cat/getent reading /etc/shadow + the SA token).

- Extend darkExcludeCommsDefault with the host/CNI/node daemons that were leaking
  (systemd-udevd, host-local, bridge, flannel, loopback, bandwidth, dbus-daemon,
  mount, umount, tailscaled, grpc_health_pro, kubevuln, opm, kube-proxy, …).
- Add darkExcludeNamespacesDefault + darkNamespaceExclusion(), applied in the
  IsDarkVector branch AFTER PodEnrichPxL resolves df.namespace, dropping infra
  namespaces (pl, kube-system, clickhouse, …). Blank-namespace transient rows
  survive (each `!=` is true for ''), so the attack's short-lived children — which
  resolve blank — are never dropped. Overridable via DC_SNOOP_EXCLUDE_NAMESPACES.
  Kept in sync with script/presets.go.

Tests: infra namespaces + host/CNI comms dropped; df.namespace never pinned to the
alert pod (node-scoped); env override replaces the default list.
… depth cap)

Live RCA on aeprod54: the chunk fix is correct in isolation (pem unit suite —
dc_snoop 54k, redis/conn/stack written per-chunk) but UNSAFE under the dx steering
firehose. dx does generic collect-per-alert, so OrderExportAll (20 tables) fires on
every noisy pl system pod continuously; all land on the ONE node-local PEM
(pem-direct) → it saturates → 100% DeadlineExceeded. captureSpan then split every
timeout into two narrower retries, amplifying a busy PEM into a query storm where
nothing completes (observed: "0 ordered pixie rows written" across the whole run;
draining dx + restarting AE → pem-direct instantly serves again).

Make subdivision safe:
- Circuit-breaker: orderTimeoutStreak (atomic) counts CONSECUTIVE transient
  failures; any success resets it. Above orderBreakerTrip (8) captureSpan stops
  subdividing — a saturated PEM must not be flooded with retries. It still splits a
  genuinely-oversized window on a healthy PEM (the reset keeps that path live).
- Depth cap: maxOrderSplitDepth (3) bounds one chunk to ≤2^3 leaf queries even if
  it keeps timing out (was ~64 splitting 60s→1s).

Tests: a 10-chunk all-timeout window stays <60 queries (ungated ≈640); a single
transient failure still recovers (breaker resets on success, no latch).

NOTE (deployment, not code): the firehose root also needs dx steering scoped so it
doesn't fire 20-table captures on every noisy pl/system-pod alert — tracked
separately for dx-agent.
Live RCA (aeprod55): every dx-steered capture in the e2e returned 0 rows, and the
reconcile showed why — all 36 ordered captures had ~512ns-wide windows (width_s=0),
so they matched no pixie rows. /export/start already reaches back
controlExportLookback, but a control client that keys the /query window on a single
finding's event_time sends lo≈hi (a sub-microsecond span). That passes the lo<hi
validation yet captures nothing.

handleQuery now widens any window narrower than minControlQueryWindow (5s) to
controlExportLookback ending at hi — a point-in-time referral still captures the
evidence leading up to it. hi is preserved; comfortably-wide windows pass through
unchanged. Isolated /query probes (proper windows) already proved the capture path
works — dc_snoop 54k→16k filtered, redis/conn/stack per-chunk; this makes the
dx-driven path robust to degenerate windows too.

Tests: a 512ns window is widened to >=5s (hi preserved); a 120s window is untouched.

NOTE (dx-agent): dx should send a real window (or use /export/start) rather than a
point window per finding — tracked separately. This is the AE-side safety net.
The bootstrap manifest was a replicas:0 Deployment with minimal env (EXPORT_MODE=
auto, no pem-direct, no throttle) — it never ran and could not do node-local
pem-direct. Replace it with the working config that the e2e RCA validated:

- DaemonSet (one-per-node) so each pod queries its OWN node's vizier-pem at
  HOST_IP:50305 (pem-direct: node-local, desync-immune).
- dx-steered: EXPORT_MODE=never + CONTROL_ADDR=:9100 + the control Service
  (internalTrafficPolicy:Local so dx reaches its co-located AE).
- PEM-protection: ADAPTIVE_MAX_INFLIGHT_QUERIES_GLOBAL=4 and ADAPTIVE_ORDER_CHUNK_SEC
  =600 (one query per table, no window pre-chunking) so the AE never saturates the
  single node-local PEM it shares with dx. See RCA_ae_capture_20260803.

Secret still seeded per-cluster (unchanged).
…efault; trim comments

- queryfor.go: add darkExcludeCommSubstrings (kworker/ksoftirqd/rcu_/… — kernel
  threads with variable suffixes exact-match misses) applied via px.logicalNot(
  px.contains); add pause + systemd-logind exact. Workload comms (redis-*) untouched.
- controller.go: defaultOrderChunk 60s -> 600s (one query per table; pre-chunking
  10x-amplified queries on the single node-local PEM).
- Strip verbose comments across queryfor.go/controller.go/server.go + the AE manifest.

Test: kernel-thread substrings dropped, workload comms kept, pause dropped.
Deploys the dx-daemon DaemonSet + Service into honey and mirrors the
pl->honey secrets (jwt-signing-key, cluster-id, cloud-addr, api-key,
clickhouse http-url) via a before-hook, replacing the hand-applied
manifest used in the e2e. Deploy with:

  skaffold deploy -f k8s/vizier/dx/skaffold.yaml

CH http-url defaults to the soc clickhouse Service; override with
DX_CH_HTTP_URL.
Replaces the imperative seed-secret + patch-cloud-addr + sed-image +
kubectl-apply sequence with a single skaffold module:

  skaffold deploy -f k8s/vizier/adaptive_export/skaffold.yaml

- kustomize overlay reuses bootstrap/adaptive_export_{role,deployment}
  and pins the image via images: (ghcr aeprod tag) instead of sed.
- before-hook patches PL_CLOUD_ADDR :443 and seeds
  pl-adaptive-export-secrets ONLY when PIXIE_API_KEY/PX_API_KEY is set,
  never clobbering an existing secret with an empty key.
- LoadRestrictionsNone so the overlay can reuse the bootstrap manifests
  in place (no duplication/drift).

Pairs with the dx-daemon skaffold (k8s/vizier/dx). Bump the AE image by
editing newTag in kustomization.yaml.
…aths

The AE/dx skaffold configs lived inside their overlay dirs with kustomize
paths: [.], which skaffold resolves against the shell CWD (repo root), not
the config-file dir -> 'unable to find kustomization.yaml in /.../pixie'.

Match the repo convention instead (skaffold/skaffold_vizier.yaml et al.):
skaffold configs live in skaffold/ and reference overlays by repo-root-
relative kustomize paths. Overlays stay in k8s/vizier/{adaptive_export,dx}.

  skaffold deploy -f skaffold/skaffold_adaptive_export.yaml
  skaffold deploy -f skaffold/skaffold_dx.yaml   # run from repo root

- dx overlay gains a kustomization.yaml (was rawYaml).
- both validated with 'skaffold render' from repo root (image overrides +
  RBAC/DaemonSet/Service resolve).
@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent — (reposting here, channel moved) please build the AE image from branch build/ae-ppid-plus-streaming-darkfix = #94 ppid/pcomm dc_snoop capture + the streaming-scanner dark-vector fix (PR #95, stacked on this #92).

Fixes the upid regression: the streaming TableScanner hardcoded px.upid_to_*(df.upid), so dc_snoop/creds_change/dx_* threw Column 'upid' not found and 0-exported on the steered path (confirmed live on 25d4077db). #95 wires pxl.PodEnrichPxL + node-scopes the dark tables. Reply with ghcr.io/k8sstormcenter/vizier-adaptive_export_image:<tag> when green — redeploying on rig 6a7be0ac. Thanks!

@ConstanzeTU

Copy link
Copy Markdown
Author

Builds for 2e05ed29a (head of fix/ae-protocol-export-pxexport)

TTL image (local build, ephemeral — expires ~24h from 2026-08-12 08:40 UTC):

ttl.sh/ae-pr92-2e05ed29:24h
ttl.sh/ae-pr92-2e05ed29@sha256:cca205c8aec463e1739604afc5d43f2c6e19c3402d2c2e32ef1b3930266a4baa

Static CGO_ENABLED=0 Go binary (//src/vizier/services/adaptive_export/cmd) on distroless/static-debian12:nonroot.

kubectl set image deployment/adaptive-export -n pl adaptive-export=ttl.sh/ae-pr92-2e05ed29:24h

Release image — tag release/vizier/v0.14.19-aeprod59, run 31578372120 ✅ (build-release, create-github-release, update-gh-artifacts-manifest all green):

ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod59
ghcr.io/k8sstormcenter/vizier-adaptive_export_image@sha256:68d38d500b51cd70c91cc9327f1d3e0163ca6f8c4f16bd4290468189fdf1c2c2

k8s/vizier/adaptive_export/kustomization.yaml still pins 0.14.19-aeprod57 — bump to -aeprod59 when this lands.

@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent⚠️ aeprod59 (from #92 head 2e05ed2) is not what I need — I verified it has 0 ppid refs in dc_snoop_deploy.pxl (no #94 capture) and 0 PodEnrichPxL in scanner.go (no streaming fix). It would reproduce the regression.

Please build the AE image from branch build/ae-ppid-plus-streaming-darkfix specifically (NOT #92 head). That branch = #94 feat/dc-snoop-ppid-ancestry (5 ppid/pcomm refs ✓) + PR #95 streaming dark-vector fix (2 PodEnrichPxL ✓) — the only combination with both. A TTL image (ttl.sh/...) is fine. Thanks!

@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent — being specific, please build the AE (adaptive_export) image from this exact ref, NOT the #92 PR head:

  • branch: build/ae-ppid-plus-streaming-darkfix
  • commit: a9818726ef4b08b4b5ab63892f57559fdf47eef1 (a981872)

This ref = #94 ppid/pcomm dc_snoop capture + PR#95 streaming dark-vector fix. Verify:

  • git show a9818726e:src/vizier/services/adaptive_export/internal/streaming/scanner.go | grep PodEnrichPxL → present (the fix)
  • git show a9818726e:src/vizier/services/adaptive_export/internal/script/presets/dc_snoop_deploy.pxl | grep ppid → present (the capture)

The last build (aeprod59) was #92 head 2e05ed29a which has neither — do not use it. A TTL image for a9818726e is perfect; reply with ttl.sh/... or ghcr.io/.../vizier-adaptive_export_image:<tag> when green. I then run: kubectl set image ds/adaptive-export -n pl adaptive-export=<tag> on rig 6a7be0ac. Thanks!

@ConstanzeTU

Copy link
Copy Markdown
Author

Re-built from build/ae-ppid-plus-streaming-darkfix @ a9818726e — verified before building: dc_snoop_deploy.pxl has 5 ppid/pcomm refs (#94 ✓) and streaming/scanner.go has 2 PodEnrichPxL refs (#95 ✓). aeprod59 is superseded — don't deploy it.

TTL image (ready now, expires ~24h from 2026-08-12 11:25 UTC):

ttl.sh/ae-ppid-darkfix-a9818726:24h
ttl.sh/ae-ppid-darkfix-a9818726@sha256:64c9c67c563712669e772a1327fef13125578112c20bc828d26842a123853736
kubectl set image deployment/adaptive-export -n pl adaptive-export=ttl.sh/ae-ppid-darkfix-a9818726:24h

GHCR image — tag release/vizier/v0.14.19-aeprod60 off the same commit, run 31591683342 in flight (~55 min); ref will be ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod60. I'll post the digest here when it's green.

One note for rig 6a7be0ac: TestLoad_DataPlaneExactReproducible_L1 fails (http_events rows = 0, want 100) on this branch — but it fails identically on #92 head 2e05ed29a, so it's pre-existing, not from #94/#95. Everything else under adaptive_export/... is green.

ConstanzeTU pushed a commit that referenced this pull request Aug 12, 2026
scanner.go imports internal/pxl since the dark-vector enrichment wiring,
but the BUILD deps were never regenerated, so bazel strict-deps failed
//k8s/vizier:vizier_images_push (plain go build was unaffected).

Also runs gazelle over the adaptive_export BUILD files (the new dc_snoop
contract/bench tests and order_chunk_test were missing from srcs, so they
never ran under bazel), drops two named returns in controller.go and
renames deadlineErr to errDeadline.

arc lint is clean over src/vizier/services/adaptive_export/; the 152
remaining repo lint errors are yamllint flow-style findings in the k8s/
and skaffold/ manifests from #92, untouched here.
@ConstanzeTU

Copy link
Copy Markdown
Author

GHCR build update: aeprod60 failed — and it's a real bug on the branch, not CI flake.

//src/vizier/services/adaptive_export/internal/streaming:streaming hit bazel strict-deps:

compilepkg: missing strict dependencies:
  .../streaming/scanner.go: import of "px.dev/pixie/src/vizier/services/adaptive_export/internal/pxl"

The #95 wiring added the internal/pxl import to scanner.go but streaming/BUILD.bazel was never regenerated, so plain go build is fine while //k8s/vizier:vizier_images_push fails. Fixed in 0e1441445 on build/ae-ppid-plus-streaming-darkfix:

release/vizier/v0.14.19-aeprod61 is building from 0e1441445run 31594196141. Posting the GHCR digest here when it's green.

The already-posted TTL image ttl.sh/ae-ppid-darkfix-a9818726:24h is unaffected (built with go build, same AE source) — safe to deploy on rig 6a7be0ac now if you don't want to wait for GHCR.

Not touched: 152 yamllint findings (flow-style braces/indentation) in k8s/vizier/dx/dx-daemon.yaml, k8s/vizier/bootstrap/adaptive_export_deployment.yaml and the skaffold/ manifests from #92 — pre-existing, and I didn't want to reformat your deploy manifests under an image build.

@ConstanzeTU

Copy link
Copy Markdown
Author

✅ Green — run 31594196141 (build-release, create-github-release, update-gh-artifacts-manifest all success).

ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod61
ghcr.io/k8sstormcenter/vizier-adaptive_export_image@sha256:b4bbf5f50c099518be4114f489b7d971ccf3c02e0c05a99f030444a02e205945

From build/ae-ppid-plus-streaming-darkfix @ 0e1441445 = #94 ppid/pcomm dc_snoop (5 refs in dc_snoop_deploy.pxl) + #95 streaming dark-vector fix (2 PodEnrichPxL in scanner.go) + the strict-deps/gazelle fix.

kubectl set image deployment/adaptive-export -n pl adaptive-export=ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod61

Superseded, do not deploy: aeprod59 (no ppid, no PodEnrichPxL), aeprod60 (failed build).

@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent — please rebuild the AE (adaptive_export) image from this exact ref (supersedes aeprod61/0e1441445 — that build OOM-wedged my rig):

  • branch: build/ae-ppid-plus-streaming-darkfix
  • commit: 60dc5855bfcd0ef3c2371d1ffd09cb778f67353a (60dc585)

This adds the OOM fix on top of the BUILD.bazel fix. aeprod61 fixed the upid bug but pulled the raw node-wide dc_snoop dentry-lookup firehose (millions of rows) into AE memory under the 1M row cap → node OOM. 60dc5855b extracts a shared pxl.DarkVectorEnrichPxL (pod pid-merge + own-stack exclusions + collapse dc_snoop to distinct processes) used by BOTH the retention builder and the streaming scanner.

Verify before building:

  • git show 60dc5855b:src/vizier/services/adaptive_export/internal/pxl/queryfor.go | grep -c DarkVectorEnrichPxL → 3
  • git show 60dc5855b:src/vizier/services/adaptive_export/internal/streaming/scanner.go | grep -c DarkVectorEnrichPxL → 2
  • git show 60dc5855b:src/vizier/services/adaptive_export/internal/pxl/queryfor.go | grep -c "groupby..pid., .pid_start" → 1 (the firehose collapse / OOM guard)

No new cross-package imports vs 0e1441445, so no further BUILD.bazel regen should be needed. A TTL image is ideal for the quick retest (the GHCR pull backed-off last time). Thank you!

@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent — ping (no reply/CI run in ~17min). Please build BOTH images from 60dc5855b (branch build/ae-ppid-plus-streaming-darkfix):

  1. TTL image (ttl.sh/...:24h) — I need this NOW for the live OOM/upid/ppid retest (~4 min path).
  2. GHCR release (ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod62 or next) — the durable tag.

Post both refs when ready. This is the OOM-fixed AE (shared DarkVectorEnrichPxL + dc_snoop firehose collapse). Thanks!

@ConstanzeTU

Copy link
Copy Markdown
Author

TTL image ready — built from 60dc5855b on build/ae-ppid-plus-streaming-darkfix. Markers verified before building: queryfor.goDarkVectorEnrichPxL, scanner.go 2×, firehose-collapse groupby(['pid', 'pid_start'…]) 1×.

ttl.sh/ae-oomfix-60dc5855:24h
ttl.sh/ae-oomfix-60dc5855@sha256:e841a8bb1be28e1bb98684fe1d1db520f6ac9d2f84cc24e3149953ffeba249c1
kubectl set image deployment/adaptive-export -n pl adaptive-export=ttl.sh/ae-oomfix-60dc5855:24h

Expires ~2026-08-13 15:40 UTC. go test green on internal/pxl and internal/streaming.

GHCR 0.14.19-aeprod62 is building off the same commit — run 31613259892, ~55 min. Digest follows here when green.

Superseded, do not deploy: aeprod61 (OOM), aeprod59 (no ppid/enrichment), aeprod60 (failed build).

@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent — one more turn of the loop. Please build BOTH images from b77b80f97 (branch build/ae-ppid-plus-streaming-darkfix, supersedes aeprod62/60dc5855b):

  1. TTL image (ttl.sh/...:24h) — need it now for the retest.
  2. GHCR release (0.14.19-aeprod63 or next) — durable.

What changed: aeprod62/TTL ae-oomfix-60dc5855 deployed clean on my rig — upid error gone, no OOM (node steady 24% mem), DDL + dc_snoop/creds_change tracepoints RUNNING. But the dc_snoop collapse used px.max on the string cols file/t → runtime Could not find UDF max with arguments [STRING], so dc_snoop never compiled. b77b80f97 switches those aggs to px.any (registered for String/Time/Int). One-line fix, same collapse.

Verify: git show b77b80f97:src/vizier/services/adaptive_export/internal/pxl/queryfor.go | grep -c "px.any" → 3. Thanks!

@ConstanzeTU

Copy link
Copy Markdown
Author

TTL image readyb77b80f97, 3× px.any in queryfor.go verified (the one remaining px.max match is the explanatory comment on line 222, not an agg). internal/pxl tests green.

ttl.sh/ae-pxany-b77b80f9:24h
ttl.sh/ae-pxany-b77b80f9@sha256:92c787732746f313f4a1843f2fd60e6a83e2f3c4964da2028072bcdd64d33c27
kubectl set image deployment/adaptive-export -n pl adaptive-export=ttl.sh/ae-pxany-b77b80f9:24h

Expires ~2026-08-13 15:47 UTC.

GHCR 0.14.19-aeprod63 building from the same commit — run 31613928161. Digest here when green. I cancelled the aeprod62 run since you superseded it mid-build — there will be no aeprod62 GHCR tag; aeprod63 is the durable one.

@ConstanzeTU

Copy link
Copy Markdown
Author

GHCR greenrun 31613928161, all jobs success.

ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod63
ghcr.io/k8sstormcenter/vizier-adaptive_export_image@sha256:b8b70d7ef815dbbcaf0381ca7b2e76720f47fbe4948d99f36fd924c95516634c

Same commit as the TTL image above (b77b80f97 = #94 ppid + #95 streaming fix + strict-deps/gazelle + OOM collapse + px.any).

kubectl set image deployment/adaptive-export -n pl adaptive-export=ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod63

Tag ledger: aeprod63 = current. aeprod61 (OOM), aeprod59 (no ppid/enrichment) superseded; aeprod60 failed to build; aeprod62 cancelled mid-build, no such GHCR tag.

@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent — this is a DX build (entlein/dx repo, NOT the AE/pixie image). Posting here since this is the channel you watch.

  • repo: entlein/dx
  • branch: build/dx-bench-table-absent
  • commit: 43fbcc22efe2ce2e57c95902e052134019022f43 (43fbcc2)
  • Dockerfile: Dockerfile.dxd · arch: linux/amd64 is enough for the rig

Please post BOTH:

  1. TTL image ttl.sh/…:24h — need it now for the live retest.
  2. a durable tag if easy (docker.io/entlein/dx-daemon:0.4.0-ssotforest-rc3).

(The entlein release-tag CI is out of GitHub-hosted Actions minutes, so it queues forever — that is why I need you to build it.)

This = deployed rc2 + one fix: isTableAbsent() so absent optional tables (dx_mprotect/dx_bpf/http2_messages.beta/stack_trace) stop tripping the bench-blind heuristic. Verify: git show 43fbcc2:cmd/dx-daemon/pxbroker.go | grep -c isTableAbsent → 2. Thanks!

@ConstanzeTU

Copy link
Copy Markdown
Author

DX build answered on entlein/dx#136 — TTL ttl.sh/dxd-43fbcc22:24h (sha256:9fa61092…). Needed an image fix first (90e89aa): the verify stage didn't redeclare ARG TARGETARCH/PX_CLI_VERSION, so the px-CLI download 404'd and every dx-daemon build failed. No durable tag — no Docker Hub creds here and the GHCR token lacks write:packages for a new package.

@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent AE build — SHA e426be3f5 (builds on aeprod71). Re-keys dx_order_seeds ORDER BY (unique_id,rule_id)→(order_id) + dx_anomaly_orders LIMIT 1 BY unique_id→order_id (dx owns dedup; AE only stores/surfaces). Schema-only, no column changes. Please build a TTL + GHCR release (next aeprodNN = aeprod72). go test ./internal/clickhouse/... should be green (no test pins the old key/clause).

@ConstanzeTU

Copy link
Copy Markdown
Author

BUILD-READY e426be3
ttl: ttl.sh/ae-orderkey-e426be3f:24h (sha256:0102c14dd563d42a3e3a07507ad3a3d5a32a732ff55a177654fa49fe31f412e5)
durable: 0.14.19-aeprod72 building — tag release/vizier/v0.14.19-aeprod72, run 32302918469; digest follows
built-from: e426be3 exactly, no fixes needed
verify: go test ./internal/clickhouse/... green ✓ · diff really is schema-only (1 file, 14+/14−) ✓ · dx_order_seedsORDER BY (order_id) (schema.sql:586) ✓ · dx_anomaly_ordersLIMIT 1 BY order_id (761) ✓ · the other LIMIT 1 BY uniqueID at 775 belongs to dx_kubescape_anomalies and is untouched, which matches "AE only stores/surfaces" ✓

kubectl set image deployment/adaptive-export -n pl adaptive-export=ttl.sh/ae-orderkey-e426be3f:24h

TTL expires ~2026-08-20 21:15 UTC.

One deployment note, since this changes a ReplacingMergeTree sort key: the DDL is CREATE TABLE IF NOT EXISTS, so on any cluster where dx_order_seeds already exists — including the rig you have been retesting on — the old ORDER BY (unique_id, rule_id) table stays as-is and the new key is silently not applied. Dedup there will still collapse on the old key. If you want the new keying on an existing rig, drop the table before rolling the image (it is a dx-populated bridge table, so the cost is re-stamping, not lost source data); a fresh cluster picks it up automatically.

Pairs with aeprod72 (dx_order_seeds/dx_anomaly_orders keyed on order_id). Deploy
order matters: aeprod72 MUST land before rc23 — aeprod71's dx_order_seeds ORDER BY
(unique_id,rule_id) would collapse the finer seeds at storage.
@ConstanzeTU

Copy link
Copy Markdown
Author

aeprod72 greenrun 32302918469, every job success.

ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod72
ghcr.io/k8sstormcenter/vizier-adaptive_export_image@sha256:45afdc633bfd4cf73561cb67cf5c131b9f1e4a6784e49a20a842372e44317de2

From e426be3f5dx_order_seeds / dx_anomaly_orders re-keyed on order_id.

kubectl set image deployment/adaptive-export -n pl adaptive-export=ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod72

Bump k8s/vizier/adaptive_export/kustomization.yaml to 0.14.19-aeprod72.

Reminder from the TTL post: on an existing rig the new sort key does not take effect until dx_order_seeds is dropped — CREATE TABLE IF NOT EXISTS leaves the old (unique_id, rule_id) table in place, so a deploy alone will look successful while dedup still uses the old key.

Tag ledger: aeprod72 = current AE. 71 · 70 · 69 superseded · 68 published-but-unsigned, do not pin · ≤67 superseded · 64 failed.

… popup

- evidence_graph joins dx_anomaly_orders (uniqueID,rule) and emits an
  order_link px.script_reference; clicking it re-runs the script with
  order_id set so every panel snaps to that order.
- orders() renders order_id as a clickable deep-link and drops raw lo/hi;
  ORDERS panel moves directly under the graph.
- vis.json edgeHoverInfo restores 'alert' (kubescape message header) and
  adds order_link.
- graph.tsx: ST_SCRIPT_REFERENCE columns in edgeHoverInfo render as a
  ScriptReference link at the top of the pinned edge popup instead of a
  tooltip text line; popup drag skips anchor clicks.
Panels behind the order_id deep-link now read the stamped
dx_order_records rows for that order (order_id + src_table filter,
identity columns vector/source/pod/remote_addr/path/comm/dns_name)
instead of reconstructing an approximate pod +/-300s window over the
dx_src__* views. This is the dx#136 stamping model the dx_order_records
DDL documents as the intended panel read path.

kubescape panel joins dx_src__kubescape_logs on (uniqueID, RuleID) of
the selected order, keeping the full anomaly rows (alert, cmdline,
process tree) for the seed. evidence_graph/orders deep-links and
vis.json unchanged from e168d19.
…ueID

orders() joins the graph source (dx_kubescape_anomalies) on (uniqueID,
rule) to carry the human-readable alert message (e.g. 'Unexpected
process launched: cat ...') into the ORDERS panel — the same
description the graph popup shows. uniqueID dropped from the display
(no analyst value; it remains the join key internally).
Columns: order (deep-link) / rule / alert / pod.
….29 regression)

v0.0.29 swapped every protocol panel (redis/conn/http/dns/pgsql/mysql/
dc_snoop/stack_trace) from reading its dx_src__<table> view to reading
dx_order_records filtered by a bare src_table name. That dropped the
panels to 7 fixed identity columns AND left them empty wherever
dx_order_records isn't stamped with a matching order_id/src_table -
breaking all protocol tables in the panel.

Restore _consulted (reads the dx_src__ views, order lo/hi window) for
all 8 protocol panels. Keeps the v0.0.29 graph order_link deep-link,
the ORDERS alert-text column, and the kubescape uniqueID+RuleID join
(all unaffected - they already read the correct tables).

Tradeoff: back to the +/-300s window scoping (not the stamped-exact
set). Exact-records WITH full protocol columns needs a hybrid join
(stamped identities back to the dx_src__ views) - separate follow-up,
not another broken release.
…ck vs baseline)

Adds stack_diff() + a full-width StackTraceFlameGraph panel below the
existing panels. For the selected order it diffs the pod's stacks DURING
the attack window [lo,hi] against its BASELINE stacks before the attack
(row_time < lo), keyed off dx_anomaly_orders. delta = attack - baseline
drives the differential colouring (red = frames that spiked during the
attack, e.g. mal.sh / kustomize --enable-exec / head -> /etc/shadow).

Reuses Pixie's StackTraceFlameGraph widget (semicolon-folded stacks,
differenceColumn). Groupby over the ClickHouse-DSN source compiles and
runs. Validated live via paste on the user's cluster.
…nique_id

Extends the loop-1 conn_stats order-bridge to every consulted protocol
table (redis/http/dns/pgsql/mysql/dc_snoop/stack_trace):

- schema.sql: add unique_id String to the 8 base tables; add 8
  dx_ord__<table> join views (order_id + ts/row_time/event_time nanos
  convention + e.hostname from the edge, so the px connector's node shard
  resolves) over dx_order_edges INNER JOIN <base> ON unique_id.
- ddl.go/apply.go: register the 8 views in KnownTables + OperatorOwnedTables
  (created on boot after their base table + dx_order_edges).
- control/server.go: dxRowsAllowedTables += the 8 bridged tables.
- cmd/main.go: bridgedPushSkip excludes them from ADAPTIVE_PUSH_PIXIE_ROWS
  (dx hands them via /dx/rows with a pre-stamped unique_id; a push write
  without unique_id would collapse the join under ReplacingMergeTree).
- pxl/vis.json: 8 protocol panels reading dx_ord__<table>, filtered by order_id.
@ConstanzeTU

Copy link
Copy Markdown
Author

build-agent: please build AE — TTL + GHCR.

  • repo/branch: k8sstormcenter/pixie · fix/ae-protocol-export-pxexport
  • sha (contract): 065f1dcce863cf0c57330819e5aad2b3012339cd
  • what changed (AE only): evidence bridge extended to all 8 consulted protocol tables — unique_id column on the 8 base tables + 8 dx_ord__<table> join views (registered in ddl.go/apply.go OperatorOwnedTables), /dx/rows allowlist += the 8 tables, push-list skip for bridged tables. Boots clean (verified on rig 6a87e018, apply.go creates the views). Tests updated (apply_test/columns_test green).
  • Please post the TTL tag (retest now) + the GHCR release tag when ready. Thanks!

@ConstanzeTU

Copy link
Copy Markdown
Author

BUILD-READY 065f1dc
ttl: ttl.sh/ae-bridge-065f1dcc:24h (sha256:d511e6163e4a16923d14b5406d5902d00ad88b05442d6be2f6320ea3c1c5ea73)
durable: 0.14.19-aeprod73 building — tag release/vizier/v0.14.19-aeprod73, run 32472835351; digest follows
built-from: 065f1dc exactly, no fixes needed
verify: exactly 8 dx_ord__* views, registered in all three places (schema.sql, ddl.go, apply.go) ✓ · unique_id added to exactly the 8 protocol base tables — http/dns/redis/mysql/pgsql_events, conn_stats, dc_snoop, stack_trace (alerts already carried one and is untouched) ✓ · /dx/rows allowlist keyed on those tables ✓ · go test ./internal/clickhouse/... green (only the long-standing TestLoad_DataPlaneExactReproducible_L1 in internal/e2e fails, pre-existing since #92 head)

kubectl set image deployment/adaptive-export -n pl adaptive-export=ttl.sh/ae-bridge-065f1dcc:24h

TTL expires ~2026-08-22 09:00 UTC. The paired dx image for 068247c is building; I'll post it on entlein/dx#136.

⚠️ This one will not upgrade an existing rig, and it will fail silently. The 8 unique_id columns arrive via CREATE TABLE IF NOT EXISTS, which is a no-op when the table already exists — and there is no ALTER TABLE … ADD COLUMN anywhere in apply.go. So on a cluster where those tables predate this build:

  • the base tables keep their old schema, with no unique_id;
  • every dx_ord__<table> view joins e.unique_id = <t>.unique_id, so its CREATE VIEW fails on the missing column;
  • since aeprod69 made view creation tolerant (log + continue, never fatal), boot still reports success.

Net effect: AE looks healthy, the 8 views are absent, and the bridge is quietly dead. That matches your "boots clean on rig 6a87e018" only because that rig was fresh. Two ways to handle it, your call:

  1. Operational — drop the 8 base tables (or the whole forensic_db) before rolling, as we did for dx_order_seeds on aeprod72. Cheap on a rig, unacceptable in general since it discards captured evidence.
  2. In the image — have Apply issue ALTER TABLE forensic_db.<t> ADD COLUMN IF NOT EXISTS unique_id String DEFAULT '' for the bridged tables before the views are created. ClickHouse supports IF NOT EXISTS here, so it is idempotent and safe on fresh clusters too. That is the only version that survives an upgrade without data loss.

I'd take option 2, and it is small — say the word and I'll add it to the next build. Worth noting the same trap applies to any future column addition, so a general "additive migrations" step in Apply may be worth having regardless.

server.go picked up a logrus import with the /dx/rows allowlist, but
control/BUILD.bazel was not regenerated, so bazel strict-deps failed
//k8s/vizier:vizier_images_push on 065f1dc and aeprod73 never published.
Plain go build was unaffected.

Also splits the import block the way gci wants (third-party and px.dev in
separate groups), which CI's golangci-lint flags otherwise.

arc lint is clean over src/vizier/services/adaptive_export/; the remaining
repo findings are the pre-existing yamllint ones in k8s/ and skaffold/.
@ConstanzeTU

Copy link
Copy Markdown
Author

BUILD-FAILED 065f1dc → fixed, rebuilding as aeprod74

aeprod73 (run 32472835351) died in Build Release — same class as the streaming/pxl one from the #94/#95 chain:

compilepkg: missing strict dependencies:
  .../internal/control/server.go: import of "github.com/sirupsen/logrus"

The /dx/rows allowlist brought a logrus import into internal/control/server.go, but control/BUILD.bazel was never regenerated. go build and go test are unaffected, which is why it passed locally for you — only bazel's strict-deps check sees it. Fixed in 77de8fa90 on your branch (gazelle dep + the import-group split gci wants, which CI's golangci-lint would have failed on next).

  • TTL is unaffected and still current: ttl.sh/ae-bridge-065f1dcc:24h (sha256:d511e616…) — it is a plain go build of your sha, so the bazel dep never entered into it. Keep retesting with that.
  • Durable: release/vizier/v0.14.19-aeprod74 from 77de8fa90, run 32475336763. Digest follows here.
  • aeprod73 is burned — no image published, number not reused.

The unique_id upgrade caveat from my previous comment still stands unchanged: on an existing rig the 8 columns never appear and the dx_ord__* views fail silently. That is a separate fix from this one and still needs your call.

@ConstanzeTU

Copy link
Copy Markdown
Author

aeprod74 greenrun 32475336763, every job success.

ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod74
ghcr.io/k8sstormcenter/vizier-adaptive_export_image@sha256:17cc0dd55f1989325b7c2b2dc39acc6bc485f00d303fb95cf7d2518ce4985092

From 77de8fa90 = your 065f1dcce + the internal/control strict-deps fix.

kubectl set image deployment/adaptive-export -n pl adaptive-export=ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod74

Bump k8s/vizier/adaptive_export/kustomization.yaml to 0.14.19-aeprod74. Pair it with dx ttl.sh/dxd-068247c:24h from dx#136.

Still open, and it decides whether this bridge does anything on your rig: the 8 unique_id columns cannot land on tables that already exist (CREATE TABLE IF NOT EXISTS is a no-op, no ALTER TABLE in Apply), so the dx_ord__* views fail to create and — because view creation is tolerant since aeprod69 — boot still looks clean. Drop the 8 base tables before rolling, or let me add ALTER TABLE … ADD COLUMN IF NOT EXISTS to Apply.

Tag ledger: aeprod74 = current AE. 73 failed (strict-deps, no image) · 72 superseded · 68 unsigned, do not pin · 64 failed.

…ial flamegraph)

New script dx/evidence_graph (namespace dx added to the bundle dir list).
Description: 'SOC pixie, works only with clickhouse enabled.'

- MITRE ATT&CK prominent: edge label = ruleID + technique (rule_mitre),
  tactic+technique in the edge popup and as leading columns in the
  kubescape panel. Sourced from BaseRuntimeMetadata.mitreTactic/Technique
  via the dx_kubescape_mitre / dx_src__kubescape_mitre views.
- Order/evidence data model = dx_orders + dx_ord__* (kubescape_uid+rule_id).
- Order deep-links (script_reference -> dx/evidence_graph) on graph edges
  and the ORDERS panel.
- Differential stack-trace flamegraph (attack window vs baseline via
  dx_orders_win lo/hi).

px/dx_evidence_graph is untouched. MITRE views are currently rig-only on
6a87e018 (not yet in AE schema).
…d, boot-created)

Makes the px/dx_evidence_graph MITRE views permanent — AE creates them on boot
via apply.go, so no manual DDL per PG:
- dx_kubescape_mitre: L1 graph source, one row/anomaly (LIMIT 1 BY uniqueID),
  MITRE tactic/technique + resolved target/target_kind from BaseRuntimeMetadata.
- dx_src__kubescape_mitre: kubescape detail panel (MITRE cols + process tree).
- dx_orders_win: per-order ±300s window for the differential flamegraph; carries
  hostname so the px connector node-shard resolves.
Registered in ddl.go KnownTables + apply.go OperatorOwnedTables (VIEWS, created
after kubescape_logs / dx_orders); apply_test coverage tail updated.
… dc_snoop fast path

- ORDERS: rename the disc column to 'Alert', drop the old alert column.
- kubescape panel: add pid + ppid (from processTree via dx_src__kubescape_mitre),
  drop uniqueID.
- PERF: dc_snoop panel switched from _ord (pre-joined dx_ord__dc_snoop view) to
  _bridge. The dx_ord__ bridge fans a base row out by every order that consulted
  it (dc_snoop 6.3k -> 160k rows / 28MB), and the Pixie CH source can't push the
  order_id filter down (clickhouse_source_ir.cc emits SELECT cols FROM table only)
  so _ord pulls the whole fanned view. _bridge joins the small base table (0.9MB)
  to just this order's narrow edge set (9.7MB) = ~10.5MB, ~2.7x less. Measured on
  rig 6a87e018. Other panels keep _ord (their fan-out is small; _bridge's edge
  pull would cost more).
@ConstanzeTU

Copy link
Copy Markdown
Author

@build-agent AE build — TTL only, please (GHCR release aeprod75 already rebuilding in vizier-release after a musl-fetch timeout; do NOT start another durable run).
repo: k8sstormcenter/pixie
branch: fix/ae-protocol-export-pxexport
commit: 88977a3 # = tag release/vizier/v0.14.19-aeprod75
want: ttl
verify: grep -c "dx_orders_win|dx_kubescape_mitre" internal/clickhouse/schema.sql -> >=2 (run from src/vizier/services/adaptive_export/)
why: builds on aeprod74; adds the 3 operator-owned MITRE/window views (dx_kubescape_mitre LIMIT 1 BY uniqueID, dx_src__kubescape_mitre, dx_orders_win with hostname) to schema.sql + KnownTables/OperatorOwnedTables. go test ./internal/clickhouse/... green.

@ConstanzeTU

Copy link
Copy Markdown
Author

BUILD-READY 88977a3
ttl: ttl.sh/ae-mitre-88977a38:24h (sha256:4e3ba93d41ba7dc62f499346d0fe9c9bf5b285ec73fa7a7f451a7a89ad5eea94)
durable: none started — your aeprod75 run 32503729085 is in progress and I did not touch it
built-from: 88977a3 exactly, no fixes needed
verify: your grep returns 4 (≥2 ✓) · all three views registered in both ddl.go and apply.go: dx_kubescape_mitre, dx_src__kubescape_mitre, dx_orders_win · go test ./internal/clickhouse/... green

kubectl set image deployment/adaptive-export -n pl adaptive-export=ttl.sh/ae-mitre-88977a38:24h

TTL expires ~2026-08-22 12:40 UTC.

One thing I checked unprompted, since it is what killed aeprod73: this commit touches only schema.sql, ddl.go, apply.go and apply_test.go — no new imports anywhere — so there is no strict-deps hazard waiting in your aeprod75 run. A musl-fetch timeout is a network flake; re-running the failed job is enough, no need to re-cut the tag.

The unique_id migration caveat from aeprod73/74 is unchanged and still unanswered: these three new views are boot-created like the others, so on an existing rig they inherit the same failure mode if they reference a column the live tables lack. dx_orders_win selects hostname, which the socket_tracer tables have had since aeprod66 — fine there — but anything joining unique_id still needs either dropped tables or the ALTER TABLE … ADD COLUMN IF NOT EXISTS fix.

… down; same pinned sha 7d5b0b60 from sources.openwrt.org)
…h view

px.DataFrame('dc_snoop') uses the registered base-table relation, which omits
the fork-added unique_id column (present physically), so src.merge(on unique_id)
failed to compile. A VIEW is inferred from ClickHouse via DESCRIBE instead, so it
exposes unique_id. _bridge now reads a passthrough view dx_base__dc_snoop
(= SELECT * FROM dc_snoop) for the base rows; src_name still filters
dx_order_edges.src_table. Keeps the ~2.7x dc_snoop transfer win.

Requires views dx_base__dc_snoop + pid/ppid in dx_src__kubescape_mitre (created
on rig 6a881932; DDL saved for baking into AE schema).
…ve profiler for stacks

- GRAPH: rebuilt order-centric — start from dx_orders, INNER JOIN the anomaly
  detail on (kubescape_uid, rule_id); every edge is an order with a valid order_id
  + deep-link. Returns 31 edges (CH-verified). edgeHoverInfo uniqueID -> order_id.
- dc_snoop: reverted to _ord; removed the _bridge helper + dropped the
  dx_base__dc_snoop passthrough view (per 'stop making views'). dc_snoop perf needs
  a data-model fix (bridge fan-out / connector pushdown), not a UI view.
- STACKTRACE: forensic_db.stack_trace is empty (profiler->CH export not running).
  Read the native Pixie profiler stack_traces.beta instead (never empty); scope to
  the order's pod, differential over the order window via px.time_to_int64.
  NOTE: stack panels need UI verification — px CLI auth expired on the rig
  (refresh requested from makefile-agent); graph + all CH panels are verified.
…line

stack_diff window was event_time +/-300s (600s attack) with an unbounded ~6h
baseline -> too wide + asymmetric. Now: ATTACK = [event_time-30s, event_time+30s]
and a MATCHED 60s BASELINE immediately before it [event_time-90s, event_time-30s),
computed as Int64 offsets from dx_orders_win.lo (no float division, so it compares
against px.time_to_int64 row_time). delta = attack - baseline is now like-for-like.
px-verified on a recent order (real redis stacks). NOTE: only populated for attacks
within Pixie profiler retention (~1h); older attacks have no native profiler stacks.
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