Files
buzz/test/otel-e2e/run.sh
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 deee33ba60 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>
2026-06-30 14:14:45 -04:00

353 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# test/otel-e2e/run.sh — OTEL E2E harness for the Buzz relay
# =============================================================================
#
# Proves both export surfaces introduced in PR #1398:
#
# (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; 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]
#
# Prerequisites:
# - Docker (for compose services + collector)
# - Rust toolchain (for relay + test binary)
# - psql on PATH (or docker exec fallback if not found)
# - Ports 3000, 4317, 5432, 6379, 9000, 9102 available
#
# The relay runs on the HOST (matching the existing host.docker.internal
# Prometheus pattern). Only postgres, redis, minio, and the otel-collector
# run in Docker.
#
# RELAY_BINARY override:
# By default this script builds the relay from REPO_ROOT. To test a relay
# built from a different branch (e.g. the otel-migration PR), set:
#
# RELAY_BINARY=/path/to/buzz-relay just otel-e2e --skip-build
#
# or just point at the otel-migration worktree's ci binary:
#
# RELAY_BINARY=$(pwd)/../buzz/.worktrees/duncan-otel-migration/target/ci/buzz-relay \
# just otel-e2e --skip-build
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
SKIP_BUILD="${1:-}"
# ── Colors ────────────────────────────────────────────────────────────────────
BLUE='\033[0;34m'
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${BLUE}[otel-e2e]${NC} $*"; }
ok() { echo -e "${GREEN}[otel-e2e ✓]${NC} $*"; }
warn() { echo -e "${YELLOW}[otel-e2e !]${NC} $*"; }
err() { echo -e "${RED}[otel-e2e ✗]${NC} $*" >&2; }
# ── Relay PID tracking ────────────────────────────────────────────────────────
RELAY_PID_FILE=/tmp/buzz-otel-e2e-relay.pid
cleanup() {
log "Tearing down..."
# Kill relay if running.
if [[ -f "${RELAY_PID_FILE}" ]]; then
RELAY_PID=$(cat "${RELAY_PID_FILE}")
if kill -0 "${RELAY_PID}" 2>/dev/null; then
kill "${RELAY_PID}" && wait "${RELAY_PID}" 2>/dev/null || true
fi
rm -f "${RELAY_PID_FILE}"
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"
}
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
# ── Wait helpers ──────────────────────────────────────────────────────────────
wait_healthy() {
local service="$1" container="$2"
log "Waiting for ${service}..."
for _ in $(seq 1 60); do
status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found")
if [[ "${status}" == "healthy" ]]; then
ok "${service} healthy"
return 0
fi
sleep 2
done
err "${service} did not become healthy within 120s"
docker logs "${container}" || true
return 1
}
wait_healthy "Postgres" "buzz-postgres"
wait_healthy "Redis" "buzz-redis"
wait_healthy "MinIO" "buzz-minio"
# otel-collector: healthcheck uses nc; retry with a TCP connect fallback.
log "Waiting for otel-collector (gRPC :4317)..."
for _ in $(seq 1 30); do
if docker inspect --format='{{.State.Health.Status}}' "buzz-otel-collector" 2>/dev/null | grep -q "healthy"; then
ok "otel-collector healthy"
break
fi
# Fallback: try TCP connect from host.
if nc -z -w1 127.0.0.1 4317 2>/dev/null; then
ok "otel-collector reachable on :4317"
break
fi
sleep 2
done
# ── Step 2: Apply DB schema ───────────────────────────────────────────────────
log "Applying database schema..."
export PGHOST=localhost PGPORT=5432 PGUSER=buzz PGPASSWORD=buzz_dev PGDATABASE=buzz
if command -v psql >/dev/null 2>&1; then
seed_psql() { PGPASSWORD="${PGPASSWORD}" psql -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" -d "${PGDATABASE}" -qtA "$@"; }
else
seed_psql() { docker exec -e PGPASSWORD="${PGPASSWORD}" buzz-postgres psql -U "${PGUSER}" -d "${PGDATABASE}" -qtA "$@"; }
fi
"${REPO_ROOT}/bin/pgschema" apply --file "${REPO_ROOT}/schema/schema.sql" --auto-approve
docker exec -i -e PGPASSWORD="${PGPASSWORD}" buzz-postgres \
psql -U "${PGUSER}" -d "${PGDATABASE}" -v ON_ERROR_STOP=1 \
< "${REPO_ROOT}/scripts/attach-schema-partitions.sql"
seed_psql -c "
INSERT INTO communities (id, host)
VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000')
ON CONFLICT (lower(host)) DO NOTHING;
"
ok "Schema applied and community seeded"
# ── Step 3: Build relay (unless --skip-build) ────────────────────────────────
RELAY_BIN="${RELAY_BINARY:-${REPO_ROOT}/target/ci/buzz-relay}"
if [[ "${SKIP_BUILD}" != "--skip-build" ]]; then
log "Building relay (profile: ci)..."
cd "${REPO_ROOT}"
cargo build --profile ci -p buzz-relay
RELAY_BIN="${REPO_ROOT}/target/ci/buzz-relay"
ok "Relay built"
else
if [[ ! -x "${RELAY_BIN}" ]]; then
err "RELAY_BINARY=${RELAY_BIN} does not exist or is not executable"
err "Either remove --skip-build to build, or set RELAY_BINARY to a pre-built binary"
exit 1
fi
log "Using pre-built relay: ${RELAY_BIN}"
fi
# ── Step 4: Set up collector output readback ─────────────────────────────────
# 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)..."
RELAY_LOG=/tmp/buzz-otel-e2e-relay-otlp.log
nohup env \
DATABASE_URL="postgres://buzz:buzz_dev@localhost:5432/buzz" \
REDIS_URL="redis://localhost:6379" \
RELAY_URL="ws://localhost:3000" \
BUZZ_BIND_ADDR="0.0.0.0:3000" \
BUZZ_REQUIRE_AUTH_TOKEN=false \
BUZZ_RECONCILE_CHANNELS=true \
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" \
RUST_LOG="buzz_relay=info" \
"${RELAY_BIN}" > "${RELAY_LOG}" 2>&1 &
RELAY_PID=$!
echo "${RELAY_PID}" > "${RELAY_PID_FILE}"
log "Waiting for relay readiness (OTLP enabled)..."
for attempt in $(seq 1 60); do
if ! kill -0 "${RELAY_PID}" 2>/dev/null; then
err "Relay process died during startup"
cat "${RELAY_LOG}"
exit 1
fi
status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true)
if [[ "${status_code}" == "200" ]]; then
ok "Relay ready (OTLP enabled)"
break
fi
sleep 1
if [[ "${attempt}" -eq 60 ]]; then
err "Relay did not become ready within 60s"
cat "${RELAY_LOG}"
exit 1
fi
done
# ── 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"
log "Running OTLP-enabled tests (a)(b)..."
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_prometheus_contains_buzz_metrics \
-- --ignored 2>&1
# Note: test_prometheus_contains_buzz_metrics matches test_prometheus_contains_buzz_metrics_with_nonzero_values
# Give traces time to flush from the test's WS interaction.
sleep 5
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_traces \
-- --ignored 2>&1
ok "OTLP-enabled assertions passed"
# ── Step 7: Stop the OTLP-enabled relay ──────────────────────────────────────
log "Stopping OTLP-enabled relay..."
kill "${RELAY_PID}" && wait "${RELAY_PID}" 2>/dev/null || true
rm -f "${RELAY_PID_FILE}"
# Save collector output for inspection.
log "Saving collector output for inspection..."
COLLECTOR_LINE_COUNT=$(wc -l < "${COLLECTOR_OUTPUT_HOST}" 2>/dev/null || echo 0)
ok "Collector received data: ${COLLECTOR_LINE_COUNT} lines at ${COLLECTOR_OUTPUT_HOST}"
# Print the span names that appeared for human inspection.
log "Span names found in collector output:"
cat "${COLLECTOR_OUTPUT_HOST}" | python3 -c "
import sys, json, re
spans = set()
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
blob = json.dumps(json.loads(line))
spans.update(re.findall(r'\"name\":\s*\"([^\"]+)\"', blob))
except Exception:
pass
for s in sorted(spans):
print(' ', s)
" || true
# ── Step 8: Clear collector output, start relay WITHOUT OTLP ─────────────────
log "Clearing collector output for disabled-path test..."
rm -f "${COLLECTOR_OUTPUT_HOST}"
sleep 1
log "Starting relay (OTLP DISABLED)..."
RELAY_LOG_DISABLED=/tmp/buzz-otel-e2e-relay-nootlp.log
nohup env \
DATABASE_URL="postgres://buzz:buzz_dev@localhost:5432/buzz" \
REDIS_URL="redis://localhost:6379" \
RELAY_URL="ws://localhost:3000" \
BUZZ_BIND_ADDR="0.0.0.0:3000" \
BUZZ_REQUIRE_AUTH_TOKEN=false \
BUZZ_RECONCILE_CHANNELS=true \
RUST_LOG="buzz_relay=info" \
"${RELAY_BIN}" > "${RELAY_LOG_DISABLED}" 2>&1 &
RELAY_PID=$!
echo "${RELAY_PID}" > "${RELAY_PID_FILE}"
log "Waiting for relay readiness (OTLP disabled)..."
for attempt in $(seq 1 60); do
if ! kill -0 "${RELAY_PID}" 2>/dev/null; then
err "Relay process died during startup (OTLP disabled)"
cat "${RELAY_LOG_DISABLED}"
exit 1
fi
status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true)
if [[ "${status_code}" == "200" ]]; then
ok "Relay ready (OTLP disabled)"
break
fi
sleep 1
if [[ "${attempt}" -eq 60 ]]; then
err "Relay did not become ready within 60s (OTLP disabled)"
cat "${RELAY_LOG_DISABLED}"
exit 1
fi
done
# ── Step 9: Run disabled-path test (d) ───────────────────────────────────────
log "Running OTLP-disabled control test (d)..."
RELAY_URL="ws://localhost:3000" \
METRICS_URL="http://localhost:9102/metrics" \
cargo test -p buzz-test-client --test e2e_otel \
test_prometheus_works_without_otlp_endpoint \
-- --ignored 2>&1
ok "OTLP-disabled assertion passed"
# Stop relay.
kill "${RELAY_PID}" && wait "${RELAY_PID}" 2>/dev/null || true
rm -f "${RELAY_PID_FILE}"
# Assert collector received NOTHING during the disabled run.
log "Asserting collector received nothing during OTLP-disabled run..."
sleep 3 # Give any possible OTLP traffic time to arrive (there should be none)
DISABLED_OUTPUT=""
if [[ -f "${COLLECTOR_OUTPUT_HOST}" ]]; then
DISABLED_OUTPUT=$(cat "${COLLECTOR_OUTPUT_HOST}" | tr -d '[:space:]')
fi
if [[ -n "${DISABLED_OUTPUT}" ]]; then
err "OTLP-disabled assertion FAILED: collector received data when OTEL_EXPORTER_OTLP_ENDPOINT was unset"
head -5 "${COLLECTOR_OUTPUT_HOST}"
exit 1
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}"
echo -e "${GREEN}║ OTEL E2E HARNESS — ALL ASSERTIONS PASSED ║${NC}"
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}║ service.name=buzz-relay verified structurally ║${NC}"
echo -e "${GREEN}║ (d) OTLP-disabled: /metrics works, collector silent ║${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"