test(otel-e2e): fix Thufir-flagged correctness issues in E2E harness

Three correctness fixes (all IMPORTANT from Thufir's review) + one MINOR:

1. Per-run isolated collector output dir (mktemp -d): stale data from a
   prior run can no longer false-green the OTLP trace assertions. The dir
   is passed into compose via BUZZ_OTEL_OUTPUT_DIR env var (replaces the
   hard-coded /tmp/buzz-otel-e2e-output bind-mount). Cleaned on success;
   preserved on failure for inspection.

2. Delete test_otlp_metrics_tagged_service_name_buzz_relay: descoped per
   Tyler/Max decision — relay drops OTLP metrics export entirely; metrics
   go out the Prometheus :9102 scrape only; OTLP carries traces only.

3. Remove OTEL_SERVICE_NAME from enabled relay start so the harness
   exercises the relay's Finding-A fallback exactly as staging does.
   Replace blob.contains("buzz-relay") substring check with a structural
   has_resource_attr_in_spans() assertion that walks ResourceSpans[].
   resource.attributes[]{key, value.stringValue} — a span/scope name
   containing "buzz-relay" can no longer satisfy this check.

4. README: note Rust tests are harness internals, not standalone; update
   manual-run example to match new env var shape.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-06-30 14:14:45 -04:00
parent fd59e6660f
commit deee33ba60
4 changed files with 113 additions and 77 deletions

View File

@@ -8,10 +8,11 @@ Local end-to-end validation for the Buzz relay's observability export surfaces
| Assertion | What's checked |
|-----------|---------------|
| **(a) Prometheus scrape** | `GET :9102/metrics` contains `buzz_ws_connections_total`, `buzz_events_received_total`, `buzz_auth_attempts_total` with **non-zero values**; does **not** contain `target_info` (suppressed via `.without_target_info()`) |
| **(b) OTLP traces** | The otel-collector received spans named `ws.auth` and `ws.event` carrying a `conn_id` attribute, tagged `service.name=buzz-relay` |
| **(c) OTLP metrics** | The otel-collector received OTLP metric data tagged `service.name=buzz-relay` |
| **(b) OTLP traces** | The otel-collector received spans named `ws.auth` and `ws.event` carrying a `conn_id` attribute; `service.name=buzz-relay` verified **structurally** via `resource.attributes` key/value (not substring) |
| **(d) OTLP-disabled control** | With `OTEL_EXPORTER_OTLP_ENDPOINT` unset the relay still serves `/metrics` correctly; the collector receives **nothing** |
OTLP metrics export is out of scope — the relay emits metrics via the Prometheus `:9102` scrape only; OTLP carries traces only.
## Prerequisites
- Docker (for compose services: postgres, redis, minio, otel-collector)
@@ -65,7 +66,7 @@ RELAY_BINARY=/path/to/buzz/.worktrees/duncan-otel-migration/target/ci/buzz-relay
▼ ▼
┌─────── DOCKER (buzz-net) ──────────────────────────────────────┐
│ │
│ otel-collector :4317 ←── relay pushes traces + metrics here │
│ otel-collector :4317 ←── relay pushes traces here │
│ exports to: debug stdout + /tmp/otelcol-output/ │
│ │
│ postgres :5432 redis :6379 minio :9000 │
@@ -96,24 +97,36 @@ All four tests are `#[ignore]` by default and selected by `run.sh`.
## Running individual tests manually
> **Note:** The Rust tests in `crates/buzz-test-client/tests/e2e_otel.rs` are
> **harness internals**. They depend on the two-relay sequence orchestrated by
> `run.sh` (OTLP-enabled run first, then OTLP-disabled run), and the OTLP trace
> assertions read collector output written during the first relay's run. Running
> the tests in isolation will fail or produce misleading results unless you
> manually replicate the same sequence. Use `just otel-e2e` as the canonical
> entry point.
For development/debugging, you can reproduce the harness sequence manually:
```bash
# Start the stack first:
# Start the stack first (per-run output dir is required):
export BUZZ_OTEL_OUTPUT_DIR=$(mktemp -d)
docker compose -f docker-compose.yml -f test/otel-e2e/compose.otel-e2e.yml \
up -d postgres redis minio minio-init otel-collector
# Start relay with OTLP enabled:
# Start relay with OTLP enabled (no OTEL_SERVICE_NAME — relay defaults it):
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_SERVICE_NAME=buzz-relay \
DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz \
REDIS_URL=redis://localhost:6379 \
RELAY_URL=ws://localhost:3000 \
BUZZ_REQUIRE_AUTH_TOKEN=false \
./target/ci/buzz-relay &
# Run all four tests:
# Run the Prometheus + OTLP trace assertions:
RELAY_URL=ws://localhost:3000 \
METRICS_URL=http://localhost:9102/metrics \
cargo test -p buzz-test-client --test e2e_otel -- --ignored
OTEL_COLLECTOR_OUTPUT=${BUZZ_OTEL_OUTPUT_DIR}/telemetry.json \
cargo test -p buzz-test-client --test e2e_otel \
test_prometheus_contains_buzz_metrics test_otlp_traces -- --ignored
# Teardown:
docker compose -f docker-compose.yml -f test/otel-e2e/compose.otel-e2e.yml down -v

View File

@@ -17,6 +17,10 @@
# The relay is pointed at the collector via:
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# (the collector's gRPC port is published to the host).
#
# BUZZ_OTEL_OUTPUT_DIR must be set to a per-run directory created by run.sh
# (via mktemp -d) before docker compose up. This prevents stale data from a
# prior run from satisfying the OTLP trace assertions.
name: buzz
services:
@@ -26,9 +30,10 @@ services:
command: ["--config=/etc/otelcol-config.yml"]
volumes:
- ./test/otel-e2e/otelcol-config.yml:/etc/otelcol-config.yml:ro
# Bind-mount to the host so assertions can read the file directly —
# the collector image is distroless (no shell/cat available via docker exec).
- /tmp/buzz-otel-e2e-output:/tmp/otelcol-output
# Per-run bind-mount set by run.sh (BUZZ_OTEL_OUTPUT_DIR=$(mktemp -d)).
# The collector image is distroless — no shell available via docker exec,
# so assertions read from the host path directly.
- ${BUZZ_OTEL_OUTPUT_DIR}:/tmp/otelcol-output
ports:
# OTLP gRPC — relay (on host) dials this
- "4317:4317"

View File

@@ -8,11 +8,14 @@
# (a) Prometheus /metrics contains expected buzz_* series with non-zero
# values and does NOT contain a target_info series.
# (b) OTLP traces: the collector received ws.auth and ws.event spans
# carrying a conn_id attribute tagged service.name=buzz-relay.
# (c) OTLP metrics: the collector received data tagged service.name=buzz-relay.
# carrying a conn_id attribute; service.name=buzz-relay verified
# structurally via resource.attributes (not substring).
# (d) OTLP-disabled control: with OTEL_EXPORTER_OTLP_ENDPOINT unset the
# relay still serves /metrics; the collector receives nothing.
#
# Note: OTLP metrics export is out of scope — relay emits metrics via the
# Prometheus :9102 scrape only; OTLP carries traces only.
#
# Usage:
# just otel-e2e # canonical one-command entry point
# ./test/otel-e2e/run.sh [--skip-build]
@@ -72,6 +75,7 @@ cleanup() {
fi
# Bring down compose stack (including collector) and remove volumes.
cd "${REPO_ROOT}"
BUZZ_OTEL_OUTPUT_DIR="${BUZZ_OTEL_OUTPUT_DIR:-/tmp/buzz-otel-e2e-missing}" \
docker compose -f docker-compose.yml -f test/otel-e2e/compose.otel-e2e.yml \
down -v --remove-orphans 2>/dev/null || true
ok "Teardown complete"
@@ -81,6 +85,14 @@ trap cleanup EXIT
# ── Step 1: Start backing services + otel-collector ──────────────────────────
cd "${REPO_ROOT}"
# Create a per-run isolated collector output dir so stale data from a prior run
# can never satisfy the OTLP trace assertions. Cleaned on success; preserved on
# failure so the output is inspectable.
BUZZ_OTEL_OUTPUT_DIR=$(mktemp -d)
export BUZZ_OTEL_OUTPUT_DIR
log "Collector output dir: ${BUZZ_OTEL_OUTPUT_DIR}"
COLLECTOR_OUTPUT_HOST="${BUZZ_OTEL_OUTPUT_DIR}/telemetry.json"
log "Starting backing services and otel-collector..."
docker compose -f docker-compose.yml -f test/otel-e2e/compose.otel-e2e.yml \
up -d postgres redis minio minio-init otel-collector
@@ -161,12 +173,10 @@ else
fi
# ── Step 4: Set up collector output readback ─────────────────────────────────
# The file exporter writes to /tmp/otelcol-output inside the collector container,
# which is bind-mounted to /tmp/buzz-otel-e2e-output on the host (see compose overlay).
# The collector image is distroless so we read directly from the host path.
COLLECTOR_OUTPUT_DIR=/tmp/buzz-otel-e2e-output
mkdir -p "${COLLECTOR_OUTPUT_DIR}"
COLLECTOR_OUTPUT_HOST="${COLLECTOR_OUTPUT_DIR}/telemetry.json"
# COLLECTOR_OUTPUT_HOST is already set to ${BUZZ_OTEL_OUTPUT_DIR}/telemetry.json
# from Step 1 (per-run mktemp -d). The file exporter writes
# /tmp/otelcol-output/telemetry.json inside the container, which maps to that
# host path via the compose bind-mount.
# ── Step 5: Start relay WITH OTLP enabled ────────────────────────────────────
log "Starting relay (OTLP enabled → http://localhost:4317)..."
@@ -180,7 +190,6 @@ nohup env \
BUZZ_REQUIRE_AUTH_TOKEN=false \
BUZZ_RECONCILE_CHANNELS=true \
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" \
OTEL_SERVICE_NAME="buzz-relay" \
RUST_LOG="buzz_relay=info" \
"${RELAY_BIN}" > "${RELAY_LOG}" 2>&1 &
RELAY_PID=$!
@@ -206,14 +215,14 @@ for attempt in $(seq 1 60); do
fi
done
# ── Step 6: Run OTLP-enabled tests (a), (b), (c) ────────────────────────────
# ── Step 6: Run OTLP-enabled tests (a), (b) ─────────────────────────────────
log "Waiting for OTLP exports to flush (batch processor 1s timeout + buffer)..."
sleep 5 # Give the relay's batch exporter time to flush after readiness
COLLECTOR_LINE_COUNT=$(wc -l < "${COLLECTOR_OUTPUT_HOST}" 2>/dev/null || echo 0)
log "Collector has ${COLLECTOR_LINE_COUNT} lines so far (may be low — OTLP metrics are periodic)"
log "Collector has ${COLLECTOR_LINE_COUNT} lines so far"
log "Running OTLP-enabled tests (a)(b)(c)..."
log "Running OTLP-enabled tests (a)(b)..."
RELAY_URL="ws://localhost:3000" \
METRICS_URL="http://localhost:9102/metrics" \
OTEL_COLLECTOR_OUTPUT="${COLLECTOR_OUTPUT_HOST}" \
@@ -229,7 +238,7 @@ RELAY_URL="ws://localhost:3000" \
METRICS_URL="http://localhost:9102/metrics" \
OTEL_COLLECTOR_OUTPUT="${COLLECTOR_OUTPUT_HOST}" \
cargo test -p buzz-test-client --test e2e_otel \
test_otlp \
test_otlp_traces \
-- --ignored 2>&1
ok "OTLP-enabled assertions passed"
@@ -327,6 +336,9 @@ if [[ -n "${DISABLED_OUTPUT}" ]]; then
fi
ok "OTLP-disabled: collector received nothing ✓"
# Clean up the per-run collector output dir on success.
rm -rf "${BUZZ_OTEL_OUTPUT_DIR}"
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
@@ -334,7 +346,7 @@ echo -e "${GREEN}║ OTEL E2E HARNESS — ALL ASSERTIONS PASSED
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
echo -e "${GREEN}║ (a) Prometheus buzz_* series present, non-zero, no ║${NC}"
echo -e "${GREEN}║ target_info ║${NC}"
echo -e "${GREEN}║ (b) OTLP traces: ws.auth + ws.event with conn_id ║${NC}"
echo -e "${GREEN}║ (c) OTLP metrics: service.name=buzz-relay present ║${NC}"
echo -e "${GREEN}║ (b) OTLP traces: ws.auth + ws.event with conn_id, ║${NC}"
echo -e "${GREEN}║ service.name=buzz-relay verified structurally ║${NC}"
echo -e "${GREEN}║ (d) OTLP-disabled: /metrics works, collector silent ║${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"