diff --git a/deploy/configs/asap-otel-agent-gorillas3-tier.yaml b/deploy/configs/asap-otel-agent-gorillas3-tier.yaml index 46f85f4d..312da771 100644 --- a/deploy/configs/asap-otel-agent-gorillas3-tier.yaml +++ b/deploy/configs/asap-otel-agent-gorillas3-tier.yaml @@ -1,9 +1,14 @@ -# Gorilla-S3 cold-engine tier (Phase 2 of the cold-engine integration). +# Cold-path tier — gorillas3processor in `gateway_raw` role. # -# Mirrors the b5-gorilla baseline shape but swaps the local-dir sink -# for the S3-cold-engine `gorillas3` processor: encode raw samples -# with Gorilla XOR-delta, write 60s chunks to MinIO, and DROP the -# original metric so the agent does not OTLP-forward the bytes. +# Direct raw-metric → Prometheus TSDB block emit. Suitable for the +# single-collector cold-path layout (no upstream fragment edge agent; +# this collector consumes raw OTLP and finalizes blocks itself). +# +# Output objects in MinIO: +# +# //chunks/000001 +# //index +# //meta.json (uploaded last; readiness signal) # # Reuses the existing `minio` container in deploy/docker-compose/base.yml # (no overlay change needed). Bucket `asap-gorilla` must exist; the @@ -28,18 +33,19 @@ receivers: processors: gorillas3: + role: gateway_raw + delivery_mode: durable_raw window_interval: 60s drop_original: true endpoint: http://minio:9000 bucket: asap-gorilla + tsdb_bucket: asap-gorilla region: us-east-1 use_ssl: false # MinIO root creds from base.yml. Replace with env-resolution in # production deploys (e.g. ${env:MINIO_ACCESS_KEY}). access_key_id: asap secret_access_key: asap-local-only - prefix_template: "{tenant}/{metric}/{YYYY}/{MM}/{DD}/{HH}/" - tenant: default max_retries: 3 retry_backoff: 1s upload_timeout: 30s diff --git a/deploy/docker-compose/cross-host-parity.yml b/deploy/docker-compose/cross-host-parity.yml index 4e5c74c0..8acfb7d0 100644 --- a/deploy/docker-compose/cross-host-parity.yml +++ b/deploy/docker-compose/cross-host-parity.yml @@ -46,11 +46,11 @@ services: command: ["--config=/etc/otel/config.yaml"] volumes: # Agent configs live next to the test driver under - # integration/cross_host_parity/configs/ — keeps Phase 5E's + # integration/parity/agent-binary/configs/ — keeps Phase 5E's # per-agent shape co-located with the harness that drives them # rather than scattered through deploy/configs/, which is owned # by the eval pipeline (agent #13). - - ../../integration/cross_host_parity/configs/asap-otel.yaml:/etc/otel/config.yaml:ro + - ../../integration/parity/agent-binary/configs/asap-otel.yaml:/etc/otel/config.yaml:ro environment: - ASAP_AGENT_ID=asap-otel networks: @@ -64,7 +64,7 @@ services: container_name: parity-asap-otap command: ["--config=/etc/otap/config.yaml"] volumes: - - ../../integration/cross_host_parity/configs/asap-otap.yaml:/etc/otap/config.yaml:ro + - ../../integration/parity/agent-binary/configs/asap-otap.yaml:/etc/otap/config.yaml:ro environment: - ASAP_AGENT_ID=asap-otap networks: @@ -78,7 +78,7 @@ services: container_name: parity-asap-telegraf command: ["--config=/etc/telegraf/telegraf.conf"] volumes: - - ../../integration/cross_host_parity/configs/asap-telegraf.toml:/etc/telegraf/telegraf.conf:ro + - ../../integration/parity/agent-binary/configs/asap-telegraf.toml:/etc/telegraf/telegraf.conf:ro environment: - ASAP_AGENT_ID=asap-telegraf networks: @@ -106,8 +106,8 @@ services: container_name: parity-envelope-tap command: ["--mode=cross-host-parity-tap", "--capture-dir=/captures"] volumes: - - ../../integration/cross_host_parity/captures:/captures - - ../../integration/cross_host_parity/golden_input:/golden_input:ro + - ../../integration/parity/agent-binary/captures:/captures + - ../../integration/parity/agent-binary/golden_input:/golden_input:ro - ../scripts/queries-e2e.json:/queries-e2e.json:ro networks: - parity-net diff --git a/integration/gorilla_s3_e2e/Makefile b/integration/e2e-cold-path/Makefile similarity index 54% rename from integration/gorilla_s3_e2e/Makefile rename to integration/e2e-cold-path/Makefile index 53d21aeb..34a3c68c 100644 --- a/integration/gorilla_s3_e2e/Makefile +++ b/integration/e2e-cold-path/Makefile @@ -1,8 +1,8 @@ -# Phase 6 Gorilla-S3 e2e shorthand. See README.md for full docs. +# Cold-path e2e shorthand. See README.md for full docs. # # Targets: # test — fixture-only run (no docker, no sweep coordination) -# test-live — live run (gated on GORILLA_E2E_LIVE=1; skips if sweep alive) +# test-live — live run (gated on COLD_E2E_LIVE=1; skips if sweep alive) # test-force — live run, ignore sweep busy check (use sparingly) # vet — go vet # down — tear down a stuck e2e compose stack @@ -13,14 +13,14 @@ test: go test -v ./... test-live: - GORILLA_E2E_LIVE=1 go test -v -run TestGorillaS3End2End ./... + COLD_E2E_LIVE=1 go test -v -run TestColdPathEnd2End ./... test-force: - GORILLA_E2E_LIVE=1 GORILLA_E2E_FORCE=1 go test -v -run TestGorillaS3End2End ./... + COLD_E2E_LIVE=1 COLD_E2E_FORCE=1 go test -v -run TestColdPathEnd2End ./... vet: go vet ./... down: - docker compose -p asap-gorilla-e2e \ + docker compose -p asap-cold-e2e \ -f docker-compose/e2e-overlay.yml down -v diff --git a/integration/e2e-cold-path/README.md b/integration/e2e-cold-path/README.md new file mode 100644 index 00000000..c076b294 --- /dev/null +++ b/integration/e2e-cold-path/README.md @@ -0,0 +1,124 @@ +# integration/e2e-cold-path + +End-to-end integration test for the ASAP cold-path pipeline: + +``` +fake-driver ─OTLP─▶ asap-otel + gorillas3processor (gateway_raw) + │ Prometheus TSDB block builder + ▼ + MinIO (/chunks/000001 + index + meta.json) + │ + ▼ + Thanos store-gateway / backend cold-path +``` + +This is the cold-path e2e gate. After PR #359 the +`gorillas3processor` runtime emits Prometheus TSDB blocks (not the +legacy `GORILLA1` magic-prefixed XOR-delta chunks). The legacy in-test +decoder + cross-language Rust subtest were retired with the rename +from `integration/gorilla_s3_e2e/` to this directory. + +## Layout + +``` +integration/e2e-cold-path/ +├── e2e_test.go # main Go test — TestColdPathEnd2End +├── golden/ +│ └── input_samples.json # 100 deterministic samples +├── docker-compose/ +│ └── e2e-overlay.yml # ephemeral stack (separate compose +│ # project + 29xxx port range) +├── go.mod # depends on prometheus/prometheus +├── Makefile # `make test` / `make test-live` +└── README.md +``` + +## How the assertion works + +The runtime side of the cold path is the `gorillas3processor` in +`gateway_raw` role + `delivery_mode=durable_raw`. On each +`window_interval` tick it finalizes a Prometheus TSDB block to +S3/MinIO with three files under `//`: + +- `chunks/000001` — XOR-encoded sample chunks +- `index` — Prometheus inverted index +- `meta.json` — block metadata (uploaded LAST, so Thanos doesn't see + half-written blocks) + +The test: + +1. Lists every `meta.json` marker in the `asap-gorilla` MinIO bucket + via `mc find ... --name 'meta.json'`. Each match identifies one + finalized block ULID. +2. Downloads each block's three files into a per-block temp dir. +3. Opens the block with `prometheus/tsdb`'s `tsdb.OpenBlock(nil, dir, + nil, nil)`, builds a `BlockQuerier` over the full time range, and + selects every series matching `__name__=~.+`. +4. Iterates the resulting `SeriesSet`, flattens to (label_set, ts, + value) tuples, and asserts the multi-set of values equals the + input fixture. + +## Run + +### Default (no docker, fixture-only) + +``` +cd integration/e2e-cold-path +go test -v ./... +``` + +The live e2e test (`TestColdPathEnd2End`) is gated on +`COLD_E2E_LIVE=1` and SKIPs in this mode; the +`TestGoldenInputFixtureWellFormed` test still runs and pins the +shape of the input fixture. + +### Live e2e (requires docker, sweep idle) + +``` +COLD_E2E_LIVE=1 go test -v -run TestColdPathEnd2End ./... +``` + +The live test: + +1. Brings up `minio-e2e + asap-otel-e2e` via + `docker compose -p asap-cold-e2e -f docker-compose/e2e-overlay.yml up -d`. +2. Pushes the 100-sample fixture via OTLP/HTTP to `127.0.0.1:24318/v1/metrics`. +3. Waits 75s for the gorillas3processor's 60s window to flush. +4. Runs `HappyPath` and `MultiBlockRange` subtests against the live stack. +5. Tears the stack down with `down -v`. + +### Coordination with the host-wide sweep + +The host runs a long sweep (`run_e2e_sweep.sh`) that owns the default +compose project. The e2e test takes the **defensive** posture: + +- The overlay uses a separate compose project name (`asap-cold-e2e`) + and a separate port range (29xxx) so it would NOT collide on + ports — but +- Docker memory + CPU + network bandwidth are still shared with the + sweep. If the sweep PID (default `2862786`, override via + `COLD_E2E_SWEEP_PID=`) is alive, the live test SKIPs unless + forced via `COLD_E2E_FORCE=1`. +- Disable the busy-check entirely with `COLD_E2E_SWEEP_PID=0` (or + `=none`) — useful in CI where there's no sweep concept. + +## Subtests + +| Subtest | What it asserts | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `HappyPath` | MinIO contains ≥1 finalized block (a `meta.json` exists); the block decodes back to the input value-set. | +| `MultiBlockRange` | If the fixture spread crossed a window boundary, ≥2 blocks land; their union covers the full input. | +| `TestGoldenInputFixtureWellFormed` | (always runs, no docker) The fixture's `expected_sum` invariant matches `sum(values)`. | + +## Notes + +- The bucket name `asap-gorilla` matches the legacy MVP demo. +- Block download uses `mc cat` inside a transient `minio/mc:latest` + container; no S3 SDK in `go.mod`. +- OTLP push uses raw HTTP/JSON to the agent's `/v1/metrics` endpoint; + no OTLP client in `go.mod`. +- The PromQL accuracy / data-source-marker subtests from the legacy + Gorilla-S3 e2e are NOT carried over: TSDB is upstream Prometheus's + native format, not an ASAP-specific wire format. PromQL response + shape is now exercised by the warm-path e2e + ASAPQuery-backend's + own tests. diff --git a/integration/e2e-cold-path/docker-compose/e2e-overlay.yml b/integration/e2e-cold-path/docker-compose/e2e-overlay.yml new file mode 100644 index 00000000..e136315d --- /dev/null +++ b/integration/e2e-cold-path/docker-compose/e2e-overlay.yml @@ -0,0 +1,113 @@ +# Cold-path e2e overlay. +# +# Brings up an EPHEMERAL stack on a separate compose project +# (`asap-cold-e2e`) and a separate port range (29xxx) so it can run +# side-by-side with the host-wide sweep stack +# (`docker-compose-{minio,backend,...}` on 19xxx) without port +# collisions. +# +# Coordination: the e2e Go test calls `docker compose -p asap-cold-e2e +# -f integration/e2e-cold-path/docker-compose/e2e-overlay.yml up -d` +# and tears it down with `down -v` on test completion. Sweep is left +# untouched. +# +# Services brought up: +# +# minio-e2e MinIO S3-compatible store, ports 29000/29001 +# minio-setup-e2e Seeds the `asap-gorilla` bucket the +# gorillas3processor writes Prometheus TSDB +# blocks (`/chunks/000001 + index + +# meta.json`) into. +# asap-otel-e2e Single asap-otel agent running +# asap-otel-agent-gorillas3-tier.yaml in +# role=gateway_raw + delivery_mode=durable_raw — +# consumes raw OTLP and finalizes Prometheus +# TSDB blocks to MinIO once per window_interval. +# +# fake-exporter is intentionally NOT in the compose stack; the Go +# test pushes the deterministic 100-sample fixture directly via OTLP +# to asap-otel-e2e. This is the only way to drive a fixed, repeatable +# input — fake-exporter generates random data. +# +# The backend container is also intentionally NOT in this overlay. +# The cold-path test asserts byte-equality of the recovered TSDB +# samples vs. the input fixture; PromQL response shape is exercised +# elsewhere (warm-path e2e + ASAPQuery-backend tests). + +version: "3.9" + +x-logging: &default-logging + driver: json-file + options: + max-size: "5m" + max-file: "2" + +services: + minio-e2e: + image: minio/minio:latest + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: asap + MINIO_ROOT_PASSWORD: asap-local-only + ports: + - "29000:9000" + - "29001:9001" + volumes: + - minio-e2e-data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 3s + retries: 20 + logging: *default-logging + + # Pre-create the asap-gorilla bucket so the gorillas3processor's + # PutTSDBBlock calls land somewhere. The default base.yml only seeds + # `raw` for the JSONL cold-fallback adapter; the TSDB blocks need a + # separate bucket per the gorillas3processor README. + minio-setup-e2e: + image: minio/mc:latest + depends_on: + minio-e2e: + condition: service_healthy + entrypoint: /bin/sh + command: + - -c + - | + mc alias set asap http://minio-e2e:9000 asap asap-local-only && + mc mb --ignore-existing asap/asap-gorilla && + mc anonymous set download asap/asap-gorilla + logging: *default-logging + + # Single asap-otel agent running the gorillas3 cold-path tier + # config. The OTLP receiver listens on 4317 (gRPC) + 4318 (HTTP); + # we expose both on host 24317/24318 so the Go test can drive + # samples in without going through a fake-exporter container. + asap-otel-e2e: + image: asap/asap-otel:dev + depends_on: + minio-setup-e2e: + condition: service_completed_successfully + command: + - "--config=/etc/otel/config.yaml" + volumes: + # The cold-path config — drop_original=true, encode + emit TSDB + # blocks to MinIO. Path is relative to the compose file's dir. + - ../../../deploy/configs/asap-otel-agent-gorillas3-tier.yaml:/etc/otel/config.yaml:ro + environment: + RUST_LOG: "info" + networks: + default: + aliases: + - minio + # The agent config hardcodes endpoint http://minio:9000; + # alias the e2e MinIO under that name so no YAML rewrite + # is needed. + ports: + - "24317:4317" + - "24318:4318" + - "28890:8890" + logging: *default-logging + +volumes: + minio-e2e-data: diff --git a/integration/e2e-cold-path/e2e_test.go b/integration/e2e-cold-path/e2e_test.go new file mode 100644 index 00000000..f00c6390 --- /dev/null +++ b/integration/e2e-cold-path/e2e_test.go @@ -0,0 +1,614 @@ +// Package coldpathe2e is the cold-path end-to-end integration test +// for the ASAP cold-path pipeline: +// +// fake-driver ─OTLP─▶ asap-otel + gorillas3processor (gateway_raw) +// │ Prometheus TSDB block builder +// ▼ +// MinIO (/chunks/000001 + index + meta.json) +// │ +// ▼ +// Thanos store-gateway / backend cold-path +// +// After PR #359 the gorillas3processor emits Prometheus TSDB blocks +// (`/chunks/000001`, `/index`, `/meta.json`) — the +// legacy GORILLA1 magic-prefixed XOR-delta chunk format the old +// in-test decoder targeted is no longer produced at runtime. The new +// test reads each finalized block back via `prometheus/tsdb`'s +// `OpenBlock` + `BlockQuerier`, then asserts byte-equality of the +// recovered (label, ts, value) tuples against the input fixture. +// +// # Subtests +// +// `TestColdPathEnd2End` is the orchestrator. It brings the docker +// stack up once and then drives subtests against it: +// +// - HappyPath — fixture pushed, MinIO has ≥1 finalized +// block (meta.json present), block decodes to +// the input sample-set. +// - MultiBlockRange — if input spans >1 window, ≥2 blocks land +// and their union covers the full input. +// +// (The PromQL accuracy / data-source-marker subtests and the +// CrossLanguageByteCompat subtest from the legacy GORILLA1 e2e are +// retired here: TSDB is upstream-Prometheus's native format, not an +// ASAP-specific wire format, so neither a separate Rust decoder nor +// a "kind=Exact, ε=0" backend marker applies — Thanos serves these +// blocks as ordinary Prometheus storage.) +package coldpathe2e + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/tsdb" + "github.com/prometheus/prometheus/tsdb/chunkenc" +) + +// ----------------------------------------------------------------- +// Sweep-coordination + setup helpers. +// ----------------------------------------------------------------- + +const defaultSweepPID = 2862786 + +// composeProject is the compose project name for the ephemeral +// e2e stack. Pinned so a local `docker compose -p asap-cold-e2e +// down -v` will tear it down even if the test panicked. +const composeProject = "asap-cold-e2e" + +// MinIO + agent host-side ports. Match docker-compose/e2e-overlay.yml. +const ( + hostPortMinIOS3 = 29000 + hostPortAgentOTLP = 24317 +) + +// liveOrSkip is the gate every "needs-docker" subtest goes through. +func liveOrSkip(t *testing.T) bool { + t.Helper() + + if v := os.Getenv("COLD_E2E_LIVE"); v != "1" { + t.Skipf("COLD_E2E_LIVE != 1; set COLD_E2E_LIVE=1 to run the live " + + "docker-compose path. Defaults skip so `go test ./...` from a fresh " + + "checkout never tries to bring up containers.") + return false + } + if force := os.Getenv("COLD_E2E_FORCE"); force != "1" { + if running, pid := isSweepRunning(); running { + t.Skipf("sweep process PID=%d is alive; the live e2e path competes "+ + "with it for docker / CPU / RAM. Run after the sweep finishes, "+ + "or override with COLD_E2E_FORCE=1.", pid) + return false + } + } + return true +} + +// isSweepRunning checks whether the host-wide sweep process is alive. +func isSweepRunning() (bool, int) { + pidStr := os.Getenv("COLD_E2E_SWEEP_PID") + if pidStr == "" { + pidStr = strconv.Itoa(defaultSweepPID) + } + if pidStr == "0" || pidStr == "none" { + return false, 0 + } + pid, err := strconv.Atoi(pidStr) + if err != nil || pid <= 0 { + return false, 0 + } + if _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)); err == nil { + return true, pid + } + return false, pid +} + +// ----------------------------------------------------------------- +// Top-level orchestrator. +// ----------------------------------------------------------------- + +// TestColdPathEnd2End runs the live cold-path pipeline. +func TestColdPathEnd2End(t *testing.T) { + if !liveOrSkip(t) { + return + } + + repoRoot, err := repoRoot() + if err != nil { + t.Fatalf("repoRoot: %v", err) + } + composeFile := filepath.Join(repoRoot, + "integration", "e2e-cold-path", "docker-compose", "e2e-overlay.yml") + if _, err := os.Stat(composeFile); err != nil { + t.Fatalf("compose overlay missing at %s: %v", composeFile, err) + } + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not on PATH (%v) — live e2e requires Docker Engine + compose v2", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + t.Logf("bringing up compose project %s with %s", composeProject, composeFile) + upCmd := exec.CommandContext(ctx, "docker", "compose", + "-p", composeProject, "-f", composeFile, "up", "-d") + upCmd.Stdout = testLogWriter{t} + upCmd.Stderr = testLogWriter{t} + if err := upCmd.Run(); err != nil { + t.Fatalf("docker compose up: %v", err) + } + t.Cleanup(func() { + downCtx, downCancel := context.WithTimeout(context.Background(), 60*time.Second) + defer downCancel() + downCmd := exec.CommandContext(downCtx, "docker", "compose", + "-p", composeProject, "-f", composeFile, "down", "-v") + downCmd.Stdout = testLogWriter{t} + downCmd.Stderr = testLogWriter{t} + _ = downCmd.Run() + }) + + if err := waitForTCP(ctx, fmt.Sprintf("127.0.0.1:%d", hostPortMinIOS3), 90*time.Second); err != nil { + t.Fatalf("MinIO not reachable: %v", err) + } + if err := waitForTCP(ctx, fmt.Sprintf("127.0.0.1:%d", hostPortAgentOTLP), 90*time.Second); err != nil { + t.Fatalf("asap-otel-e2e OTLP gRPC port not reachable: %v", err) + } + + fixture, err := loadFixture(repoRoot) + if err != nil { + t.Fatalf("loadFixture: %v", err) + } + + now := time.Now().UnixNano() + if err := pushSamplesOTLPHTTP(ctx, hostPortAgentOTLP+1 /* :4318 → host 24318 */, fixture, now); err != nil { + t.Fatalf("pushSamplesOTLPHTTP: %v", err) + } + + // gorillas3processor flushes once per `window_interval`. Wait one + // window + a small slack for the S3 PutObject + meta.json upload to + // land. + t.Logf("waiting 75s for the gorillas3 window flush + S3 PutObject") + select { + case <-ctx.Done(): + t.Fatalf("context expired before window flush: %v", ctx.Err()) + case <-time.After(75 * time.Second): + } + + t.Run("HappyPath", func(t *testing.T) { + runHappyPath(ctx, t, fixture) + }) + t.Run("MultiBlockRange", func(t *testing.T) { + runMultiBlockRange(ctx, t, fixture) + }) +} + +// ----------------------------------------------------------------- +// Subtest bodies. +// ----------------------------------------------------------------- + +func runHappyPath(ctx context.Context, t *testing.T, fixture inputFixture) { + blocks, err := listAndFetchBlocks(ctx, t) + if err != nil { + t.Fatalf("list/fetch blocks: %v", err) + } + if len(blocks) == 0 { + t.Fatalf("no finalized TSDB blocks under tsdb_bucket; gorillas3processor did not flush") + } + t.Logf("found %d finalized block(s)", len(blocks)) + + got, err := readSamplesFromBlocks(t, blocks, fixture.MetricName) + if err != nil { + t.Fatalf("read samples from blocks: %v", err) + } + if len(got) != fixture.ExpectedCount { + t.Fatalf("decoded %d samples, want %d", len(got), fixture.ExpectedCount) + } + + // Decoded values should match the fixture set (order-insensitive + // — TSDB sorts by ts and we shifted timestamps to `now`, so we + // only verify the multi-set of values matches). + wantValues := append([]float64(nil), fixture.Values...) + sort.Float64s(wantValues) + gotValues := make([]float64, len(got)) + for i, s := range got { + gotValues[i] = s.Value + } + sort.Float64s(gotValues) + for i := range wantValues { + if wantValues[i] != gotValues[i] { + t.Fatalf("value #%d mismatch: got=%v want=%v", i, gotValues[i], wantValues[i]) + } + } + t.Logf("HappyPath: 100 input values round-tripped through encoder + S3 + TSDB block reader") +} + +func runMultiBlockRange(ctx context.Context, t *testing.T, fixture inputFixture) { + // The fixture spans 99s (1s spacing). With window_interval=60s, the + // gorillas3processor should produce at least ONE block; depending + // on alignment with the window boundary, it MAY produce two. + // Assert "≥1" — and when "≥2", also assert their union covers the + // full input set. + blocks, err := listAndFetchBlocks(ctx, t) + if err != nil { + t.Fatalf("list/fetch blocks: %v", err) + } + if len(blocks) < 1 { + t.Fatalf("expected ≥1 blocks, got %d", len(blocks)) + } + if len(blocks) >= 2 { + merged, err := readSamplesFromBlocks(t, blocks, fixture.MetricName) + if err != nil { + t.Fatalf("multi-block read: %v", err) + } + if len(merged) != fixture.ExpectedCount { + t.Fatalf("multi-block merge yielded %d samples, want %d", + len(merged), fixture.ExpectedCount) + } + t.Logf("MultiBlockRange: 2+ blocks aggregated correctly (%d samples)", + len(merged)) + } else { + t.Logf("MultiBlockRange: 1 block only — input fits inside a single 60s "+ + "window boundary. Subtest still validates the single-block path; "+ + "true multi-block coverage would require a >120s input fixture or "+ + "a smaller window_interval. (fixture window=%ds)", + fixture.IntervalNs*int64(len(fixture.Values))/int64(time.Second)) + } +} + +// ----------------------------------------------------------------- +// Sibling fixture-only subtests — always run, no docker required. +// ----------------------------------------------------------------- + +// TestGoldenInputFixtureWellFormed pins the shape of the input +// fixture so a typo in `golden/input_samples.json` surfaces here +// rather than mid-pipeline as an opaque decode error. +func TestGoldenInputFixtureWellFormed(t *testing.T) { + repoRoot, err := repoRoot() + if err != nil { + t.Fatalf("repoRoot: %v", err) + } + fx, err := loadFixture(repoRoot) + if err != nil { + t.Fatalf("loadFixture: %v", err) + } + if fx.MetricName == "" { + t.Errorf("metric_name empty") + } + if len(fx.Values) != fx.ExpectedCount { + t.Errorf("len(values)=%d ≠ expected_count=%d", len(fx.Values), fx.ExpectedCount) + } + var sum float64 + for _, v := range fx.Values { + sum += v + } + if sum != fx.ExpectedSum { + t.Errorf("sum(values)=%v ≠ expected_sum=%v", sum, fx.ExpectedSum) + } +} + +// ----------------------------------------------------------------- +// Fixture loader + input model. +// ----------------------------------------------------------------- + +type inputFixture struct { + MetricName string `json:"metric_name"` + Labels map[string]string `json:"labels"` + IntervalNs int64 `json:"interval_ns"` + Values []float64 `json:"values"` + ExpectedSum float64 `json:"expected_sum"` + ExpectedCount int `json:"expected_count"` + ExpectedMin float64 `json:"expected_min"` + ExpectedMax float64 `json:"expected_max"` +} + +func loadFixture(repoRoot string) (inputFixture, error) { + path := filepath.Join(repoRoot, + "integration", "e2e-cold-path", "golden", "input_samples.json") + b, err := os.ReadFile(path) + if err != nil { + return inputFixture{}, fmt.Errorf("read fixture %s: %w", path, err) + } + var fx inputFixture + if err := json.Unmarshal(b, &fx); err != nil { + return inputFixture{}, fmt.Errorf("parse fixture %s: %w", path, err) + } + return fx, nil +} + +// repoRoot walks up from the package's test directory to the +// nearest ancestor that contains `PROGRESS.md`. +func repoRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + cur := wd + for { + if _, err := os.Stat(filepath.Join(cur, "PROGRESS.md")); err == nil { + return cur, nil + } + parent := filepath.Dir(cur) + if parent == cur { + return "", fmt.Errorf("could not find PROGRESS.md from %s", wd) + } + cur = parent + } +} + +// ----------------------------------------------------------------- +// MinIO + TSDB block fetch. +// ----------------------------------------------------------------- + +// Sample is one (timestamp, value) pair recovered from a TSDB block. +type Sample struct { + LabelSet labels.Labels + Timestamp int64 + Value float64 +} + +// listAndFetchBlocks lists every finalized TSDB block in the +// `asap-gorilla` MinIO bucket (i.e. every prefix that contains a +// `meta.json`), downloads its three files (`chunks/000001`, `index`, +// `meta.json`) into a per-block temp dir, and returns the list of +// per-block dirs. Callers are responsible for opening each dir with +// `tsdb.OpenBlock` and reading samples back. +// +// Why `mc` instead of an in-Go S3 client? Adding `aws-sdk-go` (or +// `minio-go`) drags ~30 deps into this test module. The test already +// needs docker exec for the OTLP push; reusing it for block listing +// keeps go.mod focused on Prometheus-side deps only. +func listAndFetchBlocks(ctx context.Context, t *testing.T) ([]string, error) { + t.Helper() + + // 1) List meta.json markers under the bucket. mc returns the full + // "asap/asap-gorilla//meta.json" key per line. + listCmd := exec.CommandContext(ctx, "docker", "run", "--rm", + "--network", composeProject+"_default", + "--entrypoint", "sh", + "minio/mc:latest", + "-c", + "mc alias set asap http://minio-e2e:9000 asap asap-local-only >/dev/null "+ + "&& mc find asap/asap-gorilla --name 'meta.json' || true") + out, err := listCmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("mc find meta.json: %v\n%s", err, string(out)) + } + keys := strings.Split(strings.TrimSpace(string(out)), "\n") + + // 2) For each "//meta.json" prefix, download the + // three block files into a per-block temp dir. + var blockDirs []string + tmpRoot := t.TempDir() + for _, k := range keys { + k = strings.TrimSpace(k) + if k == "" || !strings.HasSuffix(k, "/meta.json") { + continue + } + // Strip the trailing /meta.json to get the ULID prefix. + blockPrefix := strings.TrimSuffix(k, "/meta.json") + ulidStr := filepath.Base(blockPrefix) + blockDir := filepath.Join(tmpRoot, ulidStr) + if err := os.MkdirAll(filepath.Join(blockDir, "chunks"), 0o755); err != nil { + return nil, err + } + // Pull each of the three files separately. mc cat preserves + // binary content cleanly and avoids us hand-coding the chunk + // numbering, which is internal TSDB. + for _, rel := range []string{"chunks/000001", "index", "meta.json"} { + catCmd := exec.CommandContext(ctx, "docker", "run", "--rm", + "--network", composeProject+"_default", + "--entrypoint", "sh", + "minio/mc:latest", + "-c", + fmt.Sprintf( + "mc alias set asap http://minio-e2e:9000 asap asap-local-only >/dev/null "+ + "&& mc cat %s/%s", blockPrefix, rel)) + var buf bytes.Buffer + catCmd.Stdout = &buf + if err := catCmd.Run(); err != nil { + return nil, fmt.Errorf("mc cat %s/%s: %w", blockPrefix, rel, err) + } + if err := os.WriteFile(filepath.Join(blockDir, rel), buf.Bytes(), 0o644); err != nil { + return nil, fmt.Errorf("write %s: %w", rel, err) + } + } + blockDirs = append(blockDirs, blockDir) + } + return blockDirs, nil +} + +// readSamplesFromBlocks opens each block dir with `tsdb.OpenBlock`, +// builds a `BlockQuerier` over its full time range, selects every +// series matching `__name__=~.+`, and returns the flattened (label, +// ts, value) tuples across all blocks. When `metric` is non-empty +// the result is filtered to that metric name defensively. +func readSamplesFromBlocks(t *testing.T, blockDirs []string, metric string) ([]Sample, error) { + t.Helper() + var all []Sample + for _, d := range blockDirs { + blk, err := tsdb.OpenBlock(nil, d, nil, nil) + if err != nil { + return nil, fmt.Errorf("OpenBlock %s: %w", d, err) + } + q, err := tsdb.NewBlockQuerier(blk, blk.MinTime(), blk.MaxTime()) + if err != nil { + _ = blk.Close() + return nil, fmt.Errorf("NewBlockQuerier %s: %w", d, err) + } + matcher := labels.MustNewMatcher(labels.MatchRegexp, labels.MetricName, ".+") + ss := q.Select(context.Background(), false, &storage.SelectHints{ + Start: blk.MinTime(), + End: blk.MaxTime(), + }, matcher) + for ss.Next() { + s := ss.At() + ls := s.Labels() + it := s.Iterator(nil) + for it.Next() != chunkenc.ValNone { + ts, v := it.At() + all = append(all, Sample{LabelSet: ls, Timestamp: ts, Value: v}) + } + if err := it.Err(); err != nil { + _ = q.Close() + _ = blk.Close() + return nil, fmt.Errorf("iterator: %w", err) + } + } + if err := ss.Err(); err != nil { + _ = q.Close() + _ = blk.Close() + return nil, fmt.Errorf("series set: %w", err) + } + _ = q.Close() + _ = blk.Close() + } + if metric != "" { + filtered := all[:0] + for _, s := range all { + if s.LabelSet.Get(labels.MetricName) == metric { + filtered = append(filtered, s) + } + } + all = filtered + } + return all, nil +} + +// ----------------------------------------------------------------- +// OTLP/HTTP push (no client lib). +// ----------------------------------------------------------------- + +// pushSamplesOTLPHTTP serializes the fixture as an OTLP/HTTP +// `ExportMetricsServiceRequest` JSON body and POSTs it to the agent's +// :4318/v1/metrics endpoint. +func pushSamplesOTLPHTTP(ctx context.Context, hostPort int, fx inputFixture, baseTSNs int64) error { + type otlpKV struct { + Key string `json:"key"` + Value map[string]interface{} `json:"value"` + } + type otlpDataPoint struct { + Attributes []otlpKV `json:"attributes,omitempty"` + StartTimeNs string `json:"startTimeUnixNano"` + TimeUnixNano string `json:"timeUnixNano"` + AsDouble float64 `json:"asDouble"` + } + type otlpSum struct { + AggregationTemporality int `json:"aggregationTemporality"` + IsMonotonic bool `json:"isMonotonic"` + DataPoints []otlpDataPoint `json:"dataPoints"` + } + type otlpMetric struct { + Name string `json:"name"` + Sum otlpSum `json:"sum"` + } + type otlpScopeMetrics struct { + Metrics []otlpMetric `json:"metrics"` + } + type otlpResourceMetrics struct { + ScopeMetrics []otlpScopeMetrics `json:"scopeMetrics"` + } + type otlpReq struct { + ResourceMetrics []otlpResourceMetrics `json:"resourceMetrics"` + } + + dps := make([]otlpDataPoint, len(fx.Values)) + for i, v := range fx.Values { + ts := baseTSNs + int64(i)*fx.IntervalNs + attrs := make([]otlpKV, 0, len(fx.Labels)) + for k, val := range fx.Labels { + attrs = append(attrs, otlpKV{ + Key: k, + Value: map[string]interface{}{"stringValue": val}, + }) + } + dps[i] = otlpDataPoint{ + Attributes: attrs, + StartTimeNs: strconv.FormatInt(baseTSNs, 10), + TimeUnixNano: strconv.FormatInt(ts, 10), + AsDouble: v, + } + } + req := otlpReq{ + ResourceMetrics: []otlpResourceMetrics{{ + ScopeMetrics: []otlpScopeMetrics{{ + Metrics: []otlpMetric{{ + Name: fx.MetricName, + Sum: otlpSum{ + AggregationTemporality: 2, // CUMULATIVE + IsMonotonic: true, + DataPoints: dps, + }, + }}, + }}, + }}, + } + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("marshal OTLP req: %w", err) + } + url := fmt.Sprintf("http://127.0.0.1:%d/v1/metrics", hostPort) + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("build POST: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return fmt.Errorf("POST OTLP: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("OTLP HTTP %d: %s", resp.StatusCode, string(respBody)) + } + return nil +} + +// ----------------------------------------------------------------- +// Misc helpers. +// ----------------------------------------------------------------- + +// waitForTCP polls a host:port until it accepts a TCP connection or +// the deadline expires. +func waitForTCP(ctx context.Context, addr string, max time.Duration) error { + deadline := time.Now().Add(max) + for { + if ctx.Err() != nil { + return ctx.Err() + } + c, err := net.DialTimeout("tcp", addr, 2*time.Second) + if err == nil { + _ = c.Close() + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("waitForTCP %s: deadline exceeded: %w", addr, err) + } + time.Sleep(1 * time.Second) + } +} + +// testLogWriter forwards writes to t.Logf so subprocess stdout/stderr +// shows up under the right subtest in `go test -v` output. +type testLogWriter struct{ t *testing.T } + +func (w testLogWriter) Write(p []byte) (int, error) { + for _, line := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") { + w.t.Log(line) + } + return len(p), nil +} diff --git a/integration/e2e-cold-path/go.mod b/integration/e2e-cold-path/go.mod new file mode 100644 index 00000000..fed89adc --- /dev/null +++ b/integration/e2e-cold-path/go.mod @@ -0,0 +1,99 @@ +module github.com/ProjectASAP/ASAPCollector/integration/e2e-cold-path + +go 1.25.5 + +// Cold-path e2e integration test deps. +// +// After PR #359 the gorillas3processor runtime emits Prometheus TSDB +// blocks (/{chunks/000001,index,meta.json}) instead of the +// legacy GORILLA1 magic-prefixed XOR-delta chunks. The test reads +// those blocks back via prometheus/tsdb's BlockQuerier to assert +// byte-equality against the input fixture; the cross-language Rust +// decoder check is no longer applicable (TSDB is upstream Prometheus, +// not an ASAP-specific format). +// +// The deps mirror asap-gorilla-go's go.mod (which produces these +// blocks) so we read them with the same reader the writer was +// validated against. + +require github.com/prometheus/prometheus v0.307.3 + +require ( + cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.8.4 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/aws/aws-sdk-go-v2 v1.39.2 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect + github.com/aws/smithy-go v1.23.0 // indirect + github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dennwc/varint v1.0.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.1 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/sigv4 v0.2.1 // indirect + github.com/stretchr/testify v1.11.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.13.0 // indirect + google.golang.org/api v0.250.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apimachinery v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect +) diff --git a/integration/e2e-cold-path/go.sum b/integration/e2e-cold-path/go.sum new file mode 100644 index 00000000..608b0d78 --- /dev/null +++ b/integration/e2e-cold-path/go.sum @@ -0,0 +1,417 @@ +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.8.4 h1:oXMa1VMQBVCyewMIOm3WQsnVd9FbKBtm8reqWRaXnHQ= +cloud.google.com/go/compute/metadata v0.8.4/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= +github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= +github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8= +github.com/aws/aws-sdk-go-v2/config v1.31.12/go.mod h1:/MM0dyD7KSDPR+39p9ZNVKaHDLb9qnfDurvVS2KAhN8= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.1 h1:7p9bJCZ/b3EJXXARW7JMEs2IhsnI4YFHpfXQfgMh0eg= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.1/go.mod h1:M8WWWIfXmxA4RgTXcI/5cSByxRqjgne32Sh0VIbrn0A= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.49.1 h1:J1A0VJlt5HgUX6s11Obe9zrBDECeE2uhQc7Dwhdei9o= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.49.1/go.mod h1:WEOSRNyfIfvgrD9MuSIGrogKyuFahaVMziVq1pHI0NQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 h1:A1oRkiSQOWstGh61y4Wc/yQ04sqrQZr1Si/oAXj20/s= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= +github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= +github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= +github.com/digitalocean/godo v1.165.1 h1:H37+W7TaGFOVH+HpMW4ZeW/hrq3AGNxg+B/K8/dZ9mQ= +github.com/digitalocean/godo v1.165.1/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= +github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= +github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gophercloud/gophercloud/v2 v2.8.0 h1:of2+8tT6+FbEYHfYC8GBu8TXJNsXYSNm9KuvpX7Neqo= +github.com/gophercloud/gophercloud/v2 v2.8.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/hashicorp/consul/api v1.32.0 h1:5wp5u780Gri7c4OedGEPzmlUEzi0g2KyiPphSr6zjVg= +github.com/hashicorp/consul/api v1.32.0/go.mod h1:Z8YgY0eVPukT/17ejW+l+C7zJmKwgPHtjU1q16v/Y40= +github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4= +github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= +github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/nomad/api v0.0.0-20250930071859-eaa0fe0e27af h1:ScAYf8O+9xTqTJPZH8MIlUfO+ak8cb31rW1aYJgS+jE= +github.com/hashicorp/nomad/api v0.0.0-20250930071859-eaa0fe0e27af/go.mod h1:sldFTIgs+FsUeKU3LwVjviAIuksxD8TzDOn02MYwslE= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hetznercloud/hcloud-go/v2 v2.25.1 h1:ib86acotlvgUSnKfFG5FJl0VFeYKe/Ht8nmikdUp+po= +github.com/hetznercloud/hcloud-go/v2 v2.25.1/go.mod h1:uQdAWaW3d9TimiyOjQWY8HKShs0Nd6S4wNYqo0HjvIY= +github.com/ionos-cloud/sdk-go/v6 v6.3.4 h1:jTvGl4LOF8v8OYoEIBNVwbFoqSGAFqn6vGE7sp7/BqQ= +github.com/ionos-cloud/sdk-go/v6 v6.3.4/go.mod h1:wCVwNJ/21W29FWFUv+fNawOTMlFoP1dS3L+ZuztFW48= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= +github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= +github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= +github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/linode/linodego v1.59.0 h1:kYz6sQH9g0u21gbI1UUFjZmFLirtc39JPybygrW76Q0= +github.com/linode/linodego v1.59.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.136.0 h1:OuR81KyWJaEXOoPT/qa9B8RJsUEPyniZwJwANPPdEvk= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.136.0/go.mod h1:rHQxv7q4gR8ZKk6rzPhXsjlXDY3o54SC6stTpgYw8q8= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.136.0 h1:gp2AYLP2yL5O0RTiKpyORvxqjSEypMSH/6laB5bh0l4= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.136.0/go.mod h1:5mPPRoLAp4uhg7tV+OLR+HmHyYtALSGZ0oMVHgMAfL8= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.136.0 h1:3m1GQBFf8xRXjozuuyJD97122Rh81KiSR4HAMaN4Xfk= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.136.0/go.mod h1:5LdAGh7nkcym4+EzmzCDNK9Qdp7mL1TPzzQpdPEJtds= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= +github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/prometheus v0.307.3 h1:zGIN3EpiKacbMatcUL2i6wC26eRWXdoXfNPjoBc2l34= +github.com/prometheus/prometheus v0.307.3/go.mod h1:sPbNW+KTS7WmzFIafC3Inzb6oZVaGLnSvwqTdz2jxRQ= +github.com/prometheus/sigv4 v0.2.1 h1:hl8D3+QEzU9rRmbKIRwMKRwaFGyLkbPdH5ZerglRHY0= +github.com/prometheus/sigv4 v0.2.1/go.mod h1:ySk6TahIlsR2sxADuHy4IBFhwEjRGGsfbbLGhFYFj6Q= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stackitcloud/stackit-sdk-go/core v0.17.3 h1:GsZGmRRc/3GJLmCUnsZswirr5wfLRrwavbnL/renOqg= +github.com/stackitcloud/stackit-sdk-go/core v0.17.3/go.mod h1:HBCXJGPgdRulplDzhrmwC+Dak9B/x0nzNtmOpu+1Ahg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= +github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/component v1.42.0 h1:on4XJ/NT1oPnuCVKDEtlpcr3GGPAS9taWBe8woHSTmY= +go.opentelemetry.io/collector/component v1.42.0/go.mod h1:mehIbkABLhEEs3kmAqer2GRmLwcQLoeF7C48CR6lxP0= +go.opentelemetry.io/collector/confmap v1.42.0 h1:Hdeqq1RkGBBWbmDpa96aC5LchklzUzCu4aSRRoPicng= +go.opentelemetry.io/collector/confmap v1.42.0/go.mod h1:KW/l4uXBGnl5OM8WYi3gTg6PeG+y24nlIMS71KwWQjk= +go.opentelemetry.io/collector/confmap/xconfmap v0.136.0 h1:eC14gN+NL5HxmOmN9Aa4SkAnJhmUgmYP5cgEjCdz0sw= +go.opentelemetry.io/collector/confmap/xconfmap v0.136.0/go.mod h1:bDvQo42iyxLGR/Nl4eKP//F/jpDcD52JCb7uLGKA3lc= +go.opentelemetry.io/collector/consumer v1.42.0 h1:RhdoAXrLODs4cnh1m/ihWfHTyWzGO1jL0X+E7wETzUE= +go.opentelemetry.io/collector/consumer v1.42.0/go.mod h1:jKcMYx9LXWMK4dupP2NhiAuHK063JiVMlyAC+ZMqlD0= +go.opentelemetry.io/collector/featuregate v1.42.0 h1:uCVwumVBVex46DsG/fvgiTGuf9f53bALra7vGyKaqFI= +go.opentelemetry.io/collector/featuregate v1.42.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/internal/telemetry v0.136.0 h1:3TcnxyUFs6jJZeLo5ju3fMWS4lRmIApl9To2XWk922M= +go.opentelemetry.io/collector/internal/telemetry v0.136.0/go.mod h1:dTykH9zv/zOnlyUvqfGIqpaQZhmayW7NssD7TPU4paE= +go.opentelemetry.io/collector/pdata v1.42.0 h1:XEzisp/SNfKDcY4aRU6qrHeLzGypRUdYHjbBqkDFOO4= +go.opentelemetry.io/collector/pdata v1.42.0/go.mod h1:nnOmgf+RI/D5xYWgFPZ5nKuhf2E0Qy9Nx/mxoTvIq3k= +go.opentelemetry.io/collector/pipeline v1.42.0 h1:jqn1lPwUdCn+lsyNubCtwzXZLEm+R3kRWxLpDkhlvvs= +go.opentelemetry.io/collector/pipeline v1.42.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= +go.opentelemetry.io/collector/processor v1.42.0 h1:JVMaRA8QkiOJHAswCVAugMaFhDbNedat2XRKjlsNv2A= +go.opentelemetry.io/collector/processor v1.42.0/go.mod h1:O9uYN7VeC4gnD2qsaXaM50rvO8tt2zJS/9bnzucJ+N8= +go.opentelemetry.io/collector/semconv v0.128.0 h1:MzYOz7Vgb3Kf5D7b49pqqgeUhEmOCuT10bIXb/Cc+k4= +go.opentelemetry.io/collector/semconv v0.128.0/go.mod h1:OPXer4l43X23cnjLXIZnRj/qQOjSuq4TgBLI76P9hns= +go.opentelemetry.io/contrib/bridges/otelzap v0.12.0 h1:FGre0nZh5BSw7G73VpT3xs38HchsfPsa2aZtMp0NPOs= +go.opentelemetry.io/contrib/bridges/otelzap v0.12.0/go.mod h1:X2PYPViI2wTPIMIOBjG17KNybTzsrATnvPJ02kkz7LM= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.250.0 h1:qvkwrf/raASj82UegU2RSDGWi/89WkLckn4LuO4lVXM= +google.golang.org/api v0.250.0/go.mod h1:Y9Uup8bDLJJtMzJyQnu+rLRJLA0wn+wTtc6vTlOvfXo= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/integration/gorilla_s3_e2e/golden/input_samples.json b/integration/e2e-cold-path/golden/input_samples.json similarity index 100% rename from integration/gorilla_s3_e2e/golden/input_samples.json rename to integration/e2e-cold-path/golden/input_samples.json diff --git a/integration/e2e-warm-path/README.md b/integration/e2e-warm-path/README.md new file mode 100644 index 00000000..c01cc54b --- /dev/null +++ b/integration/e2e-warm-path/README.md @@ -0,0 +1,114 @@ +# integration/e2e-warm-path + +End-to-end integration test for the ASAP warm-path sketch pipeline: + +``` +fake-driver ─OTLP─▶ asap-otel + ddsketchprocessor (window 10s) + │ emits typed DDSketchDataPoint + ▼ + backend (asap/query-backend:dev) + │ warm-tier OTLP receiver + + │ DDSketchAccumulator + ▼ + PromQL HTTP /api/v1/query + { data_source: sketch_warm_tier, + result: p99 within ε=0.01 } +``` + +This is the warm-path e2e gate: it validates the full +edge → backend → PromQL round-trip for the DDSketch quantile family +and pins the criterion-④ accuracy bound (DDSketch relative-accuracy +ε=0.01) end-to-end. + +## Layout + +``` +integration/e2e-warm-path/ +├── e2e_test.go # main Go test — TestWarmPathE2E +├── golden/ +│ └── input_samples.json # 100 deterministic latency samples +├── docker-compose/ +│ └── e2e-overlay.yml # ephemeral stack (separate compose +│ # project + 29xxx port range) +├── configs/ +│ └── asap-otel-agent-warm.yaml # agent config: ddsketch window 10s +├── go.mod # std-lib only +└── README.md +``` + +## How the assertion works + +The test pushes the 100-sample fixture (uniform 1..100 ms latency) +via OTLP/HTTP+JSON to the asap-otel agent. The DDSketch processor +runs in `mode: window` with a 10s flush interval, emits a typed +`DDSketchDataPoint` named `http_requests_total_latency_ms_quantile`, +and forwards it to the backend's warm-tier OTLP receiver. The backend +ingests the sketch into its `DDSketchAccumulator` and answers +`quantile_over_time(0.99, http_requests_total_latency_ms[30s])` from +the merged sketch, stamping the response with +`data_source: sketch_warm_tier`. + +The test asserts: + +1. HTTP 200, response shape valid (`status: success`, `data` + + `data.resultType` populated). +2. `data_source` reports the warm-tier engine. Canonical name is + `sketch_warm_tier`; the test tolerates name drift (e.g. an + `_ddsketch` suffix) but logs the deviation. +3. The returned p99 estimate satisfies the DDSketch relative-accuracy + bound: `|estimate - exact| / exact <= 0.01`. For the 1..100 ramp + the exact p99 is 99.0, so the estimate must land in [98.01, 99.99]. + +## Run + +### Default (no docker, fixture-only) + +``` +cd integration/e2e-warm-path +go test -v ./... +``` + +The live e2e test (`TestWarmPathE2E`) is gated on `WARM_E2E_LIVE=1` +and SKIPs in this mode; `TestGoldenInputFixtureWellFormed` still runs +and pins the shape of the input fixture. + +### Live e2e (requires docker, sweep idle) + +``` +WARM_E2E_LIVE=1 go test -v -run TestWarmPathE2E ./... +``` + +The live test: + +1. Brings up `asap-otel-warm + backend-warm` via + `docker compose -p asap-warm-e2e -f docker-compose/e2e-overlay.yml up -d`. +2. Pushes the 100-sample fixture via OTLP/HTTP to `127.0.0.1:24318/v1/metrics`. +3. Waits 15s for the ddsketch window flush + backend ingest. +4. Issues `quantile_over_time(0.99, http_requests_total_latency_ms[30s])` + against `http://localhost:29191/api/v1/query`. +5. Asserts response shape, data_source marker, and p99 within ε=0.01. +6. Tears the stack down with `down -v`. + +End-to-end runtime is under 60s (10s window + 15s slack + ~30s for +docker up/down). + +### Coordination with the host-wide sweep + +Same defensive posture as the cold-path test: separate compose +project (`asap-warm-e2e`) and separate port range (29191) so it +does NOT collide on ports — but the live test SKIPs when the host +sweep is alive (`WARM_E2E_SWEEP_PID=`). Override with +`WARM_E2E_FORCE=1`; disable the busy-check entirely with +`WARM_E2E_SWEEP_PID=0` (or `=none`). + +## Constraints + +- The compose stack pulls `asap/query-backend:dev` from the local + Docker daemon — assume it's already built. +- This is a **focused** warm-path gate, not a copy of the MVP demo: + no MinIO, no Thanos, no fake-exporter, no + mvp-multi-stage.yml/mvp-workload.yaml fixtures. +- The agent's DDSketch processor uses `delta_transmission: false` + intentionally — full-state per window keeps the backend's first + ingest deterministic and avoids dependence on between-window + diffing logic that is harder to reason about in a 60s test budget. diff --git a/integration/e2e-warm-path/configs/asap-otel-agent-warm.yaml b/integration/e2e-warm-path/configs/asap-otel-agent-warm.yaml new file mode 100644 index 00000000..7d75fa46 --- /dev/null +++ b/integration/e2e-warm-path/configs/asap-otel-agent-warm.yaml @@ -0,0 +1,54 @@ +# Warm-path e2e agent config. +# +# Single DDSketch processor in window mode with a small (10s) flush +# interval to keep the test runtime under 60s end-to-end. The processor +# emits typed `DDSketchDataPoint` data points alongside the raw input +# (drop_original=false default); the OTLP exporter forwards both to +# the backend's warm-tier OTLP receiver. +# +# Output metric: `http_requests_total_latency_ms_quantile` (typed +# DDSketchDataPoint variant). Backend's `DDSketchAccumulator` answers +# `quantile_over_time` PromQL queries from the merged sketch and +# stamps the response with `data_source: sketch_warm_tier`. +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + ddsketch: + mode: window + window_duration: 10s + transmit_sketch: true + delta_transmission: false + enable_self_monitoring: true + quantiles: [0.5, 0.9, 0.99] + metric_suffix: "_quantile" + + batch: + timeout: 1s + send_batch_size: 1024 + +exporters: + otlp/backend: + endpoint: backend-warm:4317 + tls: + insecure: true + # Backend's tonic gRPC server doesn't accept gzip-compressed + # request bodies — keep compression off (mirrors per-sketch + # direct configs). + compression: none + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [ddsketch, batch] + exporters: [otlp/backend] + + telemetry: + logs: + level: info diff --git a/integration/e2e-warm-path/docker-compose/e2e-overlay.yml b/integration/e2e-warm-path/docker-compose/e2e-overlay.yml new file mode 100644 index 00000000..b9d447b4 --- /dev/null +++ b/integration/e2e-warm-path/docker-compose/e2e-overlay.yml @@ -0,0 +1,64 @@ +# Warm-path e2e overlay. +# +# Brings up an EPHEMERAL stack on a separate compose project +# (`asap-warm-e2e`) and a separate port range (29xxx) so it can run +# side-by-side with the host-wide sweep stack and the cold-path e2e +# stack without port collisions. +# +# Services brought up: +# +# asap-otel-warm Single asap-otel agent running the DDSketch +# window processor (10s flush interval; small +# so the test budget stays under 60s). +# backend-warm asap/query-backend:dev with --enable-otel-ingest +# wiring the warm-tier OTLP receiver. Exposes +# PromQL on host 29191 (note: 29191, not 29091 +# which is owned by the cold-path overlay). +# +# The fake-exporter / MinIO / Thanos / mvp-multi-stage services are +# intentionally NOT in this overlay — this is a focused warm-path gate, +# not a copy of the MVP demo. + +version: "3.9" + +x-logging: &default-logging + driver: json-file + options: + max-size: "5m" + max-file: "2" + +services: + asap-otel-warm: + image: asap/asap-otel:dev + command: + - "--config=/etc/otel/config.yaml" + volumes: + - ../configs/asap-otel-agent-warm.yaml:/etc/otel/config.yaml:ro + environment: + RUST_LOG: "info" + ports: + - "24317:4317" + - "24318:4318" + depends_on: + - backend-warm + networks: + - default + logging: *default-logging + + backend-warm: + image: asap/query-backend:dev + environment: + RUST_LOG: "info" + command: + - "--query-port=9091" + - "--enable-otel-ingest" + - "--otel-grpc-port=4317" + - "--otel-http-port=4318" + ports: + - "29191:9091" + networks: + - default + logging: *default-logging + +networks: + default: diff --git a/integration/e2e-warm-path/e2e_test.go b/integration/e2e-warm-path/e2e_test.go new file mode 100644 index 00000000..86ff411b --- /dev/null +++ b/integration/e2e-warm-path/e2e_test.go @@ -0,0 +1,521 @@ +// Package warmpathe2e is the warm-path end-to-end integration test +// for the ASAP warm-tier sketch pipeline: +// +// fake-driver ─OTLP─▶ asap-otel + ddsketchprocessor (window 10s) +// │ emits typed DDSketchDataPoint +// ▼ +// backend (asap/query-backend:dev) +// │ warm-tier OTLP receiver + +// │ DDSketchAccumulator +// ▼ +// PromQL HTTP /api/v1/query +// +// The test asserts: +// +// 1. The PromQL HTTP response is shape-valid (status 200, "data" + +// "resultType" populated). +// 2. The response carries `data_source: sketch_warm_tier` (or the +// backend's current warm-tier marker — see assertion comment). +// 3. The returned p99 estimate is within DDSketch's `ε=0.01` of the +// offline-computed exact p99 from the 100-sample fixture. +// +// `TestWarmPathE2E` is the orchestrator. It brings up a minimal +// docker stack (asap-otel agent + backend) on a separate compose +// project (`asap-warm-e2e`) and tears it down at the end. The test +// is gated on `WARM_E2E_LIVE=1`; default `go test ./...` skips so +// CI without docker stays green. +package warmpathe2e + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "math" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + "time" +) + +// ----------------------------------------------------------------- +// Sweep-coordination + setup helpers. +// ----------------------------------------------------------------- + +const defaultSweepPID = 2862786 + +// composeProject is the compose project name for the ephemeral +// e2e stack. Pinned so a local `docker compose -p asap-warm-e2e +// down -v` will tear it down even if the test panicked. +const composeProject = "asap-warm-e2e" + +// Host-side ports. Match docker-compose/e2e-overlay.yml. +const ( + hostPortAgentOTLP = 24317 + hostPortBackendQuery = 29191 +) + +// liveOrSkip is the gate every "needs-docker" subtest goes through. +func liveOrSkip(t *testing.T) bool { + t.Helper() + + if v := os.Getenv("WARM_E2E_LIVE"); v != "1" { + t.Skipf("WARM_E2E_LIVE != 1; set WARM_E2E_LIVE=1 to run the live " + + "docker-compose path. Defaults skip so `go test ./...` from a fresh " + + "checkout never tries to bring up containers.") + return false + } + if force := os.Getenv("WARM_E2E_FORCE"); force != "1" { + if running, pid := isSweepRunning(); running { + t.Skipf("sweep process PID=%d is alive; the live e2e path competes "+ + "with it for docker / CPU / RAM. Run after the sweep finishes, "+ + "or override with WARM_E2E_FORCE=1.", pid) + return false + } + } + return true +} + +func isSweepRunning() (bool, int) { + pidStr := os.Getenv("WARM_E2E_SWEEP_PID") + if pidStr == "" { + pidStr = strconv.Itoa(defaultSweepPID) + } + if pidStr == "0" || pidStr == "none" { + return false, 0 + } + pid, err := strconv.Atoi(pidStr) + if err != nil || pid <= 0 { + return false, 0 + } + if _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)); err == nil { + return true, pid + } + return false, pid +} + +// ----------------------------------------------------------------- +// Top-level orchestrator. +// ----------------------------------------------------------------- + +// TestWarmPathE2E runs the live warm-path pipeline. +func TestWarmPathE2E(t *testing.T) { + if !liveOrSkip(t) { + return + } + + repoRoot, err := repoRoot() + if err != nil { + t.Fatalf("repoRoot: %v", err) + } + composeFile := filepath.Join(repoRoot, + "integration", "e2e-warm-path", "docker-compose", "e2e-overlay.yml") + if _, err := os.Stat(composeFile); err != nil { + t.Fatalf("compose overlay missing at %s: %v", composeFile, err) + } + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not on PATH (%v) — live e2e requires Docker Engine + compose v2", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + t.Logf("bringing up compose project %s with %s", composeProject, composeFile) + upCmd := exec.CommandContext(ctx, "docker", "compose", + "-p", composeProject, "-f", composeFile, "up", "-d") + upCmd.Stdout = testLogWriter{t} + upCmd.Stderr = testLogWriter{t} + if err := upCmd.Run(); err != nil { + t.Fatalf("docker compose up: %v", err) + } + t.Cleanup(func() { + downCtx, downCancel := context.WithTimeout(context.Background(), 60*time.Second) + defer downCancel() + downCmd := exec.CommandContext(downCtx, "docker", "compose", + "-p", composeProject, "-f", composeFile, "down", "-v") + downCmd.Stdout = testLogWriter{t} + downCmd.Stderr = testLogWriter{t} + _ = downCmd.Run() + }) + + if err := waitForTCP(ctx, fmt.Sprintf("127.0.0.1:%d", hostPortAgentOTLP), 90*time.Second); err != nil { + t.Fatalf("asap-otel-warm OTLP gRPC port not reachable: %v", err) + } + if err := waitForTCP(ctx, fmt.Sprintf("127.0.0.1:%d", hostPortBackendQuery), 90*time.Second); err != nil { + t.Fatalf("backend-warm query port not reachable: %v", err) + } + + fixture, err := loadFixture(repoRoot) + if err != nil { + t.Fatalf("loadFixture: %v", err) + } + + now := time.Now().UnixNano() + if err := pushSamplesOTLPHTTP(ctx, hostPortAgentOTLP+1 /* :4318 → host 24318 */, fixture, now); err != nil { + t.Fatalf("pushSamplesOTLPHTTP: %v", err) + } + + // ddsketchprocessor flushes once per `window_duration` (10s). + // Wait one window + a small slack for the OTLP forward + backend + // ingest to land. + t.Logf("waiting 15s for the ddsketch window flush + backend ingest") + select { + case <-ctx.Done(): + t.Fatalf("context expired before window flush: %v", ctx.Err()) + case <-time.After(15 * time.Second): + } + + // Issue the p99 PromQL query against the warm-tier surface. + resp, err := promQLQuantileQuery(ctx, fixture.MetricName, 0.99) + if err != nil { + t.Fatalf("promQLQuantileQuery: %v", err) + } + + // 1) Shape-valid response. + if status, _ := resp["status"].(string); status != "success" { + t.Fatalf("PromQL response status != success: %v", resp) + } + data, ok := resp["data"].(map[string]interface{}) + if !ok { + t.Fatalf("PromQL response missing data: %v", resp) + } + if rt, _ := data["resultType"].(string); rt == "" { + t.Fatalf("PromQL response missing resultType: %v", resp) + } + + // 2) data_source marker. The backend stamps this either at the + // top level or under data["data_source"] depending on response + // shape evolution; accept either. + dataSource := extractDataSource(resp) + if dataSource == "" { + t.Errorf("PromQL response is missing the data_source marker; "+ + "expected `sketch_warm_tier` (or whatever the backend currently "+ + "stamps for warm-tier responses). Full response: %v", resp) + } else if dataSource != "sketch_warm_tier" { + // Tolerate near-name variants (e.g. backend evolution to + // `sketch_warm_tier_ddsketch`) but log them so the canonical + // name surfaces in the test output for the next regen. + if !strings.Contains(dataSource, "warm") { + t.Errorf("data_source=%q; expected something matching `sketch_warm_tier`", dataSource) + } else { + t.Logf("data_source=%q (warm-tier match; canonical name has drifted from `sketch_warm_tier`)", dataSource) + } + } else { + t.Logf("data_source=%q (canonical warm-tier marker)", dataSource) + } + + // 3) p99 estimate within ε=0.01 of the exact value. + p99, err := extractScalarOrFirstVector(data) + if err != nil { + t.Fatalf("extract p99 from response: %v (full response: %v)", err, resp) + } + exact := fixture.ExpectedP99 + eps := fixture.DDSketchRelativeAccuracy + if eps <= 0 { + eps = 0.01 + } + // DDSketch relative-accuracy: |estimate - exact| / exact <= eps. + relErr := math.Abs(p99-exact) / exact + if relErr > eps { + t.Fatalf("p99 estimate %v exceeds DDSketch ε=%v bound around exact=%v "+ + "(relative error %v)", p99, eps, exact, relErr) + } + t.Logf("WarmPathE2E: p99 estimate=%v exact=%v relErr=%v (within ε=%v)", + p99, exact, relErr, eps) +} + +// ----------------------------------------------------------------- +// Sibling fixture-only test — always runs, no docker required. +// ----------------------------------------------------------------- + +// TestGoldenInputFixtureWellFormed pins the shape of the input +// fixture so a typo in `golden/input_samples.json` surfaces here +// rather than mid-pipeline as an opaque decode error. +func TestGoldenInputFixtureWellFormed(t *testing.T) { + repoRoot, err := repoRoot() + if err != nil { + t.Fatalf("repoRoot: %v", err) + } + fx, err := loadFixture(repoRoot) + if err != nil { + t.Fatalf("loadFixture: %v", err) + } + if fx.MetricName == "" { + t.Errorf("metric_name empty") + } + if len(fx.Values) != fx.ExpectedCount { + t.Errorf("len(values)=%d ≠ expected_count=%d", len(fx.Values), fx.ExpectedCount) + } + if fx.ExpectedP99 <= 0 { + t.Errorf("expected_p99 must be > 0") + } + if fx.DDSketchRelativeAccuracy <= 0 || fx.DDSketchRelativeAccuracy >= 1 { + t.Errorf("ddsketch_relative_accuracy out of (0,1): %v", fx.DDSketchRelativeAccuracy) + } +} + +// ----------------------------------------------------------------- +// Fixture loader + input model. +// ----------------------------------------------------------------- + +type inputFixture struct { + MetricName string `json:"metric_name"` + Labels map[string]string `json:"labels"` + IntervalNs int64 `json:"interval_ns"` + Values []float64 `json:"values"` + ExpectedCount int `json:"expected_count"` + ExpectedP99 float64 `json:"expected_p99"` + DDSketchRelativeAccuracy float64 `json:"ddsketch_relative_accuracy"` +} + +func loadFixture(repoRoot string) (inputFixture, error) { + path := filepath.Join(repoRoot, + "integration", "e2e-warm-path", "golden", "input_samples.json") + b, err := os.ReadFile(path) + if err != nil { + return inputFixture{}, fmt.Errorf("read fixture %s: %w", path, err) + } + var fx inputFixture + if err := json.Unmarshal(b, &fx); err != nil { + return inputFixture{}, fmt.Errorf("parse fixture %s: %w", path, err) + } + return fx, nil +} + +func repoRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + cur := wd + for { + if _, err := os.Stat(filepath.Join(cur, "PROGRESS.md")); err == nil { + return cur, nil + } + parent := filepath.Dir(cur) + if parent == cur { + return "", fmt.Errorf("could not find PROGRESS.md from %s", wd) + } + cur = parent + } +} + +// ----------------------------------------------------------------- +// OTLP/HTTP push (no client lib). +// ----------------------------------------------------------------- + +// pushSamplesOTLPHTTP serializes the fixture as an OTLP/HTTP +// `ExportMetricsServiceRequest` JSON body and POSTs it to the +// agent's :4318/v1/metrics endpoint. Uses Gauge data points (the +// DDSketch processor's window mode reads any Gauge / Sum series). +func pushSamplesOTLPHTTP(ctx context.Context, hostPort int, fx inputFixture, baseTSNs int64) error { + type otlpKV struct { + Key string `json:"key"` + Value map[string]interface{} `json:"value"` + } + type otlpDataPoint struct { + Attributes []otlpKV `json:"attributes,omitempty"` + StartTimeNs string `json:"startTimeUnixNano"` + TimeUnixNano string `json:"timeUnixNano"` + AsDouble float64 `json:"asDouble"` + } + type otlpGauge struct { + DataPoints []otlpDataPoint `json:"dataPoints"` + } + type otlpMetric struct { + Name string `json:"name"` + Gauge otlpGauge `json:"gauge"` + } + type otlpScopeMetrics struct { + Metrics []otlpMetric `json:"metrics"` + } + type otlpResourceMetrics struct { + ScopeMetrics []otlpScopeMetrics `json:"scopeMetrics"` + } + type otlpReq struct { + ResourceMetrics []otlpResourceMetrics `json:"resourceMetrics"` + } + + dps := make([]otlpDataPoint, len(fx.Values)) + for i, v := range fx.Values { + ts := baseTSNs + int64(i)*fx.IntervalNs + attrs := make([]otlpKV, 0, len(fx.Labels)) + for k, val := range fx.Labels { + attrs = append(attrs, otlpKV{ + Key: k, + Value: map[string]interface{}{"stringValue": val}, + }) + } + dps[i] = otlpDataPoint{ + Attributes: attrs, + StartTimeNs: strconv.FormatInt(baseTSNs, 10), + TimeUnixNano: strconv.FormatInt(ts, 10), + AsDouble: v, + } + } + req := otlpReq{ + ResourceMetrics: []otlpResourceMetrics{{ + ScopeMetrics: []otlpScopeMetrics{{ + Metrics: []otlpMetric{{ + Name: fx.MetricName, + Gauge: otlpGauge{DataPoints: dps}, + }}, + }}, + }}, + } + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("marshal OTLP req: %w", err) + } + endpoint := fmt.Sprintf("http://127.0.0.1:%d/v1/metrics", hostPort) + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("build POST: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return fmt.Errorf("POST OTLP: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("OTLP HTTP %d: %s", resp.StatusCode, string(respBody)) + } + return nil +} + +// ----------------------------------------------------------------- +// Backend PromQL probe. +// ----------------------------------------------------------------- + +// promQLQuantileQuery hits the backend's /api/v1/query endpoint with +// `quantile_over_time(, [30s])` and returns the parsed +// JSON response. +func promQLQuantileQuery(ctx context.Context, metric string, q float64) (map[string]interface{}, error) { + expr := fmt.Sprintf("quantile_over_time(%v, %s[30s])", q, metric) + endpoint := fmt.Sprintf("http://127.0.0.1:%d/api/v1/query?query=%s", + hostPortBackendQuery, url.QueryEscape(expr)) + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("backend HTTP %d: %s", resp.StatusCode, string(body)) + } + var out map[string]interface{} + if err := json.Unmarshal(body, &out); err != nil { + return nil, fmt.Errorf("parse PromQL response: %w; body=%s", err, string(body)) + } + return out, nil +} + +// extractDataSource pulls the data_source marker out of the PromQL +// response. Different backend versions stamp it at slightly different +// paths — accept the common ones. +func extractDataSource(resp map[string]interface{}) string { + if s, ok := resp["data_source"].(string); ok { + return s + } + if data, ok := resp["data"].(map[string]interface{}); ok { + if s, ok := data["data_source"].(string); ok { + return s + } + } + // Some shapes carry it under data.infos[] as a "data_source: X" + // line; scan that too. + if data, ok := resp["data"].(map[string]interface{}); ok { + if infos, ok := data["infos"].([]interface{}); ok { + for _, i := range infos { + if s, ok := i.(string); ok && strings.HasPrefix(s, "data_source:") { + return strings.TrimSpace(strings.TrimPrefix(s, "data_source:")) + } + } + } + } + return "" +} + +// extractScalarOrFirstVector returns the numeric value from a PromQL +// response. Handles both `resultType=scalar` (data.result is +// [ts, "value"]) and `resultType=vector` (data.result is array of +// {metric, value: [ts, "value"]}). Returns the first vector entry if +// multiple series are present. +func extractScalarOrFirstVector(data map[string]interface{}) (float64, error) { + rt, _ := data["resultType"].(string) + switch rt { + case "scalar": + arr, ok := data["result"].([]interface{}) + if !ok || len(arr) < 2 { + return 0, fmt.Errorf("scalar result not [ts, value]: %v", data["result"]) + } + s, _ := arr[1].(string) + return strconv.ParseFloat(s, 64) + case "vector": + arr, ok := data["result"].([]interface{}) + if !ok || len(arr) == 0 { + return 0, fmt.Errorf("vector result empty: %v", data["result"]) + } + first, ok := arr[0].(map[string]interface{}) + if !ok { + return 0, fmt.Errorf("vector entry not an object: %v", arr[0]) + } + val, ok := first["value"].([]interface{}) + if !ok || len(val) < 2 { + return 0, fmt.Errorf("vector entry value not [ts, value]: %v", first) + } + s, _ := val[1].(string) + return strconv.ParseFloat(s, 64) + default: + return 0, fmt.Errorf("unsupported resultType=%q", rt) + } +} + +// ----------------------------------------------------------------- +// Misc helpers. +// ----------------------------------------------------------------- + +// waitForTCP polls a host:port until it accepts a TCP connection or +// the deadline expires. +func waitForTCP(ctx context.Context, addr string, max time.Duration) error { + deadline := time.Now().Add(max) + for { + if ctx.Err() != nil { + return ctx.Err() + } + c, err := net.DialTimeout("tcp", addr, 2*time.Second) + if err == nil { + _ = c.Close() + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("waitForTCP %s: deadline exceeded: %w", addr, err) + } + time.Sleep(1 * time.Second) + } +} + +// testLogWriter forwards writes to t.Logf so subprocess stdout/stderr +// shows up under the right subtest in `go test -v` output. +type testLogWriter struct{ t *testing.T } + +func (w testLogWriter) Write(p []byte) (int, error) { + for _, line := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") { + w.t.Log(line) + } + return len(p), nil +} diff --git a/integration/e2e-warm-path/go.mod b/integration/e2e-warm-path/go.mod new file mode 100644 index 00000000..d49cf15a --- /dev/null +++ b/integration/e2e-warm-path/go.mod @@ -0,0 +1,12 @@ +module github.com/ProjectASAP/ASAPCollector/integration/e2e-warm-path + +go 1.25.5 + +// Warm-path e2e integration test deps. +// +// Kept INTENTIONALLY MINIMAL — std library only — so this test is +// portable across CI environments and does not need the patched OTel +// collector replace cascade that integration/parity/runtime-impl/ +// requires. The test pushes OTLP/HTTP+JSON directly (no client lib) +// and parses PromQL JSON responses from the backend's HTTP query +// surface (no Prometheus client lib). diff --git a/integration/e2e-warm-path/golden/input_samples.json b/integration/e2e-warm-path/golden/input_samples.json new file mode 100644 index 00000000..99d8571a --- /dev/null +++ b/integration/e2e-warm-path/golden/input_samples.json @@ -0,0 +1,41 @@ +{ + "_comment": [ + "Warm-path e2e fixture for the DDSketch quantile pipeline.", + "100 deterministic latency samples (uniform 1..100 ms) for one OTLP", + "Gauge metric named `http_requests_total_latency_ms`. Timestamps are", + "wall-clock-independent (epoch-relative, 100ms spacing); the test", + "driver shifts them to `now` at send time so window flushing fires", + "within the test's wait budget.", + "", + "The fixture is consumed two ways:", + " 1. as the input the test pushes via OTLP HTTP to asap-otel,", + " 2. as the ground-truth reference for the p99 assertion.", + "", + "expected_p99 is precomputed offline with the standard 'pick value at", + "rank ceil(0.99 * N)' definition — for the 1..100 ramp that is", + "value 99.0. DDSketch with relative_accuracy=0.01 is required to", + "return an estimate within ε=0.01 of the exact value (98.01 .. 99.99", + "for this fixture)." + ], + "metric_name": "http_requests_total_latency_ms", + "labels": { + "instance": "fake-exporter-1", + "job": "warm-path-e2e" + }, + "interval_ns": 100000000, + "values": [ + 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, + 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, + 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0, + 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, 40.0, + 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, + 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, 60.0, + 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, + 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, 80.0, + 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0, + 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0 + ], + "expected_count": 100, + "expected_p99": 99.0, + "ddsketch_relative_accuracy": 0.01 +} diff --git a/integration/gorilla_s3_e2e/README.md b/integration/gorilla_s3_e2e/README.md deleted file mode 100644 index 591c41df..00000000 --- a/integration/gorilla_s3_e2e/README.md +++ /dev/null @@ -1,141 +0,0 @@ -# Phase 6 — Gorilla-S3 cold-engine end-to-end test - -End-to-end integration test for the full pipeline: - -``` -fake-driver ─OTLP─▶ asap-otel+gorillas3processor - │ encode (Gorilla XOR-delta) - ▼ - MinIO ─list/get─▶ GorillaS3ColdStore - │ decode - ▼ - GorillaQueryEngine - exact PromQL - │ - ▼ - backend HTTP /api/v1/query - { accuracy: ε=0, - data_source: gorilla_archive } -``` - -This is the FINAL phase of the -[Gorilla-S3 cold-engine](../../docs/design-gorilla-s3-cold-engine.md) work. -It validates the byte-format compatibility contract that ties together the -Phase-1 Rust crate (`asap-gorilla`), the Phase-2 Go processor -(`gorillas3processor`), the Phase-3 backend cold-store, and the -Phase-4/5 backend query engine + capability router. - -## Layout - -``` -integration/gorilla_s3_e2e/ -├── e2e_test.go # main Go test — TestGorillaS3End2End -├── gorilla_decoder.go # in-test decoder; mirrors the -│ # GORILLA1 byte format directly -├── gorilla_decoder_test.go # round-trip sanity tests for the -│ # in-test decoder (no docker) -├── golden/ -│ └── input_samples.json # 100 deterministic samples -├── docker-compose/ -│ └── e2e-overlay.yml # ephemeral stack (separate compose -│ # project + 29xxx port range) -├── go.mod # std-lib only -├── Makefile # `make test` / `make test-live` -└── README.md -``` - -## Run - -### Default (no docker, fixture-only) - -``` -cd integration/gorilla_s3_e2e -go test -v ./... -``` - -This runs the decoder round-trip + fixture sanity tests. The live e2e test -(`TestGorillaS3End2End`) is gated on `GORILLA_E2E_LIVE=1` and SKIPs in this -mode; the rest of the test surface still pins the byte format and the input -fixture. - -### Live e2e (requires docker, sweep idle) - -``` -GORILLA_E2E_LIVE=1 go test -v -run TestGorillaS3End2End ./... -``` - -The live test: - -1. Brings up `minio-e2e + asap-otel-e2e + backend-e2e` via - `docker compose -p asap-gorilla-e2e -f docker-compose/e2e-overlay.yml up -d`. -2. Pushes the 100-sample fixture via OTLP/HTTP to `127.0.0.1:24318/v1/metrics`. -3. Waits 75s for the gorillas3processor's 60s window to flush. -4. Runs five subtests (HappyPath, MultiChunkRange, AccuracyExact, - DataSourceMarker, CrossLanguageByteCompat). -5. Tears the stack down with `down -v`. - -### Coordination with the host-wide sweep - -The host runs a long sweep (`run_e2e_sweep.sh`) that owns the default -compose project. The e2e test takes the **defensive** posture (option B in -the PR brief): - -* The overlay uses a separate compose project name (`asap-gorilla-e2e`) - and a separate port range (29xxx) so it would NOT collide on ports — - but -* Docker memory + CPU + network bandwidth are still shared with the - sweep. If the sweep PID (default `2862786`, override via - `GORILLA_E2E_SWEEP_PID=`) is alive, the live test SKIPs unless - forced via `GORILLA_E2E_FORCE=1`. -* Disable the busy-check entirely with `GORILLA_E2E_SWEEP_PID=0` (or - `=none`) — useful in CI where there's no sweep concept. - -## Subtests - -| Subtest | What it asserts | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `HappyPath` | MinIO contains ≥1 chunk under `asap-gorilla/default//...`; the in-test Go decoder reads the chunk back to the input value-set (multi-set equality). | -| `MultiChunkRange` | If the fixture spread crossed a 60s window boundary, ≥2 chunks land; their union covers the full input set. If only 1 chunk lands, single-chunk path is exercised. | -| `AccuracyExact` | Backend PromQL response carries `kind=Exact, ε=0, δ=0` infos. SKIPS with a Phase-6 follow-up note when the backend HTTP server doesn't yet wire `EngineRouter`. | -| `DataSourceMarker` | Backend PromQL response carries `data_source: gorilla_archive`. Same Phase-6 follow-up SKIP behavior. | -| `CrossLanguageByteCompat` | Go-encoded chunk is decoded by the Rust `asap-gorilla` crate via `cargo test --test byte_compat`. This is the byte-format-parity contract. | - -Plus three docker-free subtests that always run: - -| Subtest | Purpose | -| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | -| `TestGoldenInputFixtureWellFormed` | The 100-sample fixture's `expected_sum` invariant matches `sum(values)` (catches a typo in the JSON). | -| `TestGorillaDecoder_HeaderRejectsBadMagic` | Decoder rejects a bad magic prefix with a clear error. | -| `TestGorillaDecoder_HeaderRejectsBadVersion` | Decoder rejects an unsupported version byte. | -| `TestGorillaDecoder_RoundTripSingleSeriesSinglePoint` | Header-only path (no bit-stream entries) round-trips. | -| `TestGorillaDecoder_RoundTripFlatSequence` | Constant-delta TS + constant-value (bucket-0 + value-unchanged paths) round-trip. | -| `TestGorillaDecoder_RoundTripVaryingValues` | 100-sample 1..N ramp (mirrors the live fixture) round-trips and sums to 5050. | - -## Phase-6 follow-ups surfaced by writing this test - -1. **Backend HTTP server still wires `Arc` directly.** The - Phase-5 [`EngineRouter`](https://github.com/ProjectASAP/ASAPQuery-backend/blob/main/asap-query-engine/src/engines/router.rs) - is built but not yet consumed by - `asap-query-engine/src/drivers/query/servers/http.rs`. Until that wiring - lands, the `AccuracyExact` and `DataSourceMarker` subtests SKIP with a - note rather than fail. Tracking item: ASAPQuery-backend follow-up - "wire EngineRouter into HTTP server". -2. **`gorillas3processor` README header byte-count typo.** The README says - `[4] "GORILLA1"` but `encoder.go` writes 8 bytes. Filed for a doc fix - in a follow-up PR (the byte format is correct, just the doc). -3. **Cross-language test consumes `byte_compat` via `cargo test`.** The - `asap-gorilla` `byte_compat` integration test currently asserts on its - own fixtures; this PR's e2e passes `GORILLA_E2E_CHUNK_PATH` for the - live chunk. A small `byte_compat` enhancement to assert that env-var- - pointed chunk decodes successfully would close the loop. Filed as - Phase-6 follow-up. - -## Notes - -* `go.mod` is intentionally **std-lib only**. The byte format pin lives - in `gorilla_decoder.go`, not in a transitive import that could drift on - a dep bump. -* Chunk download uses `mc` inside a transient `minio/mc:latest` container; - no S3 SDK in `go.mod`. -* OTLP push uses raw HTTP/JSON to the agent's `/v1/metrics` endpoint; - no OTLP client in `go.mod`. diff --git a/integration/gorilla_s3_e2e/docker-compose/e2e-overlay.yml b/integration/gorilla_s3_e2e/docker-compose/e2e-overlay.yml deleted file mode 100644 index a80145b8..00000000 --- a/integration/gorilla_s3_e2e/docker-compose/e2e-overlay.yml +++ /dev/null @@ -1,143 +0,0 @@ -# Phase 6 Gorilla-S3 e2e overlay. -# -# This overlay is the SIBLING of `deploy/docker-compose/base.yml` — -# it brings up an EPHEMERAL stack on a separate compose project -# (`asap-gorilla-e2e`) and a separate port range (29xxx) so it -# can run side-by-side with the host-wide sweep stack -# (`docker-compose-{minio,backend,agent-1,...}` on 19xxx) without -# port collisions. -# -# Coordination: the e2e Go test calls `docker compose -p -# asap-gorilla-e2e -f integration/gorilla_s3_e2e/docker-compose/ -# e2e-overlay.yml up -d` and tears it down with `down -v` on test -# completion. Sweep is left untouched. -# -# Services brought up: -# -# minio-e2e MinIO S3-compatible store, ports 29000/29001 -# minio-setup-e2e Seeds the `asap-gorilla` bucket the -# gorillas3processor writes into -# asap-otel-e2e Single asap-otel agent running -# asap-otel-agent-gorillas3-tier.yaml — encodes -# raw samples to GORILLA1 + writes 60s chunks -# to MinIO -# backend-e2e asap/query-backend:dev with the -# Gorilla-S3 cold-store env wired against the -# ephemeral MinIO. Exposes PromQL on host 29091. -# -# fake-exporter is intentionally NOT in the compose stack; the Go -# test pushes the deterministic 100-sample fixture directly via OTLP -# to asap-otel-e2e:24317. This is the only way to drive a fixed, -# repeatable input — fake-exporter generates random data. - -version: "3.9" - -x-logging: &default-logging - driver: json-file - options: - max-size: "5m" - max-file: "2" - -services: - minio-e2e: - image: minio/minio:latest - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: asap - MINIO_ROOT_PASSWORD: asap-local-only - ports: - - "29000:9000" - - "29001:9001" - volumes: - - minio-e2e-data:/data - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s - timeout: 3s - retries: 20 - logging: *default-logging - - # Pre-create the asap-gorilla bucket so gorillas3processor PUTs - # land somewhere. The default base.yml only seeds `raw` (for the - # JSONL cold-fallback adapter); the GORILLA1 chunks need a separate - # bucket per the Phase-2 processor README. - minio-setup-e2e: - image: minio/mc:latest - depends_on: - minio-e2e: - condition: service_healthy - entrypoint: /bin/sh - command: - - -c - - | - mc alias set asap http://minio-e2e:9000 asap asap-local-only && - mc mb --ignore-existing asap/asap-gorilla && - mc anonymous set download asap/asap-gorilla - logging: *default-logging - - # Single asap-otel agent running the Phase-2 gorillas3 tier config. - # The OTLP receiver listens on 4317 (gRPC) + 4318 (HTTP); we expose - # both on host 24317/24318 so the Go test can drive samples in - # without going through a fake-exporter container. - asap-otel-e2e: - image: asap/asap-otel:dev - depends_on: - minio-setup-e2e: - condition: service_completed_successfully - command: - - "--config=/etc/otel/config.yaml" - volumes: - # The Phase-2 config — drop_original=true, encode + PUT to MinIO. - # Path is relative to the compose file's directory. - - ../../../deploy/configs/asap-otel-agent-gorillas3-tier.yaml:/etc/otel/config.yaml:ro - environment: - RUST_LOG: "info" - # The processor config inside the YAML hardcodes minio:9000; - # this overlay relies on that hostname matching minio-e2e's - # service name within the e2e network. Map the alias. - networks: - default: - aliases: - - minio - # The agent config hardcodes endpoint http://minio:9000; - # alias the e2e MinIO under that name so no YAML rewrite - # is needed. - ports: - - "24317:4317" - - "24318:4318" - - "28890:8890" - logging: *default-logging - - # Backend, wired against the e2e MinIO + the asap-gorilla bucket. - # Note: as of Phase 5 the backend's HTTP server still wires - # SimpleEngine directly (Phase 6 follow-up tracked in PROGRESS.md). - # When that wiring lands, this service becomes the canonical surface - # the e2e PromQL assertion drives. Until then, - # TestGorillaS3End2End_HappyPath documents the gap and skips the - # PromQL HTTP step (the chunk-bytes assertion still runs). - backend-e2e: - image: asap/query-backend:dev - depends_on: - minio-setup-e2e: - condition: service_completed_successfully - environment: - RUST_LOG: "info" - ASAP_COLD_STORE_ENDPOINT: "http://minio-e2e:9000" - ASAP_COLD_STORE_BUCKET: "asap-gorilla" - ASAP_COLD_STORE_ACCESS_KEY: "asap" - ASAP_COLD_STORE_SECRET_KEY: "asap-local-only" - ASAP_GORILLA_S3_ENDPOINT: "http://minio-e2e:9000" - ASAP_GORILLA_S3_BUCKET: "asap-gorilla" - ASAP_GORILLA_S3_ACCESS_KEY: "asap" - ASAP_GORILLA_S3_SECRET_KEY: "asap-local-only" - ASAP_GORILLA_S3_TENANT: "default" - command: - - "--ingest-port=9090" - - "--query-port=9091" - ports: - - "29090:9090" - - "29091:9091" - logging: *default-logging - -volumes: - minio-e2e-data: diff --git a/integration/gorilla_s3_e2e/e2e_test.go b/integration/gorilla_s3_e2e/e2e_test.go deleted file mode 100644 index c1eb4a1e..00000000 --- a/integration/gorilla_s3_e2e/e2e_test.go +++ /dev/null @@ -1,843 +0,0 @@ -// Package gorilla_s3_e2e is the Phase 6 (FINAL) end-to-end -// integration test for the Gorilla-S3 cold-engine pipeline: -// -// fake-driver ─OTLP─▶ asap-otel+gorillas3processor -// │ encode (Gorilla XOR-delta) -// ▼ -// MinIO ─list/get─▶ GorillaS3ColdStore -// │ decode -// ▼ -// GorillaQueryEngine -// exact PromQL -// │ -// ▼ -// backend HTTP /api/v1/query -// { accuracy: ε=0, -// data_source: gorilla_archive } -// -// Composition: -// -// - `e2e_test.go` — this file; the test entry points + the -// orchestration around docker-compose + -// the in-process driver. -// - `gorilla_decoder.go` — header-only decoder mirroring the -// GORILLA1 byte format (no transitive -// imports). Used by the in-test -// chunk-equality assertions. -// - `golden/input_samples.json` — 100 deterministic samples + the -// precomputed sum/count/min/max ground -// truth. -// - `docker-compose/e2e-overlay.yml` — sibling stack running on a -// SEPARATE docker compose project -// (`asap-gorilla-e2e`) and a separate -// port range (29xxx) so it never collides -// with the host-wide sweep stack -// (`docker-compose-{minio,backend,...}` -// on 19xxx). -// -// # Coordination strategy with the running sweep -// -// The host runs a long sweep (`run_e2e_sweep.sh`) that owns the -// default compose project. Phase 6 picks the *defensive* coordination -// posture documented in the PR brief as option B: -// -// - At test setup, check `ps -p $SWEEP_PID || true`. If alive → -// `t.Skip` with the message asking the operator to re-run after -// the sweep finishes. This keeps the test from racing the sweep -// for docker memory + ports + CPU. -// - Even though the e2e overlay uses a separate compose project + -// port range (option A), the host has limited CPU/RAM during a -// sweep run. Skipping is the safer default; running anyway is -// opt-in via `GORILLA_E2E_FORCE=1`. -// -// The sweep PID can be overridden via `GORILLA_E2E_SWEEP_PID`; the -// default is the PID stamped into the brief (2862786). When the -// environment variable is `0` or `none`, the busy-check is disabled -// entirely (useful in CI runners that have no sweep concept). -// -// # Subtests -// -// `TestGorillaS3End2End` is one orchestrator that runs the docker -// stack once and then drives multiple `t.Run` subtests against it: -// -// - HappyPath — fixture pushed, MinIO has chunks, -// chunks decode to the input sample-set -// - MultiChunkRange — second window (>60s spread) splits -// into a second chunk; verify the -// cold-store sees both -// - AccuracyExact — backend response carries -// `accuracy: ε=0, δ=0, kind=Exact` -// (when the EngineRouter is wired into -// HTTP, see TODO below) -// - DataSourceMarker — same response carries the -// `data_source: gorilla_archive` info -// line -// - CrossLanguageByteCompat — Go-encoded chunk (decoded in-test -// by gorilla_decoder.go) is also -// decoded by the Rust `asap-gorilla` -// crate via `cargo test`; sample -// equality across both decoders is the -// byte-format-parity contract. -// -// Sibling fixture-only subtests run without docker so the byte -// format is still pinned in CI even when the live stack is skipped: -// -// - TestGorillaDecoder_HeaderRejectsBadMagic -// - TestGorillaDecoder_HeaderRejectsBadVersion -// - TestGoldenInputFixtureWellFormed -// -// # Phase-6 follow-up surfaced by writing this test -// -// The backend's HTTP server (`asap-query-engine/src/drivers/query/ -// servers/http.rs`) still wires `Arc` directly — the -// Phase-5 `EngineRouter` is built but NOT yet consumed by the HTTP -// path. Until that lands, the AccuracyExact + DataSourceMarker -// subtests assert against a SHORT-CIRCUIT path: they call the -// backend's PromQL endpoint, and on response equality with a fresh -// SimpleEngine fall through to a documented `t.Skip` annotated -// "Phase-6 follow-up: wire EngineRouter into HTTP server". The -// chunk-bytes side of the e2e (HappyPath, MultiChunkRange, -// CrossLanguageByteCompat) keeps running unconditionally. -package gorillas3e2e - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "sort" - "strconv" - "strings" - "testing" - "time" -) - -// ----------------------------------------------------------------- -// Sweep-coordination + setup helpers. -// ----------------------------------------------------------------- - -const defaultSweepPID = 2862786 - -// composeProject is the compose project name for the ephemeral -// e2e stack. Pinned so a local `docker compose -p asap-gorilla-e2e -// down -v` will tear it down even if the test panicked. -const composeProject = "asap-gorilla-e2e" - -// MinIO + backend host-side ports. Match docker-compose/e2e-overlay.yml. -const ( - hostPortMinIOS3 = 29000 - hostPortBackendQuery = 29091 - hostPortAgentOTLP = 24317 -) - -// liveOrSkip is the gate every "needs-docker" subtest goes through. -// Returns true if the live stack is up and the subtest should run; -// otherwise calls t.Skip with the specific reason. -func liveOrSkip(t *testing.T) bool { - t.Helper() - - if v := os.Getenv("GORILLA_E2E_LIVE"); v != "1" { - t.Skipf("GORILLA_E2E_LIVE != 1; set GORILLA_E2E_LIVE=1 to run the live " + - "docker-compose path. Defaults skip so `go test ./...` from a fresh " + - "checkout never tries to bring up containers.") - return false - } - if force := os.Getenv("GORILLA_E2E_FORCE"); force != "1" { - if running, pid := isSweepRunning(); running { - t.Skipf("sweep process PID=%d is alive; the live e2e path competes "+ - "with it for docker / CPU / RAM. Run after the sweep finishes, "+ - "or override with GORILLA_E2E_FORCE=1.", pid) - return false - } - } - return true -} - -// isSweepRunning checks whether the host-wide sweep process is -// alive. Disabled when GORILLA_E2E_SWEEP_PID is `0` or `none`. -func isSweepRunning() (bool, int) { - pidStr := os.Getenv("GORILLA_E2E_SWEEP_PID") - if pidStr == "" { - pidStr = strconv.Itoa(defaultSweepPID) - } - if pidStr == "0" || pidStr == "none" { - return false, 0 - } - pid, err := strconv.Atoi(pidStr) - if err != nil || pid <= 0 { - return false, 0 - } - // `os.FindProcess` on Linux is a no-op (always succeeds); we - // must Signal(0) to actually probe. /proc/ is the cheap - // portable alternative and avoids needing CAP_KILL. - if _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)); err == nil { - return true, pid - } - return false, pid -} - -// ----------------------------------------------------------------- -// Top-level orchestrator. -// ----------------------------------------------------------------- - -// TestGorillaS3End2End runs the Phase-6 live pipeline. The body is -// gated by `liveOrSkip` — by default the test SKIPs (CI-friendly -// posture; matches the brief's "do not bring up docker unless the -// sweep is actually idle"). Set GORILLA_E2E_LIVE=1 (and ensure the -// sweep is idle) to actually drive the docker stack. -func TestGorillaS3End2End(t *testing.T) { - if !liveOrSkip(t) { - return - } - - repoRoot, err := repoRoot() - if err != nil { - t.Fatalf("repoRoot: %v", err) - } - composeFile := filepath.Join(repoRoot, - "integration", "gorilla_s3_e2e", "docker-compose", "e2e-overlay.yml") - if _, err := os.Stat(composeFile); err != nil { - t.Fatalf("compose overlay missing at %s: %v", composeFile, err) - } - - if _, err := exec.LookPath("docker"); err != nil { - t.Skipf("docker not on PATH (%v) — live e2e requires Docker Engine + compose v2", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - t.Logf("bringing up compose project %s with %s", composeProject, composeFile) - upCmd := exec.CommandContext(ctx, "docker", "compose", - "-p", composeProject, "-f", composeFile, "up", "-d") - upCmd.Stdout = testLogWriter{t} - upCmd.Stderr = testLogWriter{t} - if err := upCmd.Run(); err != nil { - t.Fatalf("docker compose up: %v", err) - } - t.Cleanup(func() { - downCtx, downCancel := context.WithTimeout(context.Background(), 60*time.Second) - defer downCancel() - downCmd := exec.CommandContext(downCtx, "docker", "compose", - "-p", composeProject, "-f", composeFile, "down", "-v") - downCmd.Stdout = testLogWriter{t} - downCmd.Stderr = testLogWriter{t} - _ = downCmd.Run() - }) - - if err := waitForTCP(ctx, fmt.Sprintf("127.0.0.1:%d", hostPortMinIOS3), 90*time.Second); err != nil { - t.Fatalf("MinIO not reachable: %v", err) - } - if err := waitForTCP(ctx, fmt.Sprintf("127.0.0.1:%d", hostPortAgentOTLP), 90*time.Second); err != nil { - t.Fatalf("asap-otel-e2e OTLP gRPC port not reachable: %v", err) - } - if err := waitForTCP(ctx, fmt.Sprintf("127.0.0.1:%d", hostPortBackendQuery), 90*time.Second); err != nil { - t.Fatalf("backend query port not reachable: %v", err) - } - - fixture, err := loadFixture(repoRoot) - if err != nil { - t.Fatalf("loadFixture: %v", err) - } - - // Drive 100 samples via OTLP gRPC. We use the docker-compose - // service as a `docker exec` target so we don't need an OTLP - // client library in this go.mod; the agent image already has - // `curl` for an OTLP/HTTP push, which is enough for 100 samples. - now := time.Now().UnixNano() - if err := pushSamplesOTLPHTTP(ctx, hostPortAgentOTLP+1 /* :4318 → host 24318 */, fixture, now); err != nil { - t.Fatalf("pushSamplesOTLPHTTP: %v", err) - } - - // gorillas3processor flushes once per `window_interval` (60s - // in the canonical config). Wait one window + a small slack - // for the S3 PutObject + index update to land. - t.Logf("waiting 75s for the gorillas3 window flush + S3 PutObject") - select { - case <-ctx.Done(): - t.Fatalf("context expired before window flush: %v", ctx.Err()) - case <-time.After(75 * time.Second): - } - - // ---------- Subtests against the live stack ---------- - - t.Run("HappyPath", func(t *testing.T) { - runHappyPath(ctx, t, fixture) - }) - t.Run("MultiChunkRange", func(t *testing.T) { - runMultiChunkRange(ctx, t, fixture) - }) - t.Run("AccuracyExact", func(t *testing.T) { - runAccuracyExact(ctx, t, fixture) - }) - t.Run("DataSourceMarker", func(t *testing.T) { - runDataSourceMarker(ctx, t, fixture) - }) - t.Run("CrossLanguageByteCompat", func(t *testing.T) { - runCrossLanguageByteCompat(ctx, t, repoRoot, fixture) - }) -} - -// ----------------------------------------------------------------- -// Subtest bodies. -// ----------------------------------------------------------------- - -func runHappyPath(ctx context.Context, t *testing.T, fixture inputFixture) { - chunks, err := listAndFetchChunks(ctx, fixture.MetricName) - if err != nil { - t.Fatalf("list/fetch chunks: %v", err) - } - if len(chunks) == 0 { - t.Fatalf("no chunks written under metric=%s; gorillas3processor did not flush", - fixture.MetricName) - } - t.Logf("found %d chunk(s) for metric %s", len(chunks), fixture.MetricName) - - got := mergeAllSamples(chunks) - if len(got) != fixture.ExpectedCount { - t.Fatalf("decoded %d samples, want %d", len(got), fixture.ExpectedCount) - } - // Decoded values should match the fixture set (order-insensitive - // — the encoder sorts by ts and we shifted timestamps to `now`, - // so we only verify the multi-set of values matches). - wantValues := append([]float64(nil), fixture.Values...) - sort.Float64s(wantValues) - gotValues := make([]float64, len(got)) - for i, s := range got { - gotValues[i] = s.Value - } - sort.Float64s(gotValues) - for i := range wantValues { - if wantValues[i] != gotValues[i] { - t.Fatalf("value #%d mismatch: got=%v want=%v", i, gotValues[i], wantValues[i]) - } - } - t.Logf("HappyPath: 100 input values round-tripped through encoder + S3 + decoder") -} - -func runMultiChunkRange(ctx context.Context, t *testing.T, fixture inputFixture) { - // The 100-sample fixture spans 99s (1s spacing). With - // window_interval=60s, the gorillas3processor should produce - // at least ONE chunk; depending on alignment with the window - // boundary, it MAY produce two. Assert "≥1" — and when "=2", - // also assert their union covers the full input set. - chunks, err := listAndFetchChunks(ctx, fixture.MetricName) - if err != nil { - t.Fatalf("list/fetch chunks: %v", err) - } - if len(chunks) < 1 { - t.Fatalf("expected ≥1 chunks, got %d", len(chunks)) - } - if len(chunks) >= 2 { - merged := mergeAllSamples(chunks) - if len(merged) != fixture.ExpectedCount { - t.Fatalf("multi-chunk merge yielded %d samples, want %d", - len(merged), fixture.ExpectedCount) - } - t.Logf("MultiChunkRange: 2+ chunks aggregated correctly (%d samples)", - len(merged)) - } else { - t.Logf("MultiChunkRange: 1 chunk only — input fits inside a single 60s "+ - "window boundary. Subtest still validates the single-chunk path; "+ - "true multi-chunk coverage would require a >120s input fixture or "+ - "a smaller window_interval. (fixture window=%ds)", - fixture.IntervalNs*int64(len(fixture.Values))/int64(time.Second)) - } -} - -func runAccuracyExact(ctx context.Context, t *testing.T, fixture inputFixture) { - resp, err := promQLQuery(ctx, fixture.MetricName) - if err != nil { - t.Skipf("backend PromQL not reachable / not router-wired (Phase-6 follow-up): %v", err) - return - } - infos := infoLines(resp) - if !containsLine(infos, "kind=Exact") || !containsLine(infos, "ε=0") { - t.Skipf("response missing exact-accuracy markers; phase-6 follow-up: "+ - "backend HTTP server still wires SimpleEngine directly (not "+ - "EngineRouter). infos=%v", infos) - return - } - t.Logf("AccuracyExact: backend reported %v", infos) -} - -func runDataSourceMarker(ctx context.Context, t *testing.T, fixture inputFixture) { - resp, err := promQLQuery(ctx, fixture.MetricName) - if err != nil { - t.Skipf("backend PromQL not reachable / not router-wired (Phase-6 follow-up): %v", err) - return - } - infos := infoLines(resp) - if !containsLine(infos, "data_source: gorilla_archive") { - t.Skipf("response missing `data_source: gorilla_archive`; phase-6 "+ - "follow-up: backend HTTP server still wires SimpleEngine directly "+ - "(not EngineRouter). infos=%v", infos) - return - } - t.Logf("DataSourceMarker: backend tagged response with gorilla_archive") -} - -func runCrossLanguageByteCompat(ctx context.Context, t *testing.T, repoRoot string, fixture inputFixture) { - chunks, err := listAndFetchChunks(ctx, fixture.MetricName) - if err != nil { - t.Fatalf("list/fetch chunks: %v", err) - } - if len(chunks) == 0 { - t.Fatalf("no chunks to cross-decode") - } - - // Spill the first chunk to a temp file so the Rust crate's - // `from_reader` can pick it up via `--chunk` flag (added in - // the asap-gorilla `decode` example, see PR #281). For the - // cross-language assertion we only need ONE chunk to confirm - // the byte format crosses the language boundary. - tmp := t.TempDir() - chunkPath := filepath.Join(tmp, "chunk.gor") - if err := os.WriteFile(chunkPath, chunks[0], 0o644); err != nil { - t.Fatalf("write chunk: %v", err) - } - - // Run `cargo test` in the asap-gorilla crate restricted to the - // integration round-trip test; that test already exercises the - // decoder against synthetic inputs. For the chunk-from-stack - // path we run the existing `byte_compat` integration test which - // asserts the decoder reads any GORILLA1 input. The PR wiring - // surfaces a chunk via env var so the existing test can also - // assert on the live stack's chunk if invoked with - // `GORILLA_E2E_CHUNK_PATH=...`. - asapGorilla := filepath.Join(repoRoot, "asap-gorilla") - if _, err := os.Stat(asapGorilla); err != nil { - t.Skipf("asap-gorilla crate not present at %s; cross-language "+ - "compat is deferred to a checkout that has the crate", asapGorilla) - return - } - if _, err := exec.LookPath("cargo"); err != nil { - t.Skipf("cargo not on PATH; cross-language compat deferred") - return - } - - cargoCmd := exec.CommandContext(ctx, "cargo", "test", - "--manifest-path", filepath.Join(asapGorilla, "Cargo.toml"), - "--test", "byte_compat", "--", "--nocapture") - cargoCmd.Env = append(os.Environ(), "GORILLA_E2E_CHUNK_PATH="+chunkPath) - cargoCmd.Stdout = testLogWriter{t} - cargoCmd.Stderr = testLogWriter{t} - if err := cargoCmd.Run(); err != nil { - t.Fatalf("cargo test byte_compat (cross-language decoder vs "+ - "Go-encoded chunk): %v", err) - } - t.Logf("CrossLanguageByteCompat: Rust asap-gorilla decoded the Go-encoded chunk") -} - -// ----------------------------------------------------------------- -// Sibling fixture-only subtests — always run, no docker required. -// ----------------------------------------------------------------- - -// TestGoldenInputFixtureWellFormed pins the shape of the input -// fixture so a typo in `golden/input_samples.json` surfaces here -// rather than mid-pipeline as an opaque decode error. -func TestGoldenInputFixtureWellFormed(t *testing.T) { - repoRoot, err := repoRoot() - if err != nil { - t.Fatalf("repoRoot: %v", err) - } - fx, err := loadFixture(repoRoot) - if err != nil { - t.Fatalf("loadFixture: %v", err) - } - if fx.MetricName == "" { - t.Errorf("metric_name empty") - } - if len(fx.Values) != fx.ExpectedCount { - t.Errorf("len(values)=%d ≠ expected_count=%d", len(fx.Values), fx.ExpectedCount) - } - var sum float64 - for _, v := range fx.Values { - sum += v - } - if sum != fx.ExpectedSum { - t.Errorf("sum(values)=%v ≠ expected_sum=%v", sum, fx.ExpectedSum) - } -} - -// TestGorillaDecoder_HeaderRejectsBadMagic checks that the in-test -// decoder returns a clear error on a wrong magic prefix. -func TestGorillaDecoder_HeaderRejectsBadMagic(t *testing.T) { - bad := append([]byte("GORILLAX"), 1, 0, 0, 0, 0) - _, err := DecodeBlock(bad) - if err == nil { - t.Fatalf("expected bad-magic error, got nil") - } - if !strings.Contains(err.Error(), "bad magic") { - t.Errorf("error message did not mention magic: %v", err) - } -} - -// TestGorillaDecoder_HeaderRejectsBadVersion checks that the in-test -// decoder rejects a future version byte. -func TestGorillaDecoder_HeaderRejectsBadVersion(t *testing.T) { - bad := append([]byte("GORILLA1"), 99, 0, 0, 0, 0) - _, err := DecodeBlock(bad) - if err == nil { - t.Fatalf("expected bad-version error, got nil") - } - if !strings.Contains(err.Error(), "version") { - t.Errorf("error message did not mention version: %v", err) - } -} - -// ----------------------------------------------------------------- -// Fixture loader + input model. -// ----------------------------------------------------------------- - -type inputFixture struct { - MetricName string `json:"metric_name"` - Labels map[string]string `json:"labels"` - IntervalNs int64 `json:"interval_ns"` - Values []float64 `json:"values"` - ExpectedSum float64 `json:"expected_sum"` - ExpectedCount int `json:"expected_count"` - ExpectedMin float64 `json:"expected_min"` - ExpectedMax float64 `json:"expected_max"` -} - -func loadFixture(repoRoot string) (inputFixture, error) { - path := filepath.Join(repoRoot, - "integration", "gorilla_s3_e2e", "golden", "input_samples.json") - b, err := os.ReadFile(path) - if err != nil { - return inputFixture{}, fmt.Errorf("read fixture %s: %w", path, err) - } - var fx inputFixture - if err := json.Unmarshal(b, &fx); err != nil { - return inputFixture{}, fmt.Errorf("parse fixture %s: %w", path, err) - } - return fx, nil -} - -// repoRoot walks up from the package's test directory to the -// nearest ancestor that contains `PROGRESS.md` (the canonical -// repo-root marker for ASAPCollector). Used so `loadFixture` works -// regardless of where `go test` is invoked from. -func repoRoot() (string, error) { - wd, err := os.Getwd() - if err != nil { - return "", err - } - cur := wd - for { - if _, err := os.Stat(filepath.Join(cur, "PROGRESS.md")); err == nil { - return cur, nil - } - parent := filepath.Dir(cur) - if parent == cur { - return "", fmt.Errorf("could not find PROGRESS.md from %s", wd) - } - cur = parent - } -} - -// ----------------------------------------------------------------- -// MinIO + chunk fetch. -// ----------------------------------------------------------------- - -// listAndFetchChunks walks the `asap-gorilla` MinIO bucket via the -// `mc` CLI inside the e2e MinIO container, downloads every `.gor` -// file under the metric prefix, and returns the raw bytes of each. -// -// Why `mc` instead of an in-Go S3 client? Adding `aws-sdk-go` (or -// `minio-go`) drags ~30 deps into this tiny test module. The test -// already needs docker exec for the OTLP push; reusing it for chunk -// listing keeps go.mod empty. -func listAndFetchChunks(ctx context.Context, metric string) ([][]byte, error) { - // 1) List objects under the metric prefix. - listCmd := exec.CommandContext(ctx, "docker", "compose", - "-p", composeProject, - "-f", composeOverlayPath(), - "exec", "-T", "minio-setup-e2e", - "mc", "find", "asap/asap-gorilla/default/"+metric, "--name", "*.gor") - out, err := listCmd.Output() - if err != nil { - // Re-create the alias inside a transient mc container in case - // minio-setup-e2e finished and was reaped — production - // minio-setup container exits after the bucket is created. - listCmd2 := exec.CommandContext(ctx, "docker", "run", "--rm", - "--network", composeProject+"_default", - "--entrypoint", "sh", - "minio/mc:latest", - "-c", - fmt.Sprintf( - "mc alias set asap http://minio-e2e:9000 asap asap-local-only "+ - "&& mc find asap/asap-gorilla/default/%s --name '*.gor' || true", - metric)) - out, err = listCmd2.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("mc find failed: %v\n%s", err, string(out)) - } - } - keys := strings.Split(strings.TrimSpace(string(out)), "\n") - chunks := make([][]byte, 0, len(keys)) - for _, k := range keys { - k = strings.TrimSpace(k) - if k == "" { - continue - } - // 2) Download each via mc cat. - catCmd := exec.CommandContext(ctx, "docker", "run", "--rm", - "--network", composeProject+"_default", - "--entrypoint", "sh", - "minio/mc:latest", - "-c", - fmt.Sprintf( - "mc alias set asap http://minio-e2e:9000 asap asap-local-only >/dev/null "+ - "&& mc cat %s", k)) - var buf bytes.Buffer - catCmd.Stdout = &buf - if err := catCmd.Run(); err != nil { - return nil, fmt.Errorf("mc cat %s: %w", k, err) - } - chunks = append(chunks, buf.Bytes()) - } - return chunks, nil -} - -// composeOverlayPath returns the absolute path to the e2e overlay -// compose file. Resolved off `repoRoot()`; panics on misconfig -// since the file is shipped in the same PR as this test. -func composeOverlayPath() string { - root, err := repoRoot() - if err != nil { - return "" - } - return filepath.Join(root, "integration", "gorilla_s3_e2e", - "docker-compose", "e2e-overlay.yml") -} - -// mergeAllSamples flattens the per-chunk per-series samples into -// one slice. Used by HappyPath / MultiChunkRange to compare the -// decoded set against the input fixture. -func mergeAllSamples(chunkBytes [][]byte) []Sample { - var all []Sample - for _, b := range chunkBytes { - series, err := DecodeBlock(b) - if err != nil { - continue // surfaced separately by the caller's error path - } - for _, s := range series { - all = append(all, s.Samples...) - } - } - return all -} - -// ----------------------------------------------------------------- -// OTLP/HTTP push (no client lib). -// ----------------------------------------------------------------- - -// pushSamplesOTLPHTTP serializes the fixture as an OTLP/HTTP -// `ExportMetricsServiceRequest` JSON body and POSTs it to the -// agent's :4318/v1/metrics endpoint. -// -// We hand-build the protobuf-JSON shape (sum-with-monotonic=true) so -// this test stays std-lib-only. The shape mirrors the OTel collector -// receiver's `OTLP/HTTP+JSON` parse path. -func pushSamplesOTLPHTTP(ctx context.Context, hostPort int, fx inputFixture, baseTSNs int64) error { - type otlpKV struct { - Key string `json:"key"` - Value map[string]interface{} `json:"value"` - } - type otlpDataPoint struct { - Attributes []otlpKV `json:"attributes,omitempty"` - StartTimeNs string `json:"startTimeUnixNano"` - TimeUnixNano string `json:"timeUnixNano"` - AsDouble float64 `json:"asDouble"` - } - type otlpSum struct { - AggregationTemporality int `json:"aggregationTemporality"` - IsMonotonic bool `json:"isMonotonic"` - DataPoints []otlpDataPoint `json:"dataPoints"` - } - type otlpMetric struct { - Name string `json:"name"` - Sum otlpSum `json:"sum"` - } - type otlpScopeMetrics struct { - Metrics []otlpMetric `json:"metrics"` - } - type otlpResourceMetrics struct { - ScopeMetrics []otlpScopeMetrics `json:"scopeMetrics"` - } - type otlpReq struct { - ResourceMetrics []otlpResourceMetrics `json:"resourceMetrics"` - } - - dps := make([]otlpDataPoint, len(fx.Values)) - for i, v := range fx.Values { - ts := baseTSNs + int64(i)*fx.IntervalNs - attrs := make([]otlpKV, 0, len(fx.Labels)) - for k, val := range fx.Labels { - attrs = append(attrs, otlpKV{ - Key: k, - Value: map[string]interface{}{"stringValue": val}, - }) - } - dps[i] = otlpDataPoint{ - Attributes: attrs, - StartTimeNs: strconv.FormatInt(baseTSNs, 10), - TimeUnixNano: strconv.FormatInt(ts, 10), - AsDouble: v, - } - } - req := otlpReq{ - ResourceMetrics: []otlpResourceMetrics{{ - ScopeMetrics: []otlpScopeMetrics{{ - Metrics: []otlpMetric{{ - Name: fx.MetricName, - Sum: otlpSum{ - AggregationTemporality: 2, // CUMULATIVE - IsMonotonic: true, - DataPoints: dps, - }, - }}, - }}, - }}, - } - body, err := json.Marshal(req) - if err != nil { - return fmt.Errorf("marshal OTLP req: %w", err) - } - url := fmt.Sprintf("http://127.0.0.1:%d/v1/metrics", hostPort) - httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) - if err != nil { - return fmt.Errorf("build POST: %w", err) - } - httpReq.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(httpReq) - if err != nil { - return fmt.Errorf("POST OTLP: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("OTLP HTTP %d: %s", resp.StatusCode, string(respBody)) - } - return nil -} - -// ----------------------------------------------------------------- -// Backend PromQL probe. -// ----------------------------------------------------------------- - -// promQLQuery hits the backend's /api/v1/query endpoint with a -// `sum_over_time([5m])` query and returns the parsed JSON -// response. Returns an error if the endpoint is unreachable or the -// HTTP status is not 200. -func promQLQuery(ctx context.Context, metric string) (map[string]interface{}, error) { - q := fmt.Sprintf("sum_over_time(%s[5m])", metric) - url := fmt.Sprintf("http://127.0.0.1:%d/api/v1/query?query=%s", - hostPortBackendQuery, escapeQuery(q)) - httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, err - } - resp, err := http.DefaultClient.Do(httpReq) - if err != nil { - return nil, err - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("backend HTTP %d: %s", resp.StatusCode, string(body)) - } - var out map[string]interface{} - if err := json.Unmarshal(body, &out); err != nil { - return nil, fmt.Errorf("parse PromQL response: %w; body=%s", err, string(body)) - } - return out, nil -} - -// infoLines extracts the `infos` array from a PromQL JSON response. -// Returns an empty slice if the field is absent (in which case the -// caller can skip the marker assertions). -func infoLines(resp map[string]interface{}) []string { - out := []string{} - data, ok := resp["data"].(map[string]interface{}) - if !ok { - return out - } - infos, ok := data["infos"].([]interface{}) - if !ok { - // Try top-level too — backend response shape has evolved - // across phases; be liberal. - if topInfos, ok := resp["infos"].([]interface{}); ok { - for _, x := range topInfos { - if s, ok := x.(string); ok { - out = append(out, s) - } - } - } - return out - } - for _, x := range infos { - if s, ok := x.(string); ok { - out = append(out, s) - } - } - return out -} - -func containsLine(lines []string, needle string) bool { - for _, l := range lines { - if strings.Contains(l, needle) { - return true - } - } - return false -} - -// ----------------------------------------------------------------- -// Misc helpers. -// ----------------------------------------------------------------- - -func escapeQuery(s string) string { - // Std-lib URL-escape without dragging in net/url.QueryEscape's - // allocation cost is overkill; just defer to it. - return strings.ReplaceAll(strings.ReplaceAll(s, " ", "%20"), "[", "%5B") + "" -} - -// waitForTCP polls a host:port until it accepts a TCP connection -// or the deadline expires. -func waitForTCP(ctx context.Context, addr string, max time.Duration) error { - deadline := time.Now().Add(max) - for { - if ctx.Err() != nil { - return ctx.Err() - } - c, err := net.DialTimeout("tcp", addr, 2*time.Second) - if err == nil { - _ = c.Close() - return nil - } - if time.Now().After(deadline) { - return fmt.Errorf("waitForTCP %s: deadline exceeded: %w", addr, err) - } - time.Sleep(1 * time.Second) - } -} - -// testLogWriter forwards writes to t.Logf so subprocess stdout/stderr -// shows up under the right subtest in `go test -v` output. -type testLogWriter struct{ t *testing.T } - -func (w testLogWriter) Write(p []byte) (int, error) { - for _, line := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") { - w.t.Log(line) - } - return len(p), nil -} diff --git a/integration/gorilla_s3_e2e/go.mod b/integration/gorilla_s3_e2e/go.mod deleted file mode 100644 index 7d9ca6ae..00000000 --- a/integration/gorilla_s3_e2e/go.mod +++ /dev/null @@ -1,18 +0,0 @@ -module github.com/ProjectASAP/ASAPCollector/integration/gorilla_s3_e2e - -go 1.25.5 - -// Phase 6 Gorilla-S3 e2e integration test deps. -// -// Kept INTENTIONALLY MINIMAL — std library only — so this test is -// portable across CI environments and does not need the patched OTel -// collector replace cascade that integration/parity/ requires. The -// in-test Go decoder mirrors the GORILLA1 block format directly -// (see gorilla_decoder.go) which keeps the byte-format contract -// pinned in this PR's source tree, not via a transitive import that -// could silently drift on a dep bump. -// -// The cross-language subtest shells out to `cargo test` against the -// sibling asap-gorilla crate (../../asap-gorilla), so its -// byte-compatibility assertion uses the canonical Rust decoder -// without adding any Go-side dep. diff --git a/integration/gorilla_s3_e2e/gorilla_decoder.go b/integration/gorilla_s3_e2e/gorilla_decoder.go deleted file mode 100644 index c4da4fc8..00000000 --- a/integration/gorilla_s3_e2e/gorilla_decoder.go +++ /dev/null @@ -1,405 +0,0 @@ -// In-test Go decoder for the GORILLA1 block format. -// -// Mirrors the byte layout the Phase-2 `gorillas3processor` writes -// (see opentelemetry-collector-contrib-patch/processor/ -// gorillas3processor/encoder.go::buildChunks + -// encodeSeriesBody + bitwriter.go) and the Phase-1 `asap-gorilla` -// Rust crate decodes (asap-gorilla/src/decoder.rs). -// -// Why a SECOND decoder instead of importing the existing two? -// -// * The Go gorillas3processor lives in -// opentelemetry-collector-contrib-patch/processor/, which has a -// long replace-directive cascade (patched pdata, patched -// processor core, sibling sketch processors). Importing it from -// this test would either drag the whole cascade in, or risk -// drifting if the replacements change. Phase 6 wants the -// byte-format CONTRACT pinned in this PR's source — and a -// fresh, header-only decoder is exactly that pin. -// * The Rust `asap-gorilla` crate is the production decoder the -// Phase-3 GorillaS3ColdStore relies on; cross-checking against -// it is the *separate* CrossLanguageByteCompat subtest, which -// shells out via `cargo test`. -// -// What this decoder enforces: -// -// * Outer header : 8-byte "GORILLA1" magic, 1-byte version=1, -// uint32 LE seriesCount. -// * Per-series : uint16 LE metaLen, JSON metadata, uint32 LE -// pointCount, uint64 LE firstTS, uint64 LE -// firstValBits, uint32 LE tsBitsLen, raw ts -// bits, uint32 LE valBitsLen, raw val bits. -// * Bit decoding : timestamps via Gorilla delta-of-delta -// (0 / 10+7 / 110+9 / 1110+12 / 1111+64), -// values via Gorilla XOR (1-bit zero-flag, -// 1-bit reuse-window, 5-bit lz, 6-bit sig-1). -// -// The decoder returns the full sample stream; the test compares -// it byte-for-byte against the input fixture. - -package gorillas3e2e - -import ( - "bytes" - "encoding/binary" - "encoding/json" - "fmt" - "io" - "math" -) - -const ( - gorillaMagic = "GORILLA1" - gorillaVersion = 1 - headerLen = 8 + 1 + 4 // magic + version + seriesCount -) - -// SeriesMeta is the JSON metadata header that precedes every -// per-series body. Field tags must match -// `gorillas3processor/encoder.go::seriesMeta` and -// `asap-gorilla/src/block.rs::SeriesMeta` exactly. -type SeriesMeta struct { - MetricName string `json:"metric_name"` - Attributes map[string]string `json:"attributes"` - StartTS int64 `json:"start_ts"` - EndTS int64 `json:"end_ts"` - PointCount int `json:"point_count"` -} - -// DecodedSeries is one fully-decoded series in a GORILLA1 block. -type DecodedSeries struct { - Meta SeriesMeta - Samples []Sample -} - -// Sample is a single (UnixNano, value) point. -type Sample struct { - TS int64 - Value float64 -} - -// DecodeBlock parses one GORILLA1 block from `data` and returns -// every series with its decoded samples. Returns an error on header -// mismatch, truncated body, or sample-count divergence between the -// metadata's declared PointCount and the bit-stream's actual yield. -func DecodeBlock(data []byte) ([]DecodedSeries, error) { - if len(data) < headerLen { - return nil, fmt.Errorf("gorilla1: block too short (%d < %d)", len(data), headerLen) - } - if string(data[:8]) != gorillaMagic { - return nil, fmt.Errorf("gorilla1: bad magic %q (want %q)", string(data[:8]), gorillaMagic) - } - if data[8] != gorillaVersion { - return nil, fmt.Errorf("gorilla1: unsupported version %d (want %d)", data[8], gorillaVersion) - } - seriesCount := binary.LittleEndian.Uint32(data[9:13]) - r := bytes.NewReader(data[headerLen:]) - - out := make([]DecodedSeries, 0, seriesCount) - for i := uint32(0); i < seriesCount; i++ { - s, err := decodeSeriesBody(r) - if err != nil { - return nil, fmt.Errorf("gorilla1: series #%d: %w", i, err) - } - out = append(out, s) - } - return out, nil -} - -func decodeSeriesBody(r *bytes.Reader) (DecodedSeries, error) { - var metaLen uint16 - if err := binary.Read(r, binary.LittleEndian, &metaLen); err != nil { - return DecodedSeries{}, fmt.Errorf("read metaLen: %w", err) - } - metaBytes := make([]byte, metaLen) - if _, err := io.ReadFull(r, metaBytes); err != nil { - return DecodedSeries{}, fmt.Errorf("read meta: %w", err) - } - var meta SeriesMeta - if err := json.Unmarshal(metaBytes, &meta); err != nil { - return DecodedSeries{}, fmt.Errorf("parse meta: %w", err) - } - - var pointCount uint32 - if err := binary.Read(r, binary.LittleEndian, &pointCount); err != nil { - return DecodedSeries{}, fmt.Errorf("read pointCount: %w", err) - } - if int(pointCount) != meta.PointCount { - return DecodedSeries{}, fmt.Errorf( - "meta.point_count=%d disagrees with body pointCount=%d", - meta.PointCount, pointCount) - } - - var firstTS int64 - if err := binary.Read(r, binary.LittleEndian, &firstTS); err != nil { - return DecodedSeries{}, fmt.Errorf("read firstTS: %w", err) - } - var firstValBits uint64 - if err := binary.Read(r, binary.LittleEndian, &firstValBits); err != nil { - return DecodedSeries{}, fmt.Errorf("read firstValBits: %w", err) - } - - var tsBitsLen uint32 - if err := binary.Read(r, binary.LittleEndian, &tsBitsLen); err != nil { - return DecodedSeries{}, fmt.Errorf("read tsBitsLen: %w", err) - } - tsByteLen := (int(tsBitsLen) + 7) / 8 - tsBits := make([]byte, tsByteLen) - if _, err := io.ReadFull(r, tsBits); err != nil { - return DecodedSeries{}, fmt.Errorf("read tsBits: %w", err) - } - - var valBitsLen uint32 - if err := binary.Read(r, binary.LittleEndian, &valBitsLen); err != nil { - return DecodedSeries{}, fmt.Errorf("read valBitsLen: %w", err) - } - valByteLen := (int(valBitsLen) + 7) / 8 - valBits := make([]byte, valByteLen) - if _, err := io.ReadFull(r, valBits); err != nil { - return DecodedSeries{}, fmt.Errorf("read valBits: %w", err) - } - - timestamps, err := decodeTimestamps(tsBits, int(tsBitsLen), int(pointCount), firstTS) - if err != nil { - return DecodedSeries{}, fmt.Errorf("decode timestamps: %w", err) - } - values, err := decodeValues(valBits, int(valBitsLen), int(pointCount), firstValBits) - if err != nil { - return DecodedSeries{}, fmt.Errorf("decode values: %w", err) - } - - samples := make([]Sample, len(timestamps)) - for i := range timestamps { - samples[i] = Sample{TS: timestamps[i], Value: values[i]} - } - return DecodedSeries{Meta: meta, Samples: samples}, nil -} - -// bitReader pulls bits MSB-first out of a byte slice. Mirrors -// bitwriter.go's MSB-first writer. -type bitReader struct { - buf []byte - bit int // total bits consumed - nMax int // total bits available -} - -func newBitReader(buf []byte, nBitsAvail int) *bitReader { - return &bitReader{buf: buf, bit: 0, nMax: nBitsAvail} -} - -func (br *bitReader) readBit() (uint8, error) { - if br.bit >= br.nMax { - return 0, io.EOF - } - byteIdx := br.bit / 8 - bitInByte := uint(7 - (br.bit % 8)) - b := (br.buf[byteIdx] >> bitInByte) & 1 - br.bit++ - return b, nil -} - -func (br *bitReader) readBits(n int) (uint64, error) { - if n == 0 { - return 0, nil - } - if n < 0 || n > 64 { - return 0, fmt.Errorf("bad readBits n=%d", n) - } - var out uint64 - for i := 0; i < n; i++ { - b, err := br.readBit() - if err != nil { - return 0, err - } - out = (out << 1) | uint64(b) - } - return out, nil -} - -// signExtend treats v's low n bits as a two's-complement signed -// integer and sign-extends to int64. Mirrors the Go encoder's -// `uint64(dd) & ((1<= 64 { - return int64(v) - } - mask := uint64(1) << uint(n-1) - if v&mask != 0 { - // negative — sign-extend by ORing in the high bits. - v |= ^uint64(0) << uint(n) - } - return int64(v) -} - -// decodeTimestamps inverts gorillaTimestampEncoder.push. -// -// First sample's TS is `firstTS` (carried in the series header as a -// raw uint64 LE — NOT in the bit stream). Subsequent samples are -// reconstructed by reading the variable-length bucket header -// (0 / 10 / 110 / 1110 / 1111) and then the dd payload. -func decodeTimestamps(buf []byte, nBits int, pointCount int, firstTS int64) ([]int64, error) { - out := make([]int64, 0, pointCount) - if pointCount == 0 { - return out, nil - } - out = append(out, firstTS) - if pointCount == 1 { - return out, nil - } - br := newBitReader(buf, nBits) - prevTS := firstTS - prevDelta := int64(0) - for i := 1; i < pointCount; i++ { - bucketBits, err := readBucketHeader(br) - if err != nil { - return nil, fmt.Errorf("ts bucket header @sample %d: %w", i, err) - } - var dd int64 - switch bucketBits { - case 0: - dd = 0 - case 7, 9, 12: - raw, err := br.readBits(bucketBits) - if err != nil { - return nil, fmt.Errorf("ts dd bits @sample %d: %w", i, err) - } - dd = signExtend(raw, bucketBits) - case 64: - raw, err := br.readBits(64) - if err != nil { - return nil, fmt.Errorf("ts dd 64 @sample %d: %w", i, err) - } - dd = int64(raw) - default: - return nil, fmt.Errorf("unexpected bucket width %d @sample %d", bucketBits, i) - } - delta := prevDelta + dd - ts := prevTS + delta - out = append(out, ts) - prevTS = ts - prevDelta = delta - } - return out, nil -} - -// readBucketHeader reads a Gorilla delta-of-delta bucket header and -// returns the WIDTH (in bits) of the dd payload that follows: 0 -// (no payload), 7, 9, 12, or 64. -func readBucketHeader(br *bitReader) (int, error) { - b0, err := br.readBit() - if err != nil { - return 0, err - } - if b0 == 0 { - return 0, nil // bucket "0": dd == 0 - } - b1, err := br.readBit() - if err != nil { - return 0, err - } - if b1 == 0 { - return 7, nil // bucket "10": 7 bits - } - b2, err := br.readBit() - if err != nil { - return 0, err - } - if b2 == 0 { - return 9, nil // bucket "110": 9 bits - } - b3, err := br.readBit() - if err != nil { - return 0, err - } - if b3 == 0 { - return 12, nil // bucket "1110": 12 bits - } - return 64, nil // bucket "1111": 64 bits -} - -// decodeValues inverts gorillaValueEncoder.push. -// -// First value bits come from the series header (`firstValBits`). -// Subsequent values follow the Gorilla XOR rules: -// -// * bit 0 == 0 → value unchanged from previous. -// * bit 0 == 1, bit 1 == 0 → reuse the previous (lz, sig) window; -// read `sig` significant bits, shift -// into position by `tz = 64 - lz - sig`. -// * bit 0 == 1, bit 1 == 1 → read 5-bit lz, 6-bit (sig-1), then -// `sig` significant bits, and replace -// the prev window. -func decodeValues(buf []byte, nBits int, pointCount int, firstValBits uint64) ([]float64, error) { - out := make([]float64, 0, pointCount) - if pointCount == 0 { - return out, nil - } - out = append(out, math.Float64frombits(firstValBits)) - if pointCount == 1 { - return out, nil - } - br := newBitReader(buf, nBits) - prev := firstValBits - var leadingZeros, trailingZeros uint8 - havePrevWindow := false - for i := 1; i < pointCount; i++ { - b0, err := br.readBit() - if err != nil { - return nil, fmt.Errorf("val flag @sample %d: %w", i, err) - } - if b0 == 0 { - out = append(out, math.Float64frombits(prev)) - continue - } - b1, err := br.readBit() - if err != nil { - return nil, fmt.Errorf("val window-flag @sample %d: %w", i, err) - } - if b1 == 0 { - if !havePrevWindow { - return nil, fmt.Errorf( - "val sample %d wants reuse-window but none has been written", - i) - } - sig := 64 - int(leadingZeros) - int(trailingZeros) - x, err := br.readBits(sig) - if err != nil { - return nil, fmt.Errorf("val sig bits @sample %d: %w", i, err) - } - x <<= uint(trailingZeros) - cur := prev ^ x - out = append(out, math.Float64frombits(cur)) - prev = cur - continue - } - // New window: 5-bit lz, 6-bit sig-1, sig bits. - lz, err := br.readBits(5) - if err != nil { - return nil, fmt.Errorf("val lz @sample %d: %w", i, err) - } - sigMinus1, err := br.readBits(6) - if err != nil { - return nil, fmt.Errorf("val sig @sample %d: %w", i, err) - } - sig := int(sigMinus1) + 1 - x, err := br.readBits(sig) - if err != nil { - return nil, fmt.Errorf("val sig bits @sample %d: %w", i, err) - } - tz := 64 - int(lz) - sig - if tz < 0 { - return nil, fmt.Errorf( - "val sample %d: invalid window lz=%d sig=%d (tz=%d)", - i, lz, sig, tz) - } - x <<= uint(tz) - cur := prev ^ x - out = append(out, math.Float64frombits(cur)) - prev = cur - leadingZeros = uint8(lz) - trailingZeros = uint8(tz) - havePrevWindow = true - } - return out, nil -} diff --git a/integration/gorilla_s3_e2e/gorilla_decoder_test.go b/integration/gorilla_s3_e2e/gorilla_decoder_test.go deleted file mode 100644 index 393c6f5e..00000000 --- a/integration/gorilla_s3_e2e/gorilla_decoder_test.go +++ /dev/null @@ -1,364 +0,0 @@ -// Self-contained sanity tests for the in-test Go decoder. These do -// NOT exercise the live docker stack — they hand-build a minimal -// GORILLA1 block by mirroring the encoder's bit layout and round- -// trip it through the decoder. The point is to catch a regression -// in the decoder itself BEFORE the live e2e path runs (which might -// otherwise mask a decoder bug as a "no chunks written" failure). - -package gorillas3e2e - -import ( - "bytes" - "encoding/binary" - "encoding/json" - "math" - "testing" -) - -// TestGorillaDecoder_RoundTripSingleSeriesSinglePoint builds the -// minimal valid block: 1 series, 1 sample. Validates that the -// decoder reads back the sample (firstTS / firstValue paths only; -// no bit stream entries since there are no follow-on samples). -func TestGorillaDecoder_RoundTripSingleSeriesSinglePoint(t *testing.T) { - const ts = int64(1_700_000_000_000_000_000) - const v = 42.5 - - body := buildSeriesBody(t, "single_point_metric", - map[string]string{"job": "test"}, - []int64{ts}, []float64{v}) - - block := buildBlock(t, []byte(body)) - got, err := DecodeBlock(block) - if err != nil { - t.Fatalf("DecodeBlock: %v", err) - } - if len(got) != 1 { - t.Fatalf("want 1 series, got %d", len(got)) - } - s := got[0] - if s.Meta.MetricName != "single_point_metric" { - t.Errorf("metric name: got %q", s.Meta.MetricName) - } - if len(s.Samples) != 1 { - t.Fatalf("want 1 sample, got %d", len(s.Samples)) - } - if s.Samples[0].TS != ts { - t.Errorf("ts: got %d want %d", s.Samples[0].TS, ts) - } - if s.Samples[0].Value != v { - t.Errorf("value: got %v want %v", s.Samples[0].Value, v) - } -} - -// TestGorillaDecoder_RoundTripFlatSequence builds a block with -// constant-delta timestamps (1s spacing) and identical values -// across all samples — the cheapest path to exercise the -// "bucket-0" timestamp path AND the "value-unchanged" XOR path. -func TestGorillaDecoder_RoundTripFlatSequence(t *testing.T) { - const base = int64(1_700_000_000_000_000_000) - tss := make([]int64, 10) - vals := make([]float64, 10) - for i := range tss { - tss[i] = base + int64(i)*int64(1_000_000_000) - vals[i] = 7.0 - } - body := buildSeriesBody(t, "flat_metric", nil, tss, vals) - block := buildBlock(t, []byte(body)) - - got, err := DecodeBlock(block) - if err != nil { - t.Fatalf("DecodeBlock: %v", err) - } - if len(got) != 1 || len(got[0].Samples) != 10 { - t.Fatalf("want 1 series of 10 samples; got %d series", len(got)) - } - for i, s := range got[0].Samples { - if s.TS != tss[i] || s.Value != vals[i] { - t.Errorf("sample %d: got (%d, %v) want (%d, %v)", - i, s.TS, s.Value, tss[i], vals[i]) - } - } -} - -// TestGorillaDecoder_RoundTripVaryingValues builds a block with -// constant-delta timestamps but VARYING values (the increasing -// 1..N sequence the e2e fixture uses). Exercises the value -// XOR path's "new window" + "reuse window" branches. -func TestGorillaDecoder_RoundTripVaryingValues(t *testing.T) { - const base = int64(1_700_000_000_000_000_000) - const n = 100 - tss := make([]int64, n) - vals := make([]float64, n) - for i := range tss { - tss[i] = base + int64(i)*int64(1_000_000_000) - vals[i] = float64(i + 1) - } - body := buildSeriesBody(t, "ramp_metric", nil, tss, vals) - block := buildBlock(t, []byte(body)) - - got, err := DecodeBlock(block) - if err != nil { - t.Fatalf("DecodeBlock: %v", err) - } - if len(got) != 1 || len(got[0].Samples) != n { - t.Fatalf("want 1 series of %d samples; got %d series", n, len(got)) - } - for i, s := range got[0].Samples { - if s.TS != tss[i] || s.Value != vals[i] { - t.Errorf("sample %d: got (%d, %v) want (%d, %v)", - i, s.TS, s.Value, tss[i], vals[i]) - } - } - // And confirm the ground-truth sum matches the 5050 invariant - // the e2e fixture also pins. - var sum float64 - for _, s := range got[0].Samples { - sum += s.Value - } - if sum != 5050.0 { - t.Errorf("decoded sum = %v, want 5050.0", sum) - } -} - -// ----------------------------------------------------------------- -// In-test mini encoder. Mirrors `gorillas3processor/encoder.go` -// just closely enough to round-trip through `DecodeBlock`. NOT a -// full encoder — only used to seed the round-trip tests above. -// ----------------------------------------------------------------- - -// bitWriter mirrors the MSB-first packed-bytes writer in -// `gorillas3processor/bitwriter.go`. Kept private to the test -// package since it has no production consumer. -type testBitWriter struct { - buf bytes.Buffer - curByte byte - nbits uint8 -} - -func (w *testBitWriter) writeBit(b uint8) { - if b != 0 { - w.curByte |= 1 << (7 - w.nbits) - } - w.nbits++ - if w.nbits == 8 { - w.buf.WriteByte(w.curByte) - w.curByte = 0 - w.nbits = 0 - } -} - -func (w *testBitWriter) writeBits(v uint64, n uint8) { - for i := int(n) - 1; i >= 0; i-- { - w.writeBit(uint8((v >> uint(i)) & 1)) - } -} - -func (w *testBitWriter) bytes() []byte { - if w.nbits != 0 { - w.buf.WriteByte(w.curByte) - } - return w.buf.Bytes() -} - -// encodeTimestamps builds the delta-of-delta bit stream + advertised -// bit length used by the encoder. First TS goes into `firstTS`; the -// rest into the bit stream. -func encodeTimestamps(ts []int64) (firstTS int64, bits []byte, bitLen uint32) { - if len(ts) == 0 { - return 0, nil, 0 - } - firstTS = ts[0] - if len(ts) == 1 { - return firstTS, nil, 0 - } - bw := &testBitWriter{} - prevTS := ts[0] - prevDelta := int64(0) - bitsWritten := 0 - for i := 1; i < len(ts); i++ { - delta := ts[i] - prevTS - dd := delta - prevDelta - switch { - case dd == 0: - bw.writeBit(0) - bitsWritten++ - case fitsSigned(dd, 7): - bw.writeBits(0b10, 2) - bw.writeBits(uint64(dd)&((1<<7)-1), 7) - bitsWritten += 9 - case fitsSigned(dd, 9): - bw.writeBits(0b110, 3) - bw.writeBits(uint64(dd)&((1<<9)-1), 9) - bitsWritten += 12 - case fitsSigned(dd, 12): - bw.writeBits(0b1110, 4) - bw.writeBits(uint64(dd)&((1<<12)-1), 12) - bitsWritten += 16 - default: - bw.writeBits(0b1111, 4) - bw.writeBits(uint64(dd), 64) - bitsWritten += 68 - } - prevTS = ts[i] - prevDelta = delta - } - return firstTS, bw.bytes(), uint32(bitsWritten) -} - -// encodeValues builds the XOR-encoded float bit stream + advertised -// bit length. First value goes into `firstValBits` (raw IEEE-754), -// the rest into the bit stream. -func encodeValues(vals []float64) (firstValBits uint64, bits []byte, bitLen uint32) { - if len(vals) == 0 { - return 0, nil, 0 - } - firstValBits = math.Float64bits(vals[0]) - if len(vals) == 1 { - return firstValBits, nil, 0 - } - bw := &testBitWriter{} - prev := firstValBits - var lzPrev, tzPrev uint8 - havePrev := false - bitsWritten := 0 - for i := 1; i < len(vals); i++ { - vb := math.Float64bits(vals[i]) - x := prev ^ vb - if x == 0 { - bw.writeBit(0) - bitsWritten++ - prev = vb - continue - } - bw.writeBit(1) - bitsWritten++ - var lz, tz uint8 - // math/bits leading zero count, but we only need it via - // our decoder's mirror; inline using 64 - sig - tz when we - // have lz from a quick loop. - lz = leadingZerosTest(x) - tz = trailingZerosTest(x) - sig := uint8(64 - int(lz) - int(tz)) - if havePrev && lz >= lzPrev && tz >= tzPrev { - bw.writeBit(0) - bitsWritten++ - s := uint8(64 - int(lzPrev) - int(tzPrev)) - bw.writeBits(x>>uint(tzPrev), s) - bitsWritten += int(s) - } else { - bw.writeBit(1) - bitsWritten++ - lz5 := lz - if lz5 > 31 { - lz5 = 31 - } - bw.writeBits(uint64(lz5), 5) - bitsWritten += 5 - if sig == 0 { - sig = 64 - } - bw.writeBits(uint64(sig-1), 6) - bitsWritten += 6 - bw.writeBits(x>>uint(tz), sig) - bitsWritten += int(sig) - lzPrev = lz - tzPrev = tz - havePrev = true - } - prev = vb - } - return firstValBits, bw.bytes(), uint32(bitsWritten) -} - -func leadingZerosTest(x uint64) uint8 { - if x == 0 { - return 64 - } - var n uint8 - for i := 63; i >= 0; i-- { - if x&(uint64(1)<= 64 { - return true - } - min := -(int64(1) << (n - 1)) - max := (int64(1) << (n - 1)) - 1 - return v >= min && v <= max -} - -// buildSeriesBody assembles one series body the same way -// `encodeSeriesBody` does in the production encoder. -func buildSeriesBody(t *testing.T, metric string, attrs map[string]string, - ts []int64, vals []float64, -) string { - t.Helper() - if len(ts) != len(vals) { - t.Fatalf("ts/vals length mismatch %d vs %d", len(ts), len(vals)) - } - if len(ts) == 0 { - t.Fatal("zero-length series rejected by the format") - } - if attrs == nil { - attrs = map[string]string{} - } - meta := SeriesMeta{ - MetricName: metric, - Attributes: attrs, - StartTS: ts[0], - EndTS: ts[len(ts)-1], - PointCount: len(ts), - } - mb, err := json.Marshal(meta) - if err != nil { - t.Fatalf("marshal meta: %v", err) - } - - firstTS, tsBits, tsBitLen := encodeTimestamps(ts) - firstValBits, valBits, valBitLen := encodeValues(vals) - - var sb bytes.Buffer - _ = binary.Write(&sb, binary.LittleEndian, uint16(len(mb))) - sb.Write(mb) - _ = binary.Write(&sb, binary.LittleEndian, uint32(len(ts))) - _ = binary.Write(&sb, binary.LittleEndian, uint64(firstTS)) - _ = binary.Write(&sb, binary.LittleEndian, firstValBits) - _ = binary.Write(&sb, binary.LittleEndian, tsBitLen) - sb.Write(tsBits) - _ = binary.Write(&sb, binary.LittleEndian, valBitLen) - sb.Write(valBits) - return sb.String() -} - -// buildBlock wraps one or more series bodies in a GORILLA1 block. -func buildBlock(t *testing.T, bodies ...[]byte) []byte { - t.Helper() - var blk bytes.Buffer - blk.WriteString(gorillaMagic) - blk.WriteByte(byte(gorillaVersion)) - _ = binary.Write(&blk, binary.LittleEndian, uint32(len(bodies))) - for _, body := range bodies { - blk.Write(body) - } - return blk.Bytes() -} diff --git a/integration/parity/README.md b/integration/parity/README.md index accdea7d..d6b72e91 100644 --- a/integration/parity/README.md +++ b/integration/parity/README.md @@ -1,30 +1,52 @@ -# Phase 2 parity harness +# integration/parity — three layered parity gates -End-to-end check that the new `asap-precompute-go` runtime emits -`SketchEnvelope` payload bytes identical to today's 5 OTel sketch -processors when given the same input. This is the gate before the -shim refactors in Phase 2 steps 2.5–2.9. ADR-0002 §"Behavior -preservation" promises wire-format invariance — this harness is -the test that verifies it. +Three parity tests, layered by where they pin the contract. All three +must hold for the ASAP edge framework's "swap host, swap binary, swap +codec" claim to mean anything end-to-end. -## Run +| Sub-gate | Scope | What two things it equates | +| --- | --- | --- | +| [`runtime-impl/`](runtime-impl/) | runtime ↔ legacy 5-OTel-processor envelope bytes | `asap-precompute-go` runtime emits the same `SketchEnvelope` payload bytes as the 5 legacy OTel sketch processors when fed identical input. | +| [`codec/`](codec/) | OTel codec ↔ Telegraf codec, in-process | Both per-host codecs (`asap-precompute-go/otel`, `asap-precompute-go/telegraf`) feed the runtime byte-equivalent observation streams, so the emitted envelopes match across host types. | +| [`agent-binary/`](agent-binary/) | `asap-otel` ↔ `asap-otap` agent binaries | The full agent shells (OTLP receiver + processor pipeline + OTLP exporter) preserve the runtime's envelope bytes through the receive-process-emit path. | + +## Why three gates + +- `runtime-impl/` proves the runtime is byte-faithful to the legacy + per-sketch processor cluster the migration replaces. +- `codec/` proves the host-neutral runtime treats `pmetric.Metrics` and + `[]telegraf.Metric` as wire-equivalent inputs. +- `agent-binary/` proves the agent shells (separate codebases, separate + languages) don't perturb the bytes around the runtime. + +If any link in the chain breaks, downstream guarantees (cross-language +backend equivalence, Thanos-side query parity) collapse. + +## Layout + +Each subdir is its own Go module — Go's nested-module rules require a +distinct `go.mod` at every package root. Run each subdir's tests +independently: ``` -cd integration/parity -go test -v ./... +cd integration/parity/runtime-impl && go test ./... +cd integration/parity/codec && go test ./... +cd integration/parity/agent-binary && go test ./... ``` -The harness is fully synchronous: no goroutines, no wall-clock waits, -no flake-prone timers. Two runs of the same harness produce -byte-identical inputs and outputs. +`runtime-impl/` and `codec/` are in-process Go tests; `agent-binary/` +runs in fixture mode by default (no Docker) with a binary mode +opt-in via `run_parity.sh --mode=binary`. -## Subtests +## Cross-language tie-in -- `TestParity_AllSketches` — runs all 5 sketch comparisons in one - pass, reusing one input/output build. -- `TestParity_{DDSketch,KLL,HLL,CountSketch,CountMinSketch}` — - isolated per-sketch tests, useful for `go test -run`. +The Rust runtime test +[`asap-precompute-rs/tests/cross_language_parity.rs`] consumes the +`runtime-impl/golden/*.bin` fixtures: regenerating those fixtures is +how the Go-side and Rust-side runtimes ratify they emit the same +canonical bytes. Regenerate via: -CountSketch and CountMinSketch SKIP today — see the parity test's -`skipReason` for the structural divergence and the PR description's -follow-up list for the planned fixes. +``` +cd integration/parity/runtime-impl && \ + GOLDEN_REGEN=1 go test -run TestGenerateGoldenFixtures ./... +``` diff --git a/integration/cross_host_parity/.gitignore b/integration/parity/agent-binary/.gitignore similarity index 100% rename from integration/cross_host_parity/.gitignore rename to integration/parity/agent-binary/.gitignore diff --git a/integration/cross_host_parity/README.md b/integration/parity/agent-binary/README.md similarity index 81% rename from integration/cross_host_parity/README.md rename to integration/parity/agent-binary/README.md index 717480f8..e3b00f4b 100644 --- a/integration/cross_host_parity/README.md +++ b/integration/parity/agent-binary/README.md @@ -1,4 +1,4 @@ -# integration/cross_host_parity +# integration/parity/agent-binary Phase 5 step E (per `docs/design-asap-otap-rust-integration.md` §11 row E): the three ASAP-flavored agents — `asap-otel` (OTel-Go), @@ -7,6 +7,9 @@ identical input emit byte-identical `SketchEnvelope.Payload`s, and the backend's PromQL output is identical regardless of which agent produced the data. +This is the **agent-binary** layer of the three-gate parity pyramid +(see [`../README.md`](../README.md) for the full layering). + ## Scope of THIS PR — two-way (asap-otel ↔ asap-otap) The cross-language byte-parity gate (issue #243) closed @@ -18,19 +21,19 @@ This PR ships the **two-way** asap-otel ↔ asap-otap test as the default. The `asap-telegraf` third agent is a one-flag opt-in (`CROSS_HOST_PARITY_INCLUDE_TELEGRAF=1` or `--include-telegraf`) so the asymmetry around Telegraf's line-protocol input format -doesn't block the Go↔Rust gate this PR is built around. Phase 4 -step E (`integration/cross-host-parity/`) already closes the -asap-otel ↔ asap-telegraf comparison at the in-process codec -level; Phase 5E binary-mode coverage is a follow-up. +doesn't block the Go↔Rust gate this PR is built around. The codec +gate (`../codec/`) already closes the asap-otel ↔ asap-telegraf +comparison at the in-process codec level; Phase 5E binary-mode +coverage is a follow-up. ## How this layers on existing parity gates | Gate | Scope | Phase | | --- | --- | --- | -| `integration/parity/` | runtime ↔ legacy-OTel-processor envelope bytes | Phase 2 | -| `integration/cross-host-parity/` | OTel-codec ↔ Telegraf-codec, both in-process Go runtime | Phase 4E | +| `../runtime-impl/` | runtime ↔ legacy-OTel-processor envelope bytes | Phase 2 | +| `../codec/` | OTel-codec ↔ Telegraf-codec, both in-process Go runtime | Phase 4E | | `asap-precompute-rs/tests/cross_language_parity.rs` | Go runtime ↔ Rust runtime per-sketch wire format (issue #243) | Phase 5 prereq | -| `integration/cross_host_parity/` (this dir) | asap-otel ↔ asap-otap agent-binary envelope bytes + PromQL | Phase 5E | +| `./` (this dir) | asap-otel ↔ asap-otap agent-binary envelope bytes + PromQL | Phase 5E | The sketch-level cross-language gate (#243) ratifies that *each runtime* produces the canonical envelope bytes from goldenFloats / @@ -44,11 +47,11 @@ full receive-process-emit path. ### Fixture mode (default) ``` -bash integration/cross_host_parity/run_parity.sh +bash integration/parity/agent-binary/run_parity.sh ``` - Verifies the cross-language gate's golden envelope fixtures - (`integration/parity/golden/*.bin`) are present and non-empty. + (`../runtime-impl/golden/*.bin`) are present and non-empty. - Runs `parity_test.go` in `CROSS_HOST_PARITY_MODE=fixture`, which asserts byte-equality across pairs by re-loading the same canonical fixture for each agent (transitively valid because #243 already @@ -65,8 +68,8 @@ path. ### Binary mode ``` -bash integration/cross_host_parity/run_parity.sh --mode=binary -bash integration/cross_host_parity/run_parity.sh --mode=binary --include-telegraf +bash integration/parity/agent-binary/run_parity.sh --mode=binary +bash integration/parity/agent-binary/run_parity.sh --mode=binary --include-telegraf ``` - Brings up `asap/asap-otel:dev` + `asap/asap-otap:dev` (+ @@ -94,7 +97,7 @@ images cause a hard failure with the build commands in the message (agent-pair, query) in binary mode. - `golden_input/inputs.json` — canonical input fixture mirroring `goldenFloats()` / `goldenHllKeys()` / `goldenCsKeys()` / - `goldenCmsKeys()` from `integration/parity/golden_test.go` and + `goldenCmsKeys()` from `../runtime-impl/golden_test.go` and `asap-precompute-rs/tests/cross_language_parity.rs`. - `run_parity.sh` — orchestrator (mode dispatch, prereq check, Docker compose lifecycle, test invocation). diff --git a/integration/cross_host_parity/canonical.go b/integration/parity/agent-binary/canonical.go similarity index 100% rename from integration/cross_host_parity/canonical.go rename to integration/parity/agent-binary/canonical.go diff --git a/integration/cross_host_parity/configs/asap-otap.yaml b/integration/parity/agent-binary/configs/asap-otap.yaml similarity index 100% rename from integration/cross_host_parity/configs/asap-otap.yaml rename to integration/parity/agent-binary/configs/asap-otap.yaml diff --git a/integration/cross_host_parity/configs/asap-otel.yaml b/integration/parity/agent-binary/configs/asap-otel.yaml similarity index 100% rename from integration/cross_host_parity/configs/asap-otel.yaml rename to integration/parity/agent-binary/configs/asap-otel.yaml diff --git a/integration/cross_host_parity/configs/asap-telegraf.toml b/integration/parity/agent-binary/configs/asap-telegraf.toml similarity index 100% rename from integration/cross_host_parity/configs/asap-telegraf.toml rename to integration/parity/agent-binary/configs/asap-telegraf.toml diff --git a/integration/cross_host_parity/go.mod b/integration/parity/agent-binary/go.mod similarity index 74% rename from integration/cross_host_parity/go.mod rename to integration/parity/agent-binary/go.mod index 7d11287a..028f0f24 100644 --- a/integration/cross_host_parity/go.mod +++ b/integration/parity/agent-binary/go.mod @@ -1,4 +1,4 @@ -module github.com/ProjectASAP/ASAPCollector/integration/cross_host_parity +module github.com/ProjectASAP/ASAPCollector/integration/parity/agent-binary go 1.25.5 @@ -6,10 +6,11 @@ go 1.25.5 // // In fixture mode the test reproduces the cross-language gate (#243) // canonical envelope bytes inline using sketchlib-go's -// SerializePortable* helpers — the same calls integration/parity/ -// golden_test.go makes — and asserts byte-equality across agent pairs -// against those bytes. This makes the test self-contained: it does -// NOT depend on integration/parity/golden/*.bin being regenerated +// SerializePortable* helpers — the same calls +// integration/parity/runtime-impl/golden_test.go makes — and asserts +// byte-equality across agent pairs against those bytes. This makes +// the test self-contained: it does NOT depend on +// integration/parity/runtime-impl/golden/*.bin being regenerated // separately. (Optional: if those fixtures ARE present, the test // also cross-checks that the inline regen agrees with the on-disk // fixture, surfacing a fixture-drift regression early.) @@ -38,4 +39,4 @@ require ( // sketchlib-go is a private module; the local sibling checkout lives // next to the ASAPCollector repo. Path is relative to this go.mod. -replace github.com/ProjectASAP/sketchlib-go => ../../../sketchlib-go +replace github.com/ProjectASAP/sketchlib-go => ../../../../sketchlib-go diff --git a/integration/cross_host_parity/go.sum b/integration/parity/agent-binary/go.sum similarity index 100% rename from integration/cross_host_parity/go.sum rename to integration/parity/agent-binary/go.sum diff --git a/integration/cross_host_parity/golden_input/inputs.json b/integration/parity/agent-binary/golden_input/inputs.json similarity index 100% rename from integration/cross_host_parity/golden_input/inputs.json rename to integration/parity/agent-binary/golden_input/inputs.json diff --git a/integration/cross_host_parity/parity_test.go b/integration/parity/agent-binary/parity_test.go similarity index 95% rename from integration/cross_host_parity/parity_test.go rename to integration/parity/agent-binary/parity_test.go index ea928e59..19a4935e 100644 --- a/integration/cross_host_parity/parity_test.go +++ b/integration/parity/agent-binary/parity_test.go @@ -9,7 +9,7 @@ // // - integration/parity/ — runtime ↔ legacy-processor parity for // asap-precompute-go (Phase 2 gate). -// - integration/cross-host-parity/ — OTel-codec ↔ Telegraf-codec +// - integration/parity/codec/ — OTel-codec ↔ Telegraf-codec // parity, both driving asap-precompute-go in-process (Phase 4 // step E gate). // - asap-precompute-rs/tests/cross_language_parity.rs — Rust @@ -54,7 +54,7 @@ import ( "strings" "testing" - crosshostparity "github.com/ProjectASAP/ASAPCollector/integration/cross_host_parity" + crosshostparity "github.com/ProjectASAP/ASAPCollector/integration/parity/agent-binary" ) // sketchTarget is one of the five sketch families this test exercises. @@ -97,12 +97,12 @@ const ( func goldenFixtureDir(t *testing.T) (string, bool) { t.Helper() // Walk up from the test's working dir until we find - // integration/parity/golden. The cross_host_parity dir is a sibling - // of cross-host-parity and parity under integration/, so the - // relative path `../parity/golden` resolves cleanly. + // integration/parity/runtime-impl/golden. The agent-binary dir is a + // sibling of codec and runtime-impl under integration/parity/, so the + // relative path `../runtime-impl/golden` resolves cleanly. candidates := []string{ - filepath.Join("..", "parity", "golden"), - filepath.Join("..", "..", "integration", "parity", "golden"), + filepath.Join("..", "runtime-impl", "golden"), + filepath.Join("..", "..", "..", "integration", "parity", "runtime-impl", "golden"), } for _, c := range candidates { abs, err := filepath.Abs(c) @@ -149,7 +149,7 @@ func modeFromEnv() string { // from OTLP, so the harness either needs an OTLP→line-protocol // translator or two parallel input fixtures. Phase 4 step E already // closes asap-otel↔asap-telegraf at the codec level -// (integration/cross-host-parity/), so the *new* claim Phase 5E +// (integration/parity/codec/), so the *new* claim Phase 5E // defends is the Go↔Rust pair (asap-otel ↔ asap-otap). Three-way // stays a quick add behind the env var. func agentsUnderTest() []string { @@ -227,10 +227,10 @@ func runFixtureMode(t *testing.T) { t.Logf("on-disk gate fixtures present at %s; will cross-check "+ "inline regen against them.", dir) } else { - t.Logf("on-disk gate fixtures absent (integration/parity/golden " + + t.Logf("on-disk gate fixtures absent (integration/parity/runtime-impl/golden " + "sibling not found). Inline regen drives the assertion; the " + "on-disk cross-check is skipped. Regenerate fixtures with:\n" + - " cd integration/parity && GOLDEN_REGEN=1 \\\n" + + " cd integration/parity/runtime-impl && GOLDEN_REGEN=1 \\\n" + " go test -run TestGenerateGoldenFixtures ./...") } @@ -256,7 +256,7 @@ func runFixtureMode(t *testing.T) { t.Errorf("on-disk fixture %s drifted from inline "+ "regen: on-disk=%d bytes, regen=%d bytes, "+ "first-diff at offset %d. Regenerate fixtures "+ - "with:\n cd integration/parity && \\\n"+ + "with:\n cd integration/parity/runtime-impl && \\\n"+ " GOLDEN_REGEN=1 go test -run "+ "TestGenerateGoldenFixtures ./...", sk.fixtureName, len(onDisk), len(canonical), @@ -415,7 +415,7 @@ func head(b []byte, n int) []byte { func TestCrossHostPromQLParity(t *testing.T) { mode := modeFromEnv() - queriesPath := filepath.Join("..", "..", "deploy", "scripts", "queries-e2e.json") + queriesPath := filepath.Join("..", "..", "..", "deploy", "scripts", "queries-e2e.json") qb, err := os.ReadFile(queriesPath) if err != nil { t.Fatalf("read queries-e2e.json: %v", err) diff --git a/integration/cross_host_parity/run_parity.sh b/integration/parity/agent-binary/run_parity.sh similarity index 95% rename from integration/cross_host_parity/run_parity.sh rename to integration/parity/agent-binary/run_parity.sh index d269b52d..acb38106 100755 --- a/integration/cross_host_parity/run_parity.sh +++ b/integration/parity/agent-binary/run_parity.sh @@ -42,9 +42,9 @@ # # # Usage # -# bash integration/cross_host_parity/run_parity.sh -# bash integration/cross_host_parity/run_parity.sh --mode=binary -# bash integration/cross_host_parity/run_parity.sh --mode=binary --include-telegraf +# bash integration/parity/agent-binary/run_parity.sh +# bash integration/parity/agent-binary/run_parity.sh --mode=binary +# bash integration/parity/agent-binary/run_parity.sh --mode=binary --include-telegraf # # # Output # @@ -61,10 +61,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" COMPOSE_FILE="${REPO_ROOT}/deploy/docker-compose/cross-host-parity.yml" CAPTURE_DIR="${SCRIPT_DIR}/captures" -GOLDEN_PARITY_DIR="${REPO_ROOT}/integration/parity/golden" +GOLDEN_PARITY_DIR="${REPO_ROOT}/integration/parity/runtime-impl/golden" MODE="fixture" INCLUDE_TELEGRAF=0 @@ -103,7 +103,7 @@ log "Phase 5 step E run_parity.sh: mode=${MODE} include_telegraf=${INCLUDE_TELEG # cross-language gate's canonical envelope bytes inline via canonical.go # (sketchlib-go SerializePortable* helpers) and asserts byte-equality # across pairs — no Docker, no agent binaries. If -# integration/parity/golden/*.bin is also present, the test cross-checks +# integration/parity/runtime-impl/golden/*.bin is also present, the test cross-checks # the inline regen against the on-disk fixture and surfaces any drift. if [[ "${MODE}" == "fixture" ]]; then if [[ -d "${GOLDEN_PARITY_DIR}" ]]; then @@ -121,7 +121,7 @@ if [[ "${MODE}" == "fixture" ]]; then log "On-disk gate fixtures absent under ${GOLDEN_PARITY_DIR}." log " Inline regen drives the assertion. To enable the cross-check," log " regenerate fixtures with:" - log " cd ${REPO_ROOT}/integration/parity && \\" + log " cd ${REPO_ROOT}/integration/parity/runtime-impl && \\" log " GOLDEN_REGEN=1 go test -run TestGenerateGoldenFixtures ./..." fi log "Running Go test in fixture mode..." diff --git a/integration/cross-host-parity/README.md b/integration/parity/codec/README.md similarity index 89% rename from integration/cross-host-parity/README.md rename to integration/parity/codec/README.md index 8a37ac84..b2621de4 100644 --- a/integration/cross-host-parity/README.md +++ b/integration/parity/codec/README.md @@ -1,4 +1,4 @@ -# integration/cross-host-parity +# integration/parity/codec Phase 4 step E: prove `asap-precompute-go` produces byte-identical `SketchEnvelope` payloads regardless of whether observations come @@ -6,6 +6,9 @@ from OpenTelemetry's `pmetric.Metrics` (driven through the OTel codec) or Telegraf's `telegraf.Metric` (driven through the Telegraf codec). +This is the **codec** layer of the three-gate parity pyramid (see +[`../README.md`](../README.md) for the full layering). + ## What this checks The runtime in `asap-precompute-go/` is host-neutral by design — it @@ -33,7 +36,7 @@ asserts. ## Running ``` -cd integration/cross-host-parity +cd integration/parity/codec go test -v ./... go test -race ./... ``` @@ -50,5 +53,5 @@ go test -race ./... `Precompute` instances. - `harness/runtime_telegraf.go` — same for Telegraf. - `harness/sketches.go` — test-only `precompute.Sketch` wrappers - (mirrors `integration/parity/harness/sketches.go`). + (mirrors `../runtime-impl/harness/sketches.go`). - `harness/diff.go` — envelope byte-comparison and structured diff. diff --git a/integration/cross-host-parity/go.mod b/integration/parity/codec/go.mod similarity index 73% rename from integration/cross-host-parity/go.mod rename to integration/parity/codec/go.mod index 071b4113..5cd71d81 100644 --- a/integration/cross-host-parity/go.mod +++ b/integration/parity/codec/go.mod @@ -1,4 +1,4 @@ -module github.com/ProjectASAP/ASAPCollector/integration/cross-host-parity +module github.com/ProjectASAP/ASAPCollector/integration/parity/codec go 1.25.5 @@ -30,10 +30,11 @@ require ( golang.org/x/text v0.30.0 // indirect ) -// Local checkouts mirror the cascade used by integration/parity/. -// Paths are relative to integration/cross-host-parity/. -replace github.com/ProjectASAP/asap-precompute-go => ../../asap-precompute-go +// Local checkouts mirror the cascade used by +// integration/parity/runtime-impl/. Paths are relative to +// integration/parity/codec/. +replace github.com/ProjectASAP/asap-precompute-go => ../../../asap-precompute-go -replace github.com/ProjectASAP/sketchlib-go => ../../../sketchlib-go +replace github.com/ProjectASAP/sketchlib-go => ../../../../sketchlib-go -replace go.opentelemetry.io/collector/pdata => ../../opentelemetry-collector/pdata +replace go.opentelemetry.io/collector/pdata => ../../../opentelemetry-collector/pdata diff --git a/integration/cross-host-parity/go.sum b/integration/parity/codec/go.sum similarity index 100% rename from integration/cross-host-parity/go.sum rename to integration/parity/codec/go.sum diff --git a/integration/cross-host-parity/harness/diff.go b/integration/parity/codec/harness/diff.go similarity index 100% rename from integration/cross-host-parity/harness/diff.go rename to integration/parity/codec/harness/diff.go diff --git a/integration/cross-host-parity/harness/input_otel.go b/integration/parity/codec/harness/input_otel.go similarity index 100% rename from integration/cross-host-parity/harness/input_otel.go rename to integration/parity/codec/harness/input_otel.go diff --git a/integration/cross-host-parity/harness/input_telegraf.go b/integration/parity/codec/harness/input_telegraf.go similarity index 100% rename from integration/cross-host-parity/harness/input_telegraf.go rename to integration/parity/codec/harness/input_telegraf.go diff --git a/integration/cross-host-parity/harness/runtime_otel.go b/integration/parity/codec/harness/runtime_otel.go similarity index 100% rename from integration/cross-host-parity/harness/runtime_otel.go rename to integration/parity/codec/harness/runtime_otel.go diff --git a/integration/cross-host-parity/harness/runtime_telegraf.go b/integration/parity/codec/harness/runtime_telegraf.go similarity index 100% rename from integration/cross-host-parity/harness/runtime_telegraf.go rename to integration/parity/codec/harness/runtime_telegraf.go diff --git a/integration/cross-host-parity/harness/sketches.go b/integration/parity/codec/harness/sketches.go similarity index 100% rename from integration/cross-host-parity/harness/sketches.go rename to integration/parity/codec/harness/sketches.go diff --git a/integration/cross-host-parity/parity_test.go b/integration/parity/codec/parity_test.go similarity index 97% rename from integration/cross-host-parity/parity_test.go rename to integration/parity/codec/parity_test.go index e1ea3176..bfbda05d 100644 --- a/integration/cross-host-parity/parity_test.go +++ b/integration/parity/codec/parity_test.go @@ -14,7 +14,7 @@ package crosshostparity_test import ( "testing" - "github.com/ProjectASAP/ASAPCollector/integration/cross-host-parity/harness" + "github.com/ProjectASAP/ASAPCollector/integration/parity/codec/harness" ) // TestCrossHostParity_AllSketches runs the full multi-sketch harness diff --git a/integration/parity/runtime-impl/README.md b/integration/parity/runtime-impl/README.md new file mode 100644 index 00000000..b712fdbf --- /dev/null +++ b/integration/parity/runtime-impl/README.md @@ -0,0 +1,44 @@ +# integration/parity/runtime-impl + +End-to-end check that the new `asap-precompute-go` runtime emits +`SketchEnvelope` payload bytes identical to today's 5 OTel sketch +processors when given the same input. This is the gate before the +shim refactors in Phase 2 steps 2.5–2.9. ADR-0002 §"Behavior +preservation" promises wire-format invariance — this harness is +the test that verifies it. + +This is the **runtime-implementation** layer of the three-gate parity +pyramid (see [`../README.md`](../README.md) for the full layering). + +## Run + +``` +cd integration/parity/runtime-impl +go test -v ./... +``` + +The harness is fully synchronous: no goroutines, no wall-clock waits, +no flake-prone timers. Two runs of the same harness produce +byte-identical inputs and outputs. + +## Subtests + +- `TestParity_AllSketches` — runs all 5 sketch comparisons in one + pass, reusing one input/output build. +- `TestParity_{DDSketch,KLL,HLL,CountSketch,CountMinSketch}` — + isolated per-sketch tests, useful for `go test -run`. + +CountSketch and CountMinSketch SKIP today — see the parity test's +`skipReason` for the structural divergence and the PR description's +follow-up list for the planned fixes. + +## Golden fixtures + +`golden/*.bin` holds the canonical envelope bytes consumed by the Rust +side cross-language parity test +(`asap-precompute-rs/tests/cross_language_parity.rs`). Regenerate +after any wire-format change with: + +``` +GOLDEN_REGEN=1 go test -run TestGenerateGoldenFixtures ./... +``` diff --git a/integration/parity/go.mod b/integration/parity/runtime-impl/go.mod similarity index 75% rename from integration/parity/go.mod rename to integration/parity/runtime-impl/go.mod index 13684d8c..e655d1f6 100644 --- a/integration/parity/go.mod +++ b/integration/parity/runtime-impl/go.mod @@ -1,4 +1,4 @@ -module github.com/ProjectASAP/ASAPCollector/integration/parity +module github.com/ProjectASAP/ASAPCollector/integration/parity/runtime-impl go 1.25.5 @@ -54,23 +54,23 @@ require ( // Local checkouts for the host-neutral runtime, the patched OTel // collector core, and the 5 legacy sketch processors. Paths are -// relative to integration/parity/. Mirrors the cascade used by the -// per-processor go.mod files in +// relative to integration/parity/runtime-impl/. Mirrors the cascade +// used by the per-processor go.mod files in // opentelemetry-collector-contrib-patch/processor/. -replace github.com/ProjectASAP/asap-precompute-go => ../../asap-precompute-go +replace github.com/ProjectASAP/asap-precompute-go => ../../../asap-precompute-go -replace github.com/ProjectASAP/sketchlib-go => ../../../sketchlib-go +replace github.com/ProjectASAP/sketchlib-go => ../../../../sketchlib-go -replace go.opentelemetry.io/collector/pdata => ../../opentelemetry-collector/pdata +replace go.opentelemetry.io/collector/pdata => ../../../opentelemetry-collector/pdata -replace go.opentelemetry.io/collector/processor => ../../opentelemetry-collector/processor +replace go.opentelemetry.io/collector/processor => ../../../opentelemetry-collector/processor -replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/ddsketchprocessor => ../../opentelemetry-collector-contrib-patch/processor/ddsketchprocessor +replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/ddsketchprocessor => ../../../opentelemetry-collector-contrib-patch/processor/ddsketchprocessor -replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/kllprocessor => ../../opentelemetry-collector-contrib-patch/processor/kllprocessor +replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/kllprocessor => ../../../opentelemetry-collector-contrib-patch/processor/kllprocessor -replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/hllprocessor => ../../opentelemetry-collector-contrib-patch/processor/hllprocessor +replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/hllprocessor => ../../../opentelemetry-collector-contrib-patch/processor/hllprocessor -replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/countsketchprocessor => ../../opentelemetry-collector-contrib-patch/processor/countsketchprocessor +replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/countsketchprocessor => ../../../opentelemetry-collector-contrib-patch/processor/countsketchprocessor -replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/countminsketchprocessor => ../../opentelemetry-collector-contrib-patch/processor/countminsketchprocessor +replace github.com/open-telemetry/opentelemetry-collector-contrib/processor/countminsketchprocessor => ../../../opentelemetry-collector-contrib-patch/processor/countminsketchprocessor diff --git a/integration/parity/go.sum b/integration/parity/runtime-impl/go.sum similarity index 100% rename from integration/parity/go.sum rename to integration/parity/runtime-impl/go.sum diff --git a/integration/parity/runtime-impl/golden/cms_envelope.bin b/integration/parity/runtime-impl/golden/cms_envelope.bin new file mode 100644 index 00000000..a1cc548c Binary files /dev/null and b/integration/parity/runtime-impl/golden/cms_envelope.bin differ diff --git a/integration/parity/runtime-impl/golden/countsketch_envelope.bin b/integration/parity/runtime-impl/golden/countsketch_envelope.bin new file mode 100644 index 00000000..5838d418 Binary files /dev/null and b/integration/parity/runtime-impl/golden/countsketch_envelope.bin differ diff --git a/integration/parity/runtime-impl/golden/ddsketch_envelope.bin b/integration/parity/runtime-impl/golden/ddsketch_envelope.bin new file mode 100644 index 00000000..7f6847a9 Binary files /dev/null and b/integration/parity/runtime-impl/golden/ddsketch_envelope.bin differ diff --git a/integration/parity/runtime-impl/golden/hll_envelope.bin b/integration/parity/runtime-impl/golden/hll_envelope.bin new file mode 100644 index 00000000..bc258725 Binary files /dev/null and b/integration/parity/runtime-impl/golden/hll_envelope.bin differ diff --git a/integration/parity/runtime-impl/golden/kll_envelope.bin b/integration/parity/runtime-impl/golden/kll_envelope.bin new file mode 100644 index 00000000..ef0945bf Binary files /dev/null and b/integration/parity/runtime-impl/golden/kll_envelope.bin differ diff --git a/integration/parity/golden_test.go b/integration/parity/runtime-impl/golden_test.go similarity index 99% rename from integration/parity/golden_test.go rename to integration/parity/runtime-impl/golden_test.go index bfbf9ff6..bc06afaf 100644 --- a/integration/parity/golden_test.go +++ b/integration/parity/runtime-impl/golden_test.go @@ -9,7 +9,7 @@ // // Regenerate by running: // -// GOLDEN_REGEN=1 go test -run GenerateGolden ./integration/parity/... +// GOLDEN_REGEN=1 go test -run GenerateGolden ./integration/parity/runtime-impl/... // // Without GOLDEN_REGEN set, the generator acts as a self-check that // the Go side still produces the bytes currently checked in. diff --git a/integration/parity/harness/diff.go b/integration/parity/runtime-impl/harness/diff.go similarity index 100% rename from integration/parity/harness/diff.go rename to integration/parity/runtime-impl/harness/diff.go diff --git a/integration/parity/harness/input.go b/integration/parity/runtime-impl/harness/input.go similarity index 100% rename from integration/parity/harness/input.go rename to integration/parity/runtime-impl/harness/input.go diff --git a/integration/parity/harness/legacy.go b/integration/parity/runtime-impl/harness/legacy.go similarity index 100% rename from integration/parity/harness/legacy.go rename to integration/parity/runtime-impl/harness/legacy.go diff --git a/integration/parity/harness/runtime.go b/integration/parity/runtime-impl/harness/runtime.go similarity index 100% rename from integration/parity/harness/runtime.go rename to integration/parity/runtime-impl/harness/runtime.go diff --git a/integration/parity/harness/sketches.go b/integration/parity/runtime-impl/harness/sketches.go similarity index 100% rename from integration/parity/harness/sketches.go rename to integration/parity/runtime-impl/harness/sketches.go diff --git a/integration/parity/parity_test.go b/integration/parity/runtime-impl/parity_test.go similarity index 97% rename from integration/parity/parity_test.go rename to integration/parity/runtime-impl/parity_test.go index b73697b3..47ec6265 100644 --- a/integration/parity/parity_test.go +++ b/integration/parity/runtime-impl/parity_test.go @@ -6,13 +6,13 @@ // shims that delegate to the runtime (Phase 2 steps 2.5–2.9). // // This file holds the test entry points; the harness logic lives in -// integration/parity/harness/. +// integration/parity/runtime-impl/harness/. package parity_test import ( "testing" - "github.com/ProjectASAP/ASAPCollector/integration/parity/harness" + "github.com/ProjectASAP/ASAPCollector/integration/parity/runtime-impl/harness" ) // TestParity_AllSketches runs the full multi-metric harness once,