| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- #!/usr/bin/env bash
- # Live M6 smoke test. Run from repo root:
- # bash scripts/m6_smoke.sh
- #
- # Walks through the 5 scenarios in M6_VERIFICATION.md:
- #
- # Step 2 — sliding-window ×N: 5 identical alerts → recipient sees
- # the (×5) suffix on the 5th message
- # Step 3 — dedupe_count metric: max-observed gauge and
- # dedupe_collapsed counter both tick
- # Step 4 — sliding TTL: a duplicate after > TTL is a fresh "new"
- # Step 5 — free-for-dupes: 200 dupes/s never trigger the rate limit
- # (per-source cap is 100/s, but only the first alert burns
- # a token; the other 199 are free)
- # Step 6 — per-source key isolation: same dedupe_key from a
- # different source does NOT collide
- #
- # Exit code is the number of failed checks.
- set -e
- cd "$(dirname "$0")/.."
- PG="docker exec -i broad-announce-postgres-1 psql -U ba -d ba -A -t"
- INGESTD_METRICS=http://localhost:8800/metrics
- FAKETGMD=http://localhost:8830
- WS_INGEST=ws://localhost:8800/v1/ingest/ws
- SRC_ACME=acme-001:prom-prod:s3cret-acme
- SRC_GLOBEX=globex-002:grafana:s3cret-globex
- fails=0
- pass() { echo " ✅ $*"; }
- fail() { echo " ❌ $*"; fails=$((fails+1)); }
- reset_state() {
- $PG -c "UPDATE individuals SET telegram_chat_id = NULL, telegram_user_id = NULL, telegram_invite_code = 'acme-bob-002' WHERE id = 'ind-acme-002';" >/dev/null
- $PG -c "UPDATE individuals SET telegram_chat_id = NULL, telegram_user_id = NULL WHERE id = 'ind-acme-003';" >/dev/null
- $PG -c "UPDATE subscriptions SET min_severity = 'critical' WHERE individual_id = 'ind-acme-002' AND source_id = 'prom-prod';" >/dev/null
- $PG -c "TRUNCATE deliveries;" >/dev/null
- curl -sS -X POST $FAKETGMD/admin/reset >/dev/null
- }
- # Per-source dedupe metric readers.
- dedupe_collapsed() {
- # $1 = source_id
- curl -sS "$INGESTD_METRICS" 2>/dev/null | \
- grep -E "^ba_ingestd_dedupe_collapsed_total\{[^}]*source=\"$1\"" | \
- awk '{print $NF}' | awk -F. '{print $1+0; exit}' | head -1
- }
- dedupe_max() {
- # $1 = source_id
- curl -sS "$INGESTD_METRICS" 2>/dev/null | \
- grep -E "^ba_ingestd_dedupe_count_max_observed\{[^}]*source=\"$1\"" | \
- awk '{print $NF}' | awk -F. '{print $1+0; exit}' | head -1
- }
- ws_messages() {
- # $1 = result label
- curl -sS "$INGESTD_METRICS" 2>/dev/null | \
- grep -E "^ba_ingestd_ws_messages_total\{result=\"$1\"" | \
- awk '{print $NF}' | awk -F. '{print $1+0; exit}' | head -1
- }
- # ── Setup: build loadgen-ws ──
- if [[ ! -x /tmp/loadgen-ws-m6 ]]; then
- echo "▸ Building /tmp/loadgen-ws-m6"
- (cd loadgen && CGO_ENABLED=0 go build -o /tmp/loadgen-ws-m6 ./cmd/ws)
- fi
- # ─────────────────────────────────────────────────────────────────
- echo "── M6 smoke — Dedupe + ×N suffix + sliding TTL + free-for-dupes ──"
- echo ""
- # ── Step 2: ×N suffix on the 5th message ───────────────────
- echo "── Step 2: 5 identical alerts → (×5) suffix on the last ──"
- reset_state
- sleep 1 # let the dedupe window drain
- KEY="m6-step2-$(date +%s%N)"
- # We use a unique key per run so the dedupe count starts at 1.
- /tmp/loadgen-ws-m6 --target "$WS_INGEST" --api-key "$SRC_ACME" --count 5 --rate 10 --dedupe-key "$KEY" 2>&1 | tail -2
- sleep 3
- # Read the last faketgmd message; expect "(×5)" in the title.
- last_text=$(curl -sS $FAKETGMD/admin/sent 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); items=d.get("items",[]); print(items[-1]["text"] if items else "")' 2>/dev/null)
- if echo "$last_text" | grep -q '(×5)'; then
- pass "faketgmd last message contains '(×5)'"
- else
- fail "faketgmd last message: $last_text (expected '(×5)' suffix)"
- fi
- # ── Step 3: metrics tick correctly ────────────────────────
- echo ""
- echo "── Step 3: dedupe_collapsed and dedupe_count_max_observed both tick ──"
- collapsed_before=$(dedupe_collapsed prom-prod)
- max_before=$(dedupe_max prom-prod)
- collapsed_before=${collapsed_before:-0}
- max_before=${max_before:-0}
- KEY="m6-step3-$(date +%s%N)"
- /tmp/loadgen-ws-m6 --target "$WS_INGEST" --api-key "$SRC_ACME" --count 5 --rate 10 --dedupe-key "$KEY" 2>&1 | tail -2
- sleep 2
- collapsed_after=$(dedupe_collapsed prom-prod)
- max_after=$(dedupe_max prom-prod)
- collapsed_after=${collapsed_after:-0}
- max_after=${max_after:-0}
- collapsed_delta=$((collapsed_after - collapsed_before))
- max_delta=$((max_after - max_before))
- if [[ $collapsed_delta -ge 4 ]]; then
- pass "dedupe_collapsed_total{source=\"prom-prod\"} +$collapsed_delta (≥4 of 5 are dupes)"
- else
- fail "dedupe_collapsed_total delta was $collapsed_delta (expected ≥4)"
- fi
- if [[ $max_after -ge 5 ]]; then
- pass "dedupe_count_max_observed{source=\"prom-prod\"} = $max_after (≥5)"
- else
- fail "dedupe_count_max_observed was $max_after (expected ≥5)"
- fi
- # ── Step 4: sliding TTL — a fresh dedupe_key is "new" ──────
- echo ""
- echo "── Step 4: fresh dedupe_key (no collision with prior burst) → all accepted as new ──"
- reset_state
- sleep 1
- # Use a brand new key; the loadgen's normal profile would also
- # pick a unique key per alert, but to keep this deterministic
- # we use --dedupe-key with a timestamp.
- KEY="m6-step4-$(date +%s%N)"
- recv_before=$(ws_messages received)
- acc_before=$(ws_messages accepted)
- /tmp/loadgen-ws-m6 --target "$WS_INGEST" --api-key "$SRC_ACME" --count 3 --rate 5 --dedupe-key "$KEY" 2>&1 | tail -2
- sleep 2
- recv_after=$(ws_messages received)
- acc_after=$(ws_messages accepted)
- recv_delta=$((recv_after - recv_before))
- acc_delta=$((acc_after - acc_before))
- if [[ $recv_delta -eq 3 ]]; then
- pass "ws_messages_total{result=\"received\"} +$recv_delta"
- else
- fail "ws_messages_total{result=\"received\"} delta was $recv_delta (expected 3)"
- fi
- if [[ $acc_delta -eq 3 ]]; then
- pass "ws_messages_total{result=\"accepted\"} +$acc_delta (all 3 new, fresh key)"
- else
- fail "ws_messages_total{result=\"accepted\"} delta was $acc_delta (expected 3)"
- fi
- # ── Step 5: free-for-dupes — rate limit never fires on a dupe storm ──
- echo ""
- echo "── Step 5: 200 dupes/s (10× source rate limit) → no rate_limited_source ──"
- # The default per-source rate is 100/s in docker-compose. We
- # blast 300 dupes/s (3× over) for 2 seconds and confirm the
- # rate_limited_source counter doesn't tick.
- reset_state
- rl_before=$(curl -sS "$INGESTD_METRICS" 2>/dev/null | grep -E '^ba_ingestd_rate_limit_hits_total\{[^}]*scope="source"' | awk '{print $NF}' | awk -F. '{print $1+0; exit}' | head -1)
- rl_before=${rl_before:-0}
- ws_acc_before=$(ws_messages accepted)
- KEY="m6-step5-$(date +%s%N)"
- /tmp/loadgen-ws-m6 --target "$WS_INGEST" --api-key "$SRC_ACME" --count 600 --rate 300 --dedupe-key "$KEY" 2>&1 | tail -2
- sleep 3
- rl_after=$(curl -sS "$INGESTD_METRICS" 2>/dev/null | grep -E '^ba_ingestd_rate_limit_hits_total\{[^}]*scope="source"' | awk '{print $NF}' | awk -F. '{print $1+0; exit}' | head -1)
- ws_acc_after=$(ws_messages accepted)
- rl_delta=$((rl_after - rl_before))
- accepted_delta=$((ws_acc_after - ws_acc_before))
- if [[ $rl_delta -eq 0 ]]; then
- pass "rate_limit_hits_total{scope=\"source\"} did not tick (free-for-dupes works)"
- else
- fail "rate_limit_hits_total{scope=\"source\"} +$rl_delta (expected 0)"
- fi
- if [[ $accepted_delta -ge 500 ]]; then
- pass "ws_messages_total{result=\"accepted\"} +$accepted_delta (≥500 of 600 accepted)"
- else
- fail "ws_messages_total{result=\"accepted\"} +$accepted_delta (expected ≥500)"
- fi
- # ── Step 6: per-source isolation ─────────────────────────
- echo ""
- echo "── Step 6: same dedupe_key from a different source does NOT collide ──"
- reset_state
- sleep 1
- KEY="m6-step6-$(date +%s%N)"
- globex_max_before=$(dedupe_max grafana)
- globex_max_before=${globex_max_before:-0}
- # Send 3 from acme, 3 from globex, all with the same key.
- # Expect: acme counts 1,2,3; globex counts 1,2,3 (no cross-pollination).
- /tmp/loadgen-ws-m6 --target "$WS_INGEST" --api-key "$SRC_ACME" --count 3 --rate 5 --dedupe-key "$KEY" 2>&1 | tail -2
- sleep 1
- /tmp/loadgen-ws-m6 --target "$WS_INGEST" --api-key "$SRC_GLOBEX" --count 3 --rate 5 --dedupe-key "$KEY" 2>&1 | tail -2
- sleep 2
- globex_max_after=$(dedupe_max grafana)
- globex_max_after=${globex_max_after:-0}
- if [[ $globex_max_after -ge 3 ]]; then
- pass "dedupe_count_max_observed{source=\"grafana\"} = $globex_max_after (≥3 from globex)"
- else
- fail "dedupe_count_max_observed{source=\"grafana\"} was $globex_max_after (expected ≥3)"
- fi
- # Also: acme's max should be ≥3 but not affected by globex's hits
- # (it should be the same as it was after Step 5's storm). We
- # don't assert a delta here — just that the gauge is non-zero.
- acme_max=$(dedupe_max prom-prod)
- acme_max=${acme_max:-0}
- if [[ $acme_max -ge 1 ]]; then
- pass "dedupe_count_max_observed{source=\"prom-prod\"} = $acme_max (per-source isolation preserved)"
- else
- fail "dedupe_count_max_observed{source=\"prom-prod\"} was $acme_max (expected ≥1)"
- fi
- # ── Summary ─────────────────────────────────────────────────
- echo ""
- if [[ $fails -eq 0 ]]; then
- echo "🟢 M6 smoke PASS — all checks green"
- exit 0
- else
- echo "🔴 M6 smoke FAIL — $fails check(s) failed"
- exit $fails
- fi
|