From 3d4e883cdb58f3b2be2429f0c14c4a6978058cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Fri, 17 Jul 2026 15:47:57 +0200 Subject: [PATCH 1/8] monitoring: richer colour for ok/info/warn/error lines --- scripts/taler-monitoring/lib.sh | 102 +++++++++++++++++--------------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/scripts/taler-monitoring/lib.sh b/scripts/taler-monitoring/lib.sh index 5df0819..a4c516a 100755 --- a/scripts/taler-monitoring/lib.sh +++ b/scripts/taler-monitoring/lib.sh @@ -352,17 +352,19 @@ koopa_ssh_python() { with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 - } -# ANSI colours for tags (green OK / yellow WARN / red ERROR·BLOCKER / cyan INFO). -# Default: always on so pasted logs keep visible tags. Disable: NO_COLOR=1 or CLICOLOR=0. +# ANSI colours — tag + body (label bold, detail dim). Off: NO_COLOR=1 or CLICOLOR=0. if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then - G= R= Y= C= N= B= + G= R= Y= C= M= D= W= B= N= else - G=$'\e[1;32m' # bold green [OK] - R=$'\e[1;31m' # bold red [ERROR] [BLOCKER] - Y=$'\e[1;33m' # bold yellow [WARN] - C=$'\e[1;36m' # bold cyan [INFO] - B=$'\e[1m' - N=$'\e[0m' + G=$'\e[1;32m' # bold green [OK] + R=$'\e[1;31m' # bold red [ERROR] + Y=$'\e[1;33m' # bold yellow [WARN] + C=$'\e[1;36m' # bold cyan [INFO] + M=$'\e[1;35m' # bold magenta [BLOCKER] + D=$'\e[2m' # dim tid / detail / sep + W=$'\e[1;37m' # bold white main label text + B=$'\e[1m' # bold section headers + N=$'\e[0m' # reset fi PASS_N=0 @@ -392,47 +394,54 @@ _take_tid() { TEST_N=$((TEST_N + 1)) LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N") } +# Dim test id: "www-001 " or empty _fmt_tid() { - # prefix "www-001 " or empty if [ -n "${LAST_TID:-}" ]; then - printf '%s ' "$LAST_TID" + printf '%s%s%s ' "$D" "$LAST_TID" "$N" + fi +} +# One concrete line: [TAG] tid label · detail +# $1=tag colour $2=tag text $3=label colour $4=label $5=detail (optional) +_msg_line() { + local tcol="$1" tag="$2" lcol="$3" label="$4" detail="${5:-}" + # Fixed-width tag column: "[OK] " / "[BLOCKER]" (8 chars inside incl. brackets) + local tcell + tcell=$(printf '%-9s' "[${tag}]") + if [ -n "$detail" ]; then + printf '%s%s%s %s%s%s%s %s·%s %s%s%s\n' \ + "$tcol" "$tcell" "$N" "$(_fmt_tid)" "$lcol" "$label" "$N" "$D" "$N" "$D" "$detail" "$N" + else + printf '%s%s%s %s%s%s%s\n' \ + "$tcol" "$tcell" "$N" "$(_fmt_tid)" "$lcol" "$label" "$N" fi } ok() { - # Forms (same idea as info/warn): - # ok "what passed" - # ok "what passed" "detail / ms / bytes / …" + # ok "what is good" ["concrete evidence: HTTP 200 · 12ms · …"] local label="$1" detail="${2:-}" _take_tid - if [ -n "$detail" ]; then - printf '%s[OK]%s %s%s — %s\n' "$G" "$N" "$(_fmt_tid)" "$label" "$detail" - else - printf '%s[OK]%s %s%s\n' "$G" "$N" "$(_fmt_tid)" "$label" - fi + _msg_line "$G" "OK" "$W" "$label" "$detail" PASS_N=$((PASS_N + 1)) } -# component-scoped error: err bank "libeufin down" "detail" +# component-scoped error: err bank "what failed" "why / HTTP / path" err() { local comp="$1" msg="$2" detail="${3:-}" _take_tid - printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}" + _msg_line "$R" "ERROR" "$W" "${comp}: ${msg}" "$detail" FAIL_N=$((FAIL_N + 1)) - ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}") + ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}") } -# legacy fail label ... +# fail "what failed" ["why / HTTP code / path"] fail() { local label="$1" detail="${2:-}" _take_tid - printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}" + _msg_line "$R" "ERROR" "$W" "$label" "$detail" FAIL_N=$((FAIL_N + 1)) - ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ — $detail}") + ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}") } warn() { - # Forms (same idea as err; always try to state the problem): - # warn "problem" - # warn "problem" "why / context" - # warn component "problem" "why / context" + # warn "what is soft-bad" ["why still ok to continue"] + # warn component "what" "why" local a1="${1:-}" a2="${2:-}" a3="${3:-}" local head detail _take_tid @@ -446,56 +455,51 @@ warn() { head="$a1" detail="" fi - if [ -n "$detail" ]; then - printf '%s[WARN]%s %s%s — %s\n' "$Y" "$N" "$(_fmt_tid)" "$head" "$detail" - else - printf '%s[WARN]%s %s%s\n' "$Y" "$N" "$(_fmt_tid)" "$head" - fi + _msg_line "$Y" "WARN" "$W" "$head" "$detail" WARN_N=$((WARN_N + 1)) } info() { + # info "topic" ["concrete fact / value / next step"] local label="$1" detail="${2:-}" _take_tid - if [ -n "$detail" ]; then - printf '%s[INFO]%s %s%s — %s\n' "$C" "$N" "$(_fmt_tid)" "$label" "$detail" - else - printf '%s[INFO]%s %s%s\n' "$C" "$N" "$(_fmt_tid)" "$label" - fi + _msg_line "$C" "INFO" "$W" "$label" "$detail" INFO_N=$((INFO_N + 1)) } blocker() { - # Payment/withdraw path cannot proceed because of this + # Hard stop on pay/withdraw path: blocker "step" "why it cannot continue" local step="$1" msg="$2" _take_tid - printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg" + _msg_line "$M" "BLOCKER" "$W" "${step}" "$msg" BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg") FAIL_N=$((FAIL_N + 1)) ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg") } -section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; } +section() { + printf '\n%s==%s %s%s%s %s==%s\n' "$D" "$N" "$B" "$*" "$N" "$D" "$N" +} summary() { echo "" if [ "${#BLOCKERS[@]}" -gt 0 ]; then - printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N" + printf '%s--- BLOCKERS%s %s(pay/withdraw cannot finish)%s %s---%s\n' "$M" "$N" "$D" "$N" "$M" "$N" local b for b in "${BLOCKERS[@]}"; do - printf '%s • %s%s\n' "$R" "$b" "$N" + printf '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N" done fi if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then - printf '%s--- ERRORS ---%s\n' "$R" "$N" + printf '%s--- ERRORS%s %s(failed checks)%s %s---%s\n' "$R" "$N" "$D" "$N" "$R" "$N" local e for e in "${ERRORS[@]}"; do case "$e" in BLOCKER*) continue ;; esac - printf '%s • %s%s\n' "$R" "$e" "$N" + printf '%s •%s %s%s%s\n' "$R" "$N" "$W" "$e" "$N" done fi - printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N" + printf '%stotals:%s %s%d OK%s' "$D" "$N" "$G" "$PASS_N" "$N" [ "$FAIL_N" -gt 0 ] && printf ', %s%d ERROR%s' "$R" "$FAIL_N" "$N" [ "$WARN_N" -gt 0 ] && printf ', %s%d WARN%s' "$Y" "$WARN_N" "$N" - [ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N" - [ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N" + [ "$INFO_N" -gt 0 ] && printf ', %s%d INFO%s' "$C" "$INFO_N" "$N" + [ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$M" "${#BLOCKERS[@]}" "$N" printf '\n' [ "$FAIL_N" -eq 0 ] } From f8affeb6517c369d0413ffe90d4b23416bcc9834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Fri, 17 Jul 2026 15:48:56 +0200 Subject: [PATCH 2/8] monitoring: badge backgrounds for ok/info/warn/error --- scripts/taler-monitoring/lib.sh | 61 +++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/scripts/taler-monitoring/lib.sh b/scripts/taler-monitoring/lib.sh index a4c516a..a789995 100755 --- a/scripts/taler-monitoring/lib.sh +++ b/scripts/taler-monitoring/lib.sh @@ -352,19 +352,28 @@ koopa_ssh_python() { with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 - } -# ANSI colours — tag + body (label bold, detail dim). Off: NO_COLOR=1 or CLICOLOR=0. +# ANSI colours — badge tags (fg+bg) + body (label bold, detail dim). +# Off: NO_COLOR=1 or CLICOLOR=0. Badges need a terminal that supports SGR bg. if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then G= R= Y= C= M= D= W= B= N= + BG_OK= BG_INFO= BG_WARN= BG_ERR= BG_BLK= BG_SEC= else - G=$'\e[1;32m' # bold green [OK] - R=$'\e[1;31m' # bold red [ERROR] - Y=$'\e[1;33m' # bold yellow [WARN] - C=$'\e[1;36m' # bold cyan [INFO] - M=$'\e[1;35m' # bold magenta [BLOCKER] - D=$'\e[2m' # dim tid / detail / sep - W=$'\e[1;37m' # bold white main label text - B=$'\e[1m' # bold section headers - N=$'\e[0m' # reset + G=$'\e[1;32m' # green text (totals) + R=$'\e[1;31m' # red text + Y=$'\e[1;33m' # yellow text + C=$'\e[1;36m' # cyan text + M=$'\e[1;35m' # magenta text + D=$'\e[2m' # dim tid / detail + W=$'\e[1;37m' # bold white label + B=$'\e[1m' # bold section + N=$'\e[0m' # reset + # Badge: bright text on solid background (space-padded in _msg_line) + BG_OK=$'\e[1;30;42m' # black on green + BG_INFO=$'\e[1;30;46m' # black on cyan + BG_WARN=$'\e[1;30;43m' # black on yellow + BG_ERR=$'\e[1;37;41m' # white on red + BG_BLK=$'\e[1;37;45m' # white on magenta + BG_SEC=$'\e[1;37;44m' # white on blue (section) fi PASS_N=0 @@ -401,18 +410,18 @@ _fmt_tid() { fi } # One concrete line: [TAG] tid label · detail -# $1=tag colour $2=tag text $3=label colour $4=label $5=detail (optional) +# $1=badge style (bg+fg) $2=tag text $3=label colour $4=label $5=detail _msg_line() { - local tcol="$1" tag="$2" lcol="$3" label="$4" detail="${5:-}" - # Fixed-width tag column: "[OK] " / "[BLOCKER]" (8 chars inside incl. brackets) + local badge="$1" tag="$2" lcol="$3" label="$4" detail="${5:-}" + # Fixed-width badge cell: " OK " / " BLOCKER" (9 chars, space-padded) local tcell - tcell=$(printf '%-9s' "[${tag}]") + tcell=$(printf ' %-7s ' "$tag") if [ -n "$detail" ]; then printf '%s%s%s %s%s%s%s %s·%s %s%s%s\n' \ - "$tcol" "$tcell" "$N" "$(_fmt_tid)" "$lcol" "$label" "$N" "$D" "$N" "$D" "$detail" "$N" + "$badge" "$tcell" "$N" "$(_fmt_tid)" "$lcol" "$label" "$N" "$D" "$N" "$D" "$detail" "$N" else printf '%s%s%s %s%s%s%s\n' \ - "$tcol" "$tcell" "$N" "$(_fmt_tid)" "$lcol" "$label" "$N" + "$badge" "$tcell" "$N" "$(_fmt_tid)" "$lcol" "$label" "$N" fi } @@ -420,14 +429,14 @@ ok() { # ok "what is good" ["concrete evidence: HTTP 200 · 12ms · …"] local label="$1" detail="${2:-}" _take_tid - _msg_line "$G" "OK" "$W" "$label" "$detail" + _msg_line "$BG_OK" "OK" "$W" "$label" "$detail" PASS_N=$((PASS_N + 1)) } # component-scoped error: err bank "what failed" "why / HTTP / path" err() { local comp="$1" msg="$2" detail="${3:-}" _take_tid - _msg_line "$R" "ERROR" "$W" "${comp}: ${msg}" "$detail" + _msg_line "$BG_ERR" "ERROR" "$W" "${comp}: ${msg}" "$detail" FAIL_N=$((FAIL_N + 1)) ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}") } @@ -435,7 +444,7 @@ err() { fail() { local label="$1" detail="${2:-}" _take_tid - _msg_line "$R" "ERROR" "$W" "$label" "$detail" + _msg_line "$BG_ERR" "ERROR" "$W" "$label" "$detail" FAIL_N=$((FAIL_N + 1)) ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}") } @@ -455,40 +464,42 @@ warn() { head="$a1" detail="" fi - _msg_line "$Y" "WARN" "$W" "$head" "$detail" + _msg_line "$BG_WARN" "WARN" "$W" "$head" "$detail" WARN_N=$((WARN_N + 1)) } info() { # info "topic" ["concrete fact / value / next step"] local label="$1" detail="${2:-}" _take_tid - _msg_line "$C" "INFO" "$W" "$label" "$detail" + _msg_line "$BG_INFO" "INFO" "$W" "$label" "$detail" INFO_N=$((INFO_N + 1)) } blocker() { # Hard stop on pay/withdraw path: blocker "step" "why it cannot continue" local step="$1" msg="$2" _take_tid - _msg_line "$M" "BLOCKER" "$W" "${step}" "$msg" + _msg_line "$BG_BLK" "BLOCKER" "$W" "${step}" "$msg" BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg") FAIL_N=$((FAIL_N + 1)) ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg") } section() { - printf '\n%s==%s %s%s%s %s==%s\n' "$D" "$N" "$B" "$*" "$N" "$D" "$N" + # Blue badge + bold title + local title=" $* " + printf '\n%s%s%s %s%s%s\n' "$BG_SEC" " == " "$N" "$B" "$title" "$N" } summary() { echo "" if [ "${#BLOCKERS[@]}" -gt 0 ]; then - printf '%s--- BLOCKERS%s %s(pay/withdraw cannot finish)%s %s---%s\n' "$M" "$N" "$D" "$N" "$M" "$N" + printf '%s BLOCKERS %s %s(pay/withdraw cannot finish)%s\n' "$BG_BLK" "$N" "$D" "$N" local b for b in "${BLOCKERS[@]}"; do printf '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N" done fi if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then - printf '%s--- ERRORS%s %s(failed checks)%s %s---%s\n' "$R" "$N" "$D" "$N" "$R" "$N" + printf '%s ERRORS %s %s(failed checks)%s\n' "$BG_ERR" "$N" "$D" "$N" local e for e in "${ERRORS[@]}"; do case "$e" in BLOCKER*) continue ;; esac From e1cc6d7141e92a7a98f317374f065ef6a78ec6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Fri, 17 Jul 2026 15:55:50 +0200 Subject: [PATCH 3/8] monitoring: boxed badges + area.group test IDs --- scripts/taler-monitoring/README.md | 22 +- scripts/taler-monitoring/TESTS.md | 275 +++++++------------ scripts/taler-monitoring/check_e2e.sh | 14 + scripts/taler-monitoring/check_goa_ladder.sh | 8 + scripts/taler-monitoring/check_inside.sh | 15 +- scripts/taler-monitoring/check_sanity.sh | 6 +- scripts/taler-monitoring/check_server.sh | 3 +- scripts/taler-monitoring/check_urls.sh | 16 +- scripts/taler-monitoring/check_versions.sh | 3 + scripts/taler-monitoring/lib.sh | 149 ++++++++-- 10 files changed, 286 insertions(+), 225 deletions(-) diff --git a/scripts/taler-monitoring/README.md b/scripts/taler-monitoring/README.md index 3f75b91..5428870 100644 --- a/scripts/taler-monitoring/README.md +++ b/scripts/taler-monitoring/README.md @@ -1,24 +1,26 @@ # taler-monitoring -Report for the **GOA** stack with clear severity tags and **per-area test IDs**: +Report for the **GOA** stack with boxed severity badges and **grouped test IDs** +`area.group-NN` (e.g. `www.exchange-01`, `e2e.pay-03`): | Tag | Meaning | |-----|---------| -| `[OK]` | check passed | -| `[INFO]` | inside status (container, ports, log noise) | -| `[WARN]` | degraded but maybe not fatal | -| `[ERROR]` | component problem | -| `[BLOCKER]` | **withdraw/pay path cannot complete** because of this | +| `┌ OK ┐` | check passed | +| `┌ INFO ┐` | status / note | +| `┌ WARN ┐` | degraded but maybe not fatal | +| `┌ ERROR ┐` | component problem | +| `┌ BLOCKER ┐` | **withdraw/pay path cannot complete** because of this | -IDs: **`www-001`**, **`inside-001`**, **`versions-001`**, **`sanity-001`**, **`server-001`**, **`e2e-001`** … +IDs reset per **group** inside an area — so “too many www tests” become +`www.exchange-*`, `www.bank-*`, `www.landing-*`, etc. Catalog: **[TESTS.md](./TESTS.md)**. ```text -[OK] www-001 exchange /config https://exchange…/config -[BLOCKER] e2e-015 prereq: merchant HTTP 502 +┌ OK ┐ www.exchange-01 exchange /config · HTTP 200 +┌ BLOCKER┐ e2e.prereq-02 merchant HTTP 502 ``` -End of each phase: totals + list of **BLOCKERS** and **ERRORS**. +End of each phase: totals + list of **BLOCKERS** and **ERRORS** (ids included). ## Commands diff --git a/scripts/taler-monitoring/TESTS.md b/scripts/taler-monitoring/TESTS.md index 6999b77..22a68f4 100644 --- a/scripts/taler-monitoring/TESTS.md +++ b/scripts/taler-monitoring/TESTS.md @@ -1,217 +1,132 @@ -# taler-monitoring — test IDs by area +# taler-monitoring — grouped test IDs -Every check line is numbered **per area** as `AREA-NNN` (zero-padded): - -| Area | Phase script | Meaning | -|------|--------------|---------| -| **www** | `check_urls.sh` | public HTTPS (outside-in) | -| **inside** | `check_inside.sh` | containers / processes on koopa | -| **versions** | `check_versions.sh` | deb.taler.net available + packages vs trixie | -| **sanity** | `check_sanity.sh` | public + server per component | -| **server** | `check_server.sh` | SSH host ports / processes | -| **e2e** | `check_e2e.sh` | withdraw + pay cycle | - -Format in output: +Every check line is numbered **`area.group-NN`** (two-digit group counter): ```text -[OK] www-001 exchange /config https://exchange…/config -[ERROR] e2e-012 bank-auth: admin token failed -[BLOCKER] e2e-015 prereq: merchant HTTP 502 +┌ OK ┐ www.exchange-01 exchange /config · HTTP 200 +┌ ERROR ┐ e2e.pay-03 order create failed · HTTP 502 +┌ BLOCKER┐ e2e.prereq-02 merchant secret missing ``` -IDs are assigned **in run order** within the area (`set_area` resets the counter). Optional soft checks still consume a number when they WARN. +| Area | Phase script | Groups (examples) | +|------|--------------|-------------------| +| **www** | `check_urls.sh` | `exchange` `perf` `stats` `bank` `merchant` `paivana` `landing` | +| **inside** | `check_inside.sh` | `ssh` `bank` `exchange` `merchant` `caddy` `load` | +| **versions** | `check_versions.sh` | `outside` `inside` `compare` | +| **sanity** | `check_sanity.sh` | `bank` `exchange` `merchant` | +| **server** | `check_server.sh` | (flat `server-NN` or host groups) | +| **e2e** | `check_e2e.sh` | `prereq` `load` `bank` `wallet` `atm` `settle` `pay` `shop` `paivana` `dig` `report` | +| **ladder** | `check_goa_ladder.sh` | `plan` `load` `withdraw` `pay` `report` | + +**Why groups:** flat `www-001`…`www-080` was hard to talk about. +`www.bank-04` / `e2e.atm-02` pin the failure to a logical block. + +Usage in scripts: + +```bash +set_area www +set_group exchange # → www.exchange-01, www.exchange-02, … +set_group bank # counter resets → www.bank-01 +``` + +`set_group` also prints a small group chip (`┌ www.exchange ┐`) so logs show section boundaries. +Re-entering the same group later (e.g. exchange legal after perf) **continues** NN — `www.exchange-03`, not a second `-01`. +Without `set_group`, IDs stay flat: `area-01`, `area-02`, … + +Optional soft checks still consume a number when they WARN. +Numbering follows **executed** checks (early skip may shift later NN inside the same group). --- ## www — public URLs (`./taler-monitoring.sh urls`) -| ID | Check | -|----|--------| -| www-… | exchange `/config`, currency, **alt_unit_names** | -| www-… | exchange `/keys` (+ alt_unit_names soft) | -| www-… | exchange `/intro/`, `/` (302→intro) | -| www-… | **exchange `/terms`** body (not empty / not API error) | -| www-… | **exchange `/privacy`** body | -| www-… | exchange `/terms/` (200 or redirect) | -| www-… | bank `/config`, currency, **alt_unit_names** | -| www-… | bank integration / webui / intro / `/` | -| www-… | **bank `/terms`** body | -| www-… | **bank `/privacy`** (or `/intro/privacy.html` fallback) | -| www-… | merchant `/config`, currency, currencies alt_unit_names | -| www-… | each merchant `exchanges[]` `/config` alt_unit_names | -| www-… | merchant `/intro/`, `/webui/`, `/` | -| www-… | **merchant `/terms`** body (dual-currency notice) | -| www-… | **merchant `/privacy`** body (must not be `not configured`) | -| www-… | merchant `/terms/` redirect | -| www-… | **landing (aggregated per site)**: one line for bank/merchant/exchange (`/intro` + assets + own/external link counts); failures listed as samples; soft external/shop assets compact | -| www-… | cross-links: one line (4 intros) on local stack | -| www-… | bank demo-withdraw + shop assets: one line (+ soft shop-pay if missing) | -| www-… | **bank `/intro/demo-withdraw.json`** → `taler://withdraw/HOST:PORT/taler-integration/…` + integration op HTTP 200 | -| www-… | **landing exposed links** (bank / merchant / exchange): parse each `/intro/` HTML, probe every own-stack `https://` + root-relative `href`/`src`/`content`, soft-check external stores/docs | -| www-… | landing static: `qrcode.min.js`, `og-goa-shop.png`, `qr-logo.png`, shop-pay.js/css | -| www-… | cross-links between bank ↔ merchant ↔ exchange intros (local stack) | -| www-… | **bank `/intro/demo-withdraw.json`** → `taler://withdraw/HOST/taler-integration/…` (no default `:443`/`:80`; non-default port OK) + integration op HTTP 200 | -| www-… | bank `/intro/auto-account.json` (earlier) → same withdraw shape, **no payto_uri**, login at `/webui/` | -| www-… | **performance** (outside-in RTT): bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) | -| www-… | **landing load stats**: `/intro/stats.json` → loadavg, container RSS, in-container probe ms (public; SSH container cat fallback); warn if stale/missing | +| Group | Checks (typical) | +|-------|------------------| +| **www.exchange-** | `/config`, currency, **alt_unit_names**; `/keys` (+ alt soft); `/intro/`, `/`; **`/terms`**, **`/privacy`**, `/terms/` | +| **www.perf-** | outside-in RTT: bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) | +| **www.stats-** | `/intro/stats.json` loadavg / container RSS / probe ms (public; SSH fallback) | +| **www.bank-** | `/config`, currency, alt_unit_names; integration/webui/intro; **auto-account.json**; `/terms`, `/privacy` | +| **www.merchant-** | `/config` currency + currencies alt_unit_names; listed exchanges alt; webui/intro; **`/terms`**, **`/privacy`** | +| **www.paivana-** | local GOA paywall front (redirect to template) | +| **www.landing-** | every own-stack link on bank/merchant/exchange intros; static assets; demo-withdraw / cross-links | -**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. On local stack, optional content needle (terms/privacy/FADP/GOA…). +**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. Local stack may require content needle (terms/privacy/FADP/GOA…). -**Performance rule:** Measured from the **monitoring runner** (public URLs via Caddy), not container loopback. HTTP must match expect (usually 200); **latency (ms) is on every perf OK/WARN/ERROR line**, plus a **perf summary** (n / min / p50 / avg / max). Slow ≥ `PERF_WARN_MS` (default 8000) → WARN; ≥ `PERF_FAIL_MS` (default 20000) → ERROR. +**Performance rule:** Measured from the **monitoring runner** (public via Caddy), not container loopback. Latency (ms) on every perf line + **perf summary** (n / min / p50 / avg / max). -**Landing links rule:** Own-stack (bank/exchange/taler.\* + page host) must be HTTP 200 (or redirect→200). External (App Store, Play, F-Droid, wallet.taler.net, docs/git.taler.net, …) soft WARN if down. Auto-account wallet link must be `taler://withdraw/HOST/taler-integration/…` (default ports stripped for mobile wallets), never payto. +**Landing links rule:** Own-stack must be HTTP 200 (or redirect→200). External stores/docs soft WARN. Auto-account wallet link must be `taler://withdraw/HOST/taler-integration/…` (default ports stripped), never payto. -**alt_unit_names rule:** wallet codec requires a non-empty map including scale key `"0"`. For multi-currency merchant, also follow every entry in `exchanges[]` and check that exchange’s public `/config`. - -(IDs after a failed early check may shift if later soft checks are skipped when body missing — numbering follows **executed** checks.) +**alt_unit_names rule:** non-empty map including scale key `"0"`. Multi-currency merchant: follow each `exchanges[]` public `/config`. --- ## inside — koopa SSH (`./taler-monitoring.sh inside`) -| ID | Check (typical order) | -|----|------------------------| -| inside-001 | ssh koopa (or **koopa-external** fallback) | -| inside-002+ | per-component emit: container, ports, libeufin/httpd, postgres, local `/config`/`/keys`, wirewatch, DNS pin, caddy | -| inside-… | **load / memory**: host loadavg + RAM; bank/exchange/merchant podman CPU/mem/block + process RSS by role + DB sizes | +| Group | Checks | +|-------|--------| +| **inside.ssh-** | ssh reachability / remote collect | +| **inside.bank-** | container, libeufin, postgres, local `/config`, nginx, DNS pins | +| **inside.exchange-** | container, httpd, wirewatch, aggregator, transfer, local keys, DNS | +| **inside.merchant-** | container, httpd, wirewatch, depositcheck, DNS | +| **inside.caddy-** | host reverse-proxy process | +| **inside.load-** | host loadavg + RAM; per-container RSS/CPU | -Remote lines `E|comp|LEVEL|key|detail` each become one numbered result. +Remote lines `E|comp|LEVEL|key|detail` each become one numbered result under that component group. -SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` (default `koopa-external`) when LAN is unreachable. +SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` when LAN is unreachable. --- -## sanity — bank · exchange · merchant (`./taler-monitoring.sh sanity`) +## e2e — withdraw + pay (`./taler-monitoring.sh e2e`) -| ID | Section | -|----|---------| -| sanity-001… | bank public + server | -| sanity-… | exchange public + server | -| sanity-… | merchant public + server | +| Group | Checks | +|-------|--------| +| **e2e.prereq-** | wallet-cli, currency, budgets, secrets, public reachability | +| **e2e.load-** | host/container load snapshots (before / after ATM / after pay) | +| **e2e.bank-** | account, credit, withdraw ops | +| **e2e.wallet-** | exchange + ToS, wallet setup | +| **e2e.atm-** | ATM withdraw ladder rungs | +| **e2e.settle-** | wallet settlement wait / spendable balance | +| **e2e.pay-** | variable payments | +| **e2e.shop-** | GOA shop product catalog pays | +| **e2e.paivana-** | template paywall pay | +| **e2e.dig-** | coins-missing dig (inside + withdrawal-operation) | +| **e2e.report-** | final tallies | -Sequential through the whole script (one `set_area sanity`). +When filing an issue, quote the full id + label, e.g. +`e2e.atm-03 ATM withdraw 5 GOA · bank confirmed, wallet empty`. --- -## versions — packages vs deb.taler.net (`./taler-monitoring.sh versions`) +## versions / sanity / ladder / server -### Outside (runner / public network — no SSH) - -| ID (order) | Check | -|------------|--------| -| versions-… | DNS `deb.taler.net` | -| versions-… | HTTPS portal + apt base URL | -| versions-… | suite `InRelease` / `Release` | -| versions-… | suite `Packages` + `Packages.gz` | -| versions-… | sample pool `.deb` fetchable (Range 200/206) | -| versions-… | suite offers `taler-exchange`, `taler-merchant`, `libeufin-bank` | -| versions-… | optional `trixie-testing` Packages | -| versions-… | TLS verify (soft) | - -### Inside (SSH koopa containers) - -| ID | Check | -|----|--------| -| versions-… | ssh koopa | -| versions-… | each container → `InRelease` (pasta can reach apt repo) | -| versions-… | each container lists `deb.taler.net` in apt sources | -| versions-… | each installed `taler*` / `libeufin*` / `libtaler*` / `libdonau*` vs suite version | -| versions-… | core packages installed (`taler-exchange`, `libeufin-bank`, `taler-merchant`) | - -Outside always runs. Inside skipped with `SKIP_SSH=1` (still reports outside results). - -Compare rules: - -- **match** suite → OK -- **ahead** of suite (often testing/dev) → INFO -- **behind** suite → ERROR for core packages, WARN otherwise (`TALER_PKG_BEHIND=error` forces ERROR) - -Default suite: **trixie** (`TALER_APT_SUITE`, `TALER_APT_BASE=https://deb.taler.net/apt/debian`). - -Without SSH (`SKIP_SSH=1` or remote domain): still runs outside-in repo checks; skips container install compare. +| Area.group | Meaning | +|------------|---------| +| **versions.outside-** | deb.taler.net suite index | +| **versions.inside-** | packages in containers | +| **versions.compare-** | installed vs suite | +| **sanity.bank-** / **.exchange-** / **.merchant-** | public + optional server-side per component | +| **ladder.plan-** / **.load-** / **.withdraw-** / **.pay-** / **.report-** | GOA amount ladder | +| **server-** | SSH host ports / processes (flat unless grouped later) | --- -## server — SSH ports (`./taler-monitoring.sh server`) - -| ID | Check | -|----|--------| -| server-001 | ssh | -| server-002+ | containers, local pasta ports, processes, caddy | - ---- - -## e2e — payment path (`./taler-monitoring.sh e2e`) - -Local GOA also: **ATM includes GOA:4200**, then **paivana** (HTTP 302 on `PAIVANA_PUBLIC` + public template pay `paivana` / GOA:4200 on `goa-shop`). Disable with `E2E_PAIVANA=0`. - -| ID | Step (approx.) | -|----|----------------| -| e2e-001 | budget info | -| e2e-002 | wallet-cli present | -| e2e-003 | mode / currency info | -| e2e-004… | secrets, reachability gates | -| e2e-… | account, credit, withdraw, confirm, coins, order, pay ladder | -| e2e-… | **load snapshots** at e2e-start, after-withdraw, after-pay, after-shop, e2e-end (host loadavg/RAM + container RSS/CPU/DB) | -| e2e-… | **coins** before/after each ATM withdraw, settle, pay, shop: count in circulation / spent, amount + denoms using **`alt_unit_names`** (e.g. `5 Kilo-GOA (GOA:5000)`), Δ vs previous snap | -| e2e-… | **GOA shop products** — full catalog list; **random pick of 2** (override `E2E_SHOP_PICK_N`) | -| e2e-… | balances, dig on failure, load delta + overall metrics | - -Shop product pays use instance `goa-shop` (default) and catalog `E2E_SHOP_PRODUCTS` -(`id|Product name|amount` lines). Each e2e run **shuffles** the catalog and pays -only `E2E_SHOP_PICK_N` products (**default 2**). Flow matches the landing popup -(public POST `/templates/{id}`, not private orders). Report labels use product name. - -Blockers keep the same ID prefix: `[BLOCKER] e2e-0NN step: message`. - ---- - -## ladder — GOA withdraw ranges (`./taler-monitoring.sh ladder`) - -| Step | What | -|------|------| -| ladder-001 | `GET /intro/auto-account.json` (personal account, GOA:0) | -| ladder-… | **load snapshot** before withdraws (host + bank/exchange/merchant mem/CPU) | -| ladder-… | **Phase A withdraw** (23): mint explorer pool — 0 → random → max−1 → max into **one cumulative wallet** (mids × `LADDER_WITHDRAW_SCALE`, default 1.5) | -| ladder-… | wallet-cli accept-uri + explorer confirm when `selected`; settle; coins snap | -| ladder-… | **Phase B pay** (23): same shape 0 → random → max−1 → max via merchant orders + `handle-uri --yes` | -| ladder-… | **load snapshot** after + delta + overall timing (withdraw + pay) | -| report | withdraw TSV + pay TSV + JSON | - -Default amounts (`build_ladder_pair`, strictly increasing): +## Visual format (colour on) ```text -withdraw: [0] + random mids + [max−1] + [max] -pay: [0] + (withdraw_mid / scale) + [max−1] + [max] # always ≤ matching withdraw mid +╔══════════════════════════════╗ +║ www · public URLs · … ║ +╚══════════════════════════════╝ +┌ www.exchange ┐ +┌ OK ┐ www.exchange-01 exchange /config · HTTP 200 +┌ www.bank ┐ +┌ OK ┐ www.bank-01 bank /config · HTTP 200 +┌ ERROR ┐ www.bank-04 bank /intro/auto-account.json · invalid withdraw + +┌ ERRORS · failed checks ┐ + • www.bank-04 bank /intro/auto-account.json · invalid withdraw +totals: 40 OK, 1 ERROR, 2 WARN, 5 INFO ``` -TSV `range`: `pin:0` | `random` | `pin:max-1` | `pin:max`. - -Soft: absolute **max** mint/pay may `CEILING_REJECT` / skip (WARN). **max-1** is a hard pin. -`LADDER_PAY=0` skips phase B. - -| Env | Default | Meaning | -|-----|---------|---------| -| `LADDER_STEPS` | `23` | total rungs (0 + mids + max) | -| `LADDER_MAX_AMOUNT` | `4503599627370496` | absolute last pin | -| `LADDER_TIMEOUT_S` | `3600` | large rungs need time | -| `LADDER_MAX_RUNGS` | `99` | cap list length after build | - -```bash -./taler-monitoring.sh ladder -LADDER_STEPS=10 ./taler-monitoring.sh ladder -LADDER_REPORT_DIR=/tmp/my-ladder ./taler-monitoring.sh ladder -``` - ---- - -## Run one area - -```bash -./taler-monitoring.sh urls # www only -./taler-monitoring.sh inside # inside only -./taler-monitoring.sh versions # deb.taler.net + package drift -./taler-monitoring.sh e2e # e2e only -./taler-monitoring.sh ladder # GOA amount ladder + timings -./taler-monitoring.sh -d taler.net urls -``` +`NO_COLOR=1` or `CLICOLOR=0` disables boxes/colours (ASCII `[ OK ]` + `-- group --` headers). +Always use `printf --` friendly plain headers when colour is off (leading `---` is not a printf option). diff --git a/scripts/taler-monitoring/check_e2e.sh b/scripts/taler-monitoring/check_e2e.sh index 8c4670e..7935d7a 100755 --- a/scripts/taler-monitoring/check_e2e.sh +++ b/scripts/taler-monitoring/check_e2e.sh @@ -182,6 +182,7 @@ e2e_skip_rest() { # Area e2e-### — withdraw + pay cycle set_area e2e +set_group prereq section "e2e · prerequisites" info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)" @@ -382,10 +383,12 @@ else fi # Host + bank/exchange/merchant RAM/CPU/load (SSH koopa or koopa-external) +set_group load section "e2e · load snapshot (before withdraw/pay)" metrics_report_load "${METRICS_DIR}/load-before.json" "e2e-start" || true # Local stack: koopa secrets. Remote: only explicit env (never leak local passwords to foreign banks). +set_group prereq if [ "$E2E_REMOTE" = "1" ]; then ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}" MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}" @@ -496,6 +499,7 @@ wcli_pay() { # When coins never become available: run inside + targeted bank/exchange dig dig_when_no_coins() { + set_group dig section "e2e · coins missing — inside dig" info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange" if [ -n "${WID:-}" ]; then @@ -580,6 +584,7 @@ BAL_BEFORE=$(fmt_bal "$SCRATCH/bal-before.out") info "BALANCE before" "$BAL_BEFORE" # --------------------------------------------------------------------------- +set_group bank section "e2e · bank (account · credit · withdraw)" # --------------------------------------------------------------------------- AT="" @@ -678,6 +683,7 @@ else fi # --------------------------------------------------------------------------- +set_group wallet section "e2e · wallet setup (exchange + ToS once)" # --------------------------------------------------------------------------- if ! wcli exchanges add "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-add.out" 2>&1; then @@ -1079,6 +1085,7 @@ sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="p } # --------------------------------------------------------------------------- +set_group atm section "e2e · ATM withdraw ladder" # --------------------------------------------------------------------------- WITHDRAW_OK=0 @@ -1109,12 +1116,14 @@ for WITHDRAW_AMT in $WITHDRAW_LIST; do esac done info "ATM withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)" +set_group load section "e2e · load snapshot (after ATM withdraws)" metrics_report_load "${METRICS_DIR}/load-after-withdraw.json" "after-withdraw" || true cp -f "${METRICS_DIR}/load-after-withdraw.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true metrics_report_coins "after-ATM-ladder" || true # Settlement catch-up: bank may have confirmed while wallet was still empty +set_group settle section "e2e · wallet settlement (timing)" if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then @@ -1154,6 +1163,7 @@ else fi # --------------------------------------------------------------------------- +set_group pay section "e2e · variable payments (if balance allows)" # --------------------------------------------------------------------------- PAY_OK=0 @@ -1193,6 +1203,7 @@ for PAY_AMT in $PAY_LIST; do fi done info "pay summary" "$PAY_REPORT (ok=$PAY_OK_N fail=$PAY_FAIL_N skip=$PAY_SKIP_N)" +set_group load section "e2e · load snapshot (after payments)" metrics_report_load "${METRICS_DIR}/load-after-pay.json" "after-pay" || true cp -f "${METRICS_DIR}/load-after-pay.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true @@ -1207,6 +1218,7 @@ if [ "$PAY_OK" != "1" ]; then fi # --------------------------------------------------------------------------- +set_group shop section "e2e · GOA shop products (merchant landing catalog)" # --------------------------------------------------------------------------- # Public template POST + wallet pay — full catalog list, random pick of N products. @@ -1297,6 +1309,7 @@ EOF fi # --------------------------------------------------------------------------- +set_group paivana section "e2e · paivana paywall (template GOA:4200)" # --------------------------------------------------------------------------- # Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana". @@ -1388,6 +1401,7 @@ else info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}" fi +set_group report section "e2e · report" info "user" "$USER" info "ATM withdraws" "$WITHDRAW_REPORT" diff --git a/scripts/taler-monitoring/check_goa_ladder.sh b/scripts/taler-monitoring/check_goa_ladder.sh index 28d71ed..bcea26a 100755 --- a/scripts/taler-monitoring/check_goa_ladder.sh +++ b/scripts/taler-monitoring/check_goa_ladder.sh @@ -29,7 +29,10 @@ ROOT=$(cd "$(dirname "$0")" && pwd) # shellcheck source=lib.sh source "$ROOT/lib.sh" +# Area ladder.* — GOA withdraw/pay ladder +# Groups: ladder.plan / ladder.load / ladder.withdraw / ladder.pay / ladder.report set_area ladder +set_group plan SECTION_T0=$(date +%s) now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; } elapsed_ms() { @@ -261,6 +264,7 @@ info "steps" "${LADDER_STEPS} (0 + random≥${LADDER_MIN_AMOUNT} + max-1=${CUR}: info "withdraw_scale" "${LADDER_WITHDRAW_SCALE}× pay mids (fund pay ladder)" info "pay_phase" "$([ "${LADDER_PAY}" = "1" ] && echo enabled || echo disabled)" +set_group load section "ladder · load snapshot (before withdraws)" metrics_report_load "$LOAD_BEFORE" "ladder-start" || true # Fresh wallet per rung — baseline empty (or last-rung DB if re-used later) @@ -344,6 +348,7 @@ STOP_REASON="" STOP_AMOUNT="" declare -a RUNG_JSON=() +set_group withdraw section "ladder · phase A · withdraw (${LADDER_N} rungs)" rung=0 for AMT in "$@"; do @@ -787,6 +792,7 @@ done PAY_OK_N=0 PAY_FAIL_N=0 if [ "${LADDER_PAY}" = "1" ] && [ -n "${PAY_LIST:-}" ] && [ "$FAIL_N_L" -eq 0 ]; then + set_group pay section "ladder · phase B · pay" metrics_report_coins "before-pay-ladder" || true # Merchant secret (same as e2e) @@ -1024,6 +1030,7 @@ fi ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0") # --- report --- +set_group report section "ladder · report" info "auto-account" "$ACCT_USER" info "ok_rungs" "$OK_N" @@ -1128,6 +1135,7 @@ PY wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true +set_group load section "ladder · load snapshot (after withdraws)" metrics_report_load "$LOAD_AFTER" "ladder-end" || true if [ -f "$LOAD_BEFORE" ] && [ -f "$LOAD_AFTER" ]; then diff --git a/scripts/taler-monitoring/check_inside.sh b/scripts/taler-monitoring/check_inside.sh index f49b4b6..2d012ed 100755 --- a/scripts/taler-monitoring/check_inside.sh +++ b/scripts/taler-monitoring/check_inside.sh @@ -7,8 +7,10 @@ source "$ROOT/lib.sh" # shellcheck source=metrics.sh source "$ROOT/metrics.sh" -# Area inside-### — container / process state on koopa (SSH) +# Area inside.* — container / process state on koopa (SSH) +# Groups: inside.ssh / inside.bank / inside.exchange / inside.merchant / inside.caddy set_area inside +set_group ssh section "inside · collect from koopa" if [ "${SKIP_SSH}" = "1" ]; then @@ -128,10 +130,20 @@ if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then exit 1 fi +_last_inside_grp="" while IFS= read -r line; do case "$line" in E\|*) IFS='|' read -r _ comp level key detail <<<"$line" + # Group IDs by component so issues map cleanly: inside.bank-02, inside.exchange-04 + case "$comp" in + bank|exchange|merchant|caddy) _g="$comp" ;; + *) _g="ssh" ;; + esac + if [ "$_g" != "$_last_inside_grp" ]; then + set_group "$_g" + _last_inside_grp="$_g" + fi case "$level" in OK) ok "[$comp] $key${detail:+ ($detail)}" ;; ERROR) err "$comp" "$key" "$detail" ;; @@ -143,6 +155,7 @@ while IFS= read -r line; do done <<<"$RAW" # Host loadavg + RAM + per-container RSS/CPU (same probe as e2e/ladder) +set_group load section "inside · load / memory" METRICS_DIR="${METRICS_DIR:-$(mktemp -d)}" export METRICS_DIR diff --git a/scripts/taler-monitoring/check_sanity.sh b/scripts/taler-monitoring/check_sanity.sh index 1486e9b..1e3b922 100755 --- a/scripts/taler-monitoring/check_sanity.sh +++ b/scripts/taler-monitoring/check_sanity.sh @@ -23,10 +23,12 @@ json_currency() { python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("currency") or "")' "$1" 2>/dev/null || true } -# Area sanity-### — public + optional server-side per component +# Area sanity.* — public + optional server-side per component +# Groups: sanity.bank / sanity.exchange / sanity.merchant set_area sanity # --------------------------------------------------------------------------- +set_group bank section "sanity · bank" # --------------------------------------------------------------------------- expect_code "bank public /config" 200 "$BANK_PUBLIC/config" @@ -98,6 +100,7 @@ else fi # --------------------------------------------------------------------------- +set_group exchange section "sanity · exchange" # --------------------------------------------------------------------------- expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config" @@ -193,6 +196,7 @@ else fi # --------------------------------------------------------------------------- +set_group merchant section "sanity · merchant" # --------------------------------------------------------------------------- expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config" diff --git a/scripts/taler-monitoring/check_server.sh b/scripts/taler-monitoring/check_server.sh index 961b4c8..8412a12 100755 --- a/scripts/taler-monitoring/check_server.sh +++ b/scripts/taler-monitoring/check_server.sh @@ -5,8 +5,9 @@ ROOT=$(cd "$(dirname "$0")" && pwd) # shellcheck source=lib.sh source "$ROOT/lib.sh" -# Area server-### — host/container ports via SSH +# Area server.* — host/container ports via SSH set_area server +set_group ssh section "server · ssh ${KOOPA_SSH}" if ! koopa_ssh_ok; then diff --git a/scripts/taler-monitoring/check_urls.sh b/scripts/taler-monitoring/check_urls.sh index c5cd78d..10caf0f 100755 --- a/scripts/taler-monitoring/check_urls.sh +++ b/scripts/taler-monitoring/check_urls.sh @@ -5,7 +5,9 @@ ROOT=$(cd "$(dirname "$0")" && pwd) # shellcheck source=lib.sh source "$ROOT/lib.sh" -# Area www-### — public HTTPS (outside-in) +# Area www.* — public HTTPS (outside-in) +# Groups: www.exchange / www.perf / www.stats / www.bank / www.merchant / +# www.paivana / www.landing set_area www section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)" @@ -169,7 +171,8 @@ sys.exit(0) PY } -# --- exchange (core; always required) --- www-001 … +# --- exchange (core; always required) --- www.exchange-NN +set_group exchange check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config" code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json") if [ "$code" = "200" ]; then @@ -219,6 +222,7 @@ fi # Performance — outside-in public HTTPS latency (this runner, not loopback) # Same spirit as bank landing-stats public probes; measured here from outside. # --------------------------------------------------------------------------- +set_group perf section "www · performance · public HTTPS latency (outside-in)" # Latency thresholds (ms), outside-in from this runner. @@ -392,6 +396,7 @@ PY } if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then + set_group stats section "www · performance · landing load stats (stats.json · in-container probes)" report_landing_load_stats "bank" "$BANK_PUBLIC" report_landing_load_stats "exchange" "$EXCHANGE_PUBLIC" @@ -470,7 +475,8 @@ fi info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}" -# Terms + privacy (legal docs) +# Exchange terms + privacy (same www.exchange group — issues map to exchange) +set_group exchange check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy" check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration" # trailing slash: 200 or redirect to bare path @@ -485,6 +491,7 @@ case "$code" in esac # --- bank --- +set_group bank if [ "${LOCAL_STACK:-1}" = "0" ]; then check_url_soft "bank /config" 200 "$BANK_PUBLIC/config" else @@ -545,6 +552,7 @@ else fi # --- merchant --- +set_group merchant if [ "${LOCAL_STACK:-1}" = "0" ]; then check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config" else @@ -610,6 +618,7 @@ esac # Paivana paywall (local GOA stack) — public front only; pay path is e2e # --------------------------------------------------------------------------- if [ "${LOCAL_STACK:-1}" = "1" ] && [ "${E2E_PAIVANA:-1}" != "0" ]; then + set_group paivana section "www · paivana paywall" : "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}" PAIVANA_PUBLIC="${PAIVANA_PUBLIC%/}" @@ -646,6 +655,7 @@ if [ "${CHECK_LANDING:-1}" != "1" ]; then exit 0 fi +set_group landing section "www · landing exposed links · bank / merchant / exchange" # Probe one URL: print code to stdout (200 after following redirects counts as 200). diff --git a/scripts/taler-monitoring/check_versions.sh b/scripts/taler-monitoring/check_versions.sh index eed9e5f..ea4df2f 100755 --- a/scripts/taler-monitoring/check_versions.sh +++ b/scripts/taler-monitoring/check_versions.sh @@ -19,6 +19,7 @@ ROOT=$(cd "$(dirname "$0")" && pwd) source "$ROOT/lib.sh" set_area versions +set_group outside SUITE="${TALER_APT_SUITE:-trixie}" APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}" @@ -166,6 +167,7 @@ fi # --------------------------------------------------------------------------- # 2) INSIDE — containers can reach deb.taler.net (pasta / install path) # --------------------------------------------------------------------------- +set_group inside section "versions · inside · containers → deb.taler.net" if [ "${SKIP_SSH}" = "1" ]; then @@ -266,6 +268,7 @@ ok "collected installed packages" "${n_inst} rows" # --------------------------------------------------------------------------- # 3) compare installed vs trixie (and note testing) # --------------------------------------------------------------------------- +set_group compare section "versions · compare installed vs ${SUITE}" export BEHIND_MODE HAVE_TESTING diff --git a/scripts/taler-monitoring/lib.sh b/scripts/taler-monitoring/lib.sh index a789995..5732d1d 100755 --- a/scripts/taler-monitoring/lib.sh +++ b/scripts/taler-monitoring/lib.sh @@ -352,11 +352,12 @@ koopa_ssh_python() { with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 - } -# ANSI colours — badge tags (fg+bg) + body (label bold, detail dim). -# Off: NO_COLOR=1 or CLICOLOR=0. Badges need a terminal that supports SGR bg. +# ANSI colours — boxed badges (fg+bg) + body (label bold, detail dim). +# Off: NO_COLOR=1 or CLICOLOR=0. if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then G= R= Y= C= M= D= W= B= N= BG_OK= BG_INFO= BG_WARN= BG_ERR= BG_BLK= BG_SEC= + BOX=0 else G=$'\e[1;32m' # green text (totals) R=$'\e[1;31m' # red text @@ -367,13 +368,14 @@ else W=$'\e[1;37m' # bold white label B=$'\e[1m' # bold section N=$'\e[0m' # reset - # Badge: bright text on solid background (space-padded in _msg_line) + # Badge fill: bright text on solid background BG_OK=$'\e[1;30;42m' # black on green BG_INFO=$'\e[1;30;46m' # black on cyan BG_WARN=$'\e[1;30;43m' # black on yellow BG_ERR=$'\e[1;37;41m' # white on red BG_BLK=$'\e[1;37;45m' # white on magenta BG_SEC=$'\e[1;37;44m' # white on blue (section) + BOX=1 fi PASS_N=0 @@ -383,17 +385,68 @@ INFO_N=0 BLOCKERS=() # human-readable payment/withdraw blockers ERRORS=() # all ERROR lines (component scope) -# Test IDs by area: www-001, e2e-001, inside-001, … -# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers. +# Grouped test IDs: area.group-NN (e.g. www.exchange-01, e2e.pay-03) +# set_area www # phase: www | e2e | inside | versions | … +# set_group exchange # resets counter + prints group box → www.exchange-01 +# set_group bank # → www.bank-01 +# Without set_group: area-01, area-02, … (flat within area) TEST_AREA="" +TEST_GROUP="" TEST_N=0 -# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists. LAST_TID="" +# Per-group counters so re-entering www.exchange after perf continues NN +# (shell vars TEST_CNT__). +_tid_key() { + # $1=area $2=group → safe identifier + printf 'TEST_CNT_%s_%s' "$1" "$2" | tr -c 'A-Za-z0-9_' '_' +} +_tid_save() { + [ -z "${TEST_AREA:-}" ] || [ -z "${TEST_GROUP:-}" ] && return 0 + local k + k=$(_tid_key "$TEST_AREA" "$TEST_GROUP") + eval "$k=\$TEST_N" +} +_tid_load() { + local k + k=$(_tid_key "$TEST_AREA" "$1") + eval "TEST_N=\${$k:-0}" +} set_area() { + # leave previous group counter saved + _tid_save TEST_AREA="$1" + TEST_GROUP="" TEST_N=0 LAST_TID="" } +# Start a logical sub-group. Short stable names: +# exchange bank merchant perf stats landing paivana +# prereq wallet atm settle pay shop dig load report +# ssh caddy outside inside compare withdraw +# Counter continues if the same group is re-entered later in the area. +# Prints a compact group chip so log + issue text map cleanly (www.bank-03). +set_group() { + local g="$1" + # no-op if same group already active (e.g. inside emit loop) + if [ "$g" = "${TEST_GROUP:-}" ] && [ -n "$g" ]; then + return 0 + fi + _tid_save + TEST_GROUP="$g" + LAST_TID="" + if [ -z "${TEST_AREA:-}" ] || [ -z "$g" ]; then + TEST_N=0 + return 0 + fi + _tid_load "$g" + local tag="${TEST_AREA}.${g}" + if [ "${BOX:-0}" = "1" ]; then + # cyan-outline group chip: ┌ www.exchange ┐ + printf '%s%s┌ %s ┐%s\n' "$D" "$C" "$tag" "$N" + else + printf -- '-- %s --\n' "$tag" + fi +} # Assign next id into LAST_TID (must not run in a subshell). _take_tid() { LAST_TID="" @@ -401,27 +454,40 @@ _take_tid() { return fi TEST_N=$((TEST_N + 1)) - LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N") + if [ -n "${TEST_GROUP:-}" ]; then + LAST_TID=$(printf '%s.%s-%02d' "$TEST_AREA" "$TEST_GROUP" "$TEST_N") + _tid_save + else + LAST_TID=$(printf '%s-%02d' "$TEST_AREA" "$TEST_N") + fi } -# Dim test id: "www-001 " or empty +# Dim test id: "www.exchange-01 " or empty _fmt_tid() { if [ -n "${LAST_TID:-}" ]; then printf '%s%s%s ' "$D" "$LAST_TID" "$N" fi } -# One concrete line: [TAG] tid label · detail -# $1=badge style (bg+fg) $2=tag text $3=label colour $4=label $5=detail +# Boxed badge cell: ┌ OK ┐ with filled bg (tag left-padded, width 7 for BLOCKER) +# $1=badge style $2=tag text +_fmt_badge() { + local badge="$1" tag="$2" inner + inner=$(printf -- '%-7s' "$tag") + if [ "${BOX:-0}" = "1" ]; then + printf '%s┌ %s┐%s' "$badge" "$inner" "$N" + else + printf '%s[ %s]%s' "$badge" "$inner" "$N" + fi +} +# One concrete line: ┌ OK ┐ tid label · detail +# $1=badge style $2=tag text $3=label colour $4=label $5=detail _msg_line() { local badge="$1" tag="$2" lcol="$3" label="$4" detail="${5:-}" - # Fixed-width badge cell: " OK " / " BLOCKER" (9 chars, space-padded) - local tcell - tcell=$(printf ' %-7s ' "$tag") if [ -n "$detail" ]; then - printf '%s%s%s %s%s%s%s %s·%s %s%s%s\n' \ - "$badge" "$tcell" "$N" "$(_fmt_tid)" "$lcol" "$label" "$N" "$D" "$N" "$D" "$detail" "$N" + printf -- '%s %s%s%s%s %s·%s %s%s%s\n' \ + "$(_fmt_badge "$badge" "$tag")" "$(_fmt_tid)" "$lcol" "$label" "$N" "$D" "$N" "$D" "$detail" "$N" else - printf '%s%s%s %s%s%s%s\n' \ - "$badge" "$tcell" "$N" "$(_fmt_tid)" "$lcol" "$label" "$N" + printf -- '%s %s%s%s%s\n' \ + "$(_fmt_badge "$badge" "$tag")" "$(_fmt_tid)" "$lcol" "$label" "$N" fi } @@ -484,33 +550,58 @@ blocker() { ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg") } section() { - # Blue badge + bold title - local title=" $* " - printf '\n%s%s%s %s%s%s\n' "$BG_SEC" " == " "$N" "$B" "$title" "$N" + # Boxed section header (3 lines when colour on) + local title="$*" + local w=${#title} + [ "$w" -lt 24 ] && w=24 + [ "$w" -gt 56 ] && w=56 + local pad line i + pad=$(printf -- "%-${w}s" "$title") + if [ "${BOX:-0}" = "1" ]; then + line="" + i=0 + while [ "$i" -lt $((w + 2)) ]; do + line="${line}═" + i=$((i + 1)) + done + printf -- '\n%s╔%s╗%s\n' "$BG_SEC" "$line" "$N" + printf -- '%s║%s %s%s%s %s║%s\n' "$BG_SEC" "$N" "$B" "$pad" "$N" "$BG_SEC" "$N" + printf -- '%s╚%s╝%s\n' "$BG_SEC" "$line" "$N" + else + printf -- '\n== %s ==\n' "$title" + fi } summary() { echo "" if [ "${#BLOCKERS[@]}" -gt 0 ]; then - printf '%s BLOCKERS %s %s(pay/withdraw cannot finish)%s\n' "$BG_BLK" "$N" "$D" "$N" + if [ "${BOX:-0}" = "1" ]; then + printf -- '%s┌ BLOCKERS · pay/withdraw cannot finish ┐%s\n' "$BG_BLK" "$N" + else + printf -- '--- BLOCKERS (pay/withdraw cannot finish) ---\n' + fi local b for b in "${BLOCKERS[@]}"; do - printf '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N" + printf -- '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N" done fi if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then - printf '%s ERRORS %s %s(failed checks)%s\n' "$BG_ERR" "$N" "$D" "$N" + if [ "${BOX:-0}" = "1" ]; then + printf -- '%s┌ ERRORS · failed checks ┐%s\n' "$BG_ERR" "$N" + else + printf -- '--- ERRORS (failed checks) ---\n' + fi local e for e in "${ERRORS[@]}"; do case "$e" in BLOCKER*) continue ;; esac - printf '%s •%s %s%s%s\n' "$R" "$N" "$W" "$e" "$N" + printf -- '%s •%s %s%s%s\n' "$R" "$N" "$W" "$e" "$N" done fi - printf '%stotals:%s %s%d OK%s' "$D" "$N" "$G" "$PASS_N" "$N" - [ "$FAIL_N" -gt 0 ] && printf ', %s%d ERROR%s' "$R" "$FAIL_N" "$N" - [ "$WARN_N" -gt 0 ] && printf ', %s%d WARN%s' "$Y" "$WARN_N" "$N" - [ "$INFO_N" -gt 0 ] && printf ', %s%d INFO%s' "$C" "$INFO_N" "$N" - [ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$M" "${#BLOCKERS[@]}" "$N" + printf -- '%stotals:%s %s%d OK%s' "$D" "$N" "$G" "$PASS_N" "$N" + [ "$FAIL_N" -gt 0 ] && printf -- ', %s%d ERROR%s' "$R" "$FAIL_N" "$N" + [ "$WARN_N" -gt 0 ] && printf -- ', %s%d WARN%s' "$Y" "$WARN_N" "$N" + [ "$INFO_N" -gt 0 ] && printf -- ', %s%d INFO%s' "$C" "$INFO_N" "$N" + [ "${#BLOCKERS[@]}" -gt 0 ] && printf -- ', %s%d BLOCKER%s' "$M" "${#BLOCKERS[@]}" "$N" printf '\n' [ "$FAIL_N" -eq 0 ] } From b6c3225e97011aa696f76b461d063e6a060e3e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Fri, 17 Jul 2026 16:26:18 +0200 Subject: [PATCH 4/8] landing-stats: make collector multi-stack reusable --- .../systemd/user/taler-landing-stats.service | 4 +- scripts/taler-landing/README.md | 36 ++++- .../taler-landing/collect-landing-stats.sh | 135 +++++++++++++++--- scripts/taler-landing/collect_bank_stats.py | 78 +++++++--- .../install-landing-stats-host.sh | 56 +++++--- scripts/taler-landing/profiles/goa.env | 18 +++ .../profiles/stage-testpaysan.env | 22 +++ 7 files changed, 291 insertions(+), 58 deletions(-) create mode 100644 scripts/taler-landing/profiles/goa.env create mode 100644 scripts/taler-landing/profiles/stage-testpaysan.env diff --git a/configs/systemd/user/taler-landing-stats.service b/configs/systemd/user/taler-landing-stats.service index 6273a72..cc6bd88 100644 --- a/configs/systemd/user/taler-landing-stats.service +++ b/configs/systemd/user/taler-landing-stats.service @@ -14,12 +14,14 @@ TimeoutStartSec=480 # Do not keep "active" after run — timer may fire again cleanly RemainAfterExit=no Environment=TZ=Europe/Zurich +# Stack overrides (goa / stage-testpaysan / custom) — optional +EnvironmentFile=-%h/.config/taler-landing/stack.env +# GOA defaults if stack.env absent Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log Environment=SECRETS_BANK=%h/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank Environment=BANK_URL=http://127.0.0.1:9012 Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch Environment=EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config -# All accounts except exchange (double-count of withdraw credits on exchange ledger) Environment=SCAN_SKIP=exchange Environment=TX_PAGE=500 Environment=MAX_TX_PAGES=0 diff --git a/scripts/taler-landing/README.md b/scripts/taler-landing/README.md index 0876695..f266ec7 100644 --- a/scripts/taler-landing/README.md +++ b/scripts/taler-landing/README.md @@ -1,12 +1,21 @@ -# taler-landing — central stats (hernani user systemd) +# taler-landing — generic multi-stack landing stats -Public landing numbers on +One collector (`collect_bank_stats.py` + `collect-landing-stats.sh`) for any +currency / stack. **Defaults = GOA / hacktivism**; stage TESTPAYSAN uses a +profile file. + +Public landing numbers (example GOA): - https://bank.hacktivism.ch/intro/ → `stats.json` - https://exchange.hacktivism.ch/intro/ → `stats.json` - https://taler.hacktivism.ch/intro/ → `stats.json` -are produced by **one host process** as user **`hernani`**, not by three unrelated in-container crons. +Stage TESTPAYSAN (shared bank feed on all three hosts): + +- https://stage.bank.lefrancpaysan.ch/intro/stats.json + (same file also under stage.exchange / stage.monnaie landings) + +Produced by **one host process** (user systemd timer), not three crons. ## Why host / user systemd @@ -77,16 +86,37 @@ Preferred order: Explorer password optional (shared-pool balance). +## Profiles + +| File | Stack | +|------|--------| +| `profiles/goa.env` | hacktivism GOA (container publish) | +| `profiles/stage-testpaysan.env` | FrancPaysan stage; **shared** host `stats.json` | + +```bash +# Stage (on stagepaysan): +STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh +# or: francpaysan-admin-log/scripts/stagepaysan/install-landing-stats.sh +``` + +`~/.config/taler-landing/stack.env` is sourced on each run. + ## Env overrides | Env | Default | Meaning | |-----|---------|---------| | `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback | +| `BANK_CURRENCY` | *(auto)* | `GOA` / `TESTPAYSAN` / … | | `SCAN_SKIP` | `exchange` | usernames excluded from flow | +| `COLLECT_BANK` / `_EXCHANGE` / `_MERCHANT` | `1` | phase switches | +| `COLLECT_RESOURCES` | `1` | container RSS/loadavg merge | +| `HOST_STATS_DIRS` | empty | space-separated host dirs for **shared** bank `stats.json` | +| `PUBLISH_PODMAN` | `1` | also `podman cp` into container landings | | `TX_PAGE` | `500` | transactions page size | | `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account | | `ACCOUNTS_DELTA` | `-10000` | account list window | | `ADMIN_LOG` | `%h/src/koopa/koopa-admin-log` | refresh in-container scripts | +| `STATS_SOURCE_LABEL` | `host collect_bank_stats.py` | `stats.source` field | ## UI alt names diff --git a/scripts/taler-landing/collect-landing-stats.sh b/scripts/taler-landing/collect-landing-stats.sh index e49b00f..d3baa3b 100755 --- a/scripts/taler-landing/collect-landing-stats.sh +++ b/scripts/taler-landing/collect-landing-stats.sh @@ -1,10 +1,14 @@ #!/usr/bin/env bash -# Central landing-stats collector for hernani@koopa (user systemd timer). +# Generic landing-stats orchestrator (any Taler stack). # -# - Bank: full account+ledger scan via collect_bank_stats.py → bank container -# - Exchange / merchant: existing in-container scripts, then amount_alt enrich -# - All three: container RSS / loadavg / top procs via mem-snapshot (podman exec) -# - Never wipes a good stats.json on failure (writes stats-run.json only) +# Defaults = GOA / hacktivism on koopa. Override via env or EnvironmentFile +# (see profiles: goa | stage-testpaysan). +# +# - Bank: collect_bank_stats.py (currency-agnostic) → stats.json +# - Optional: exchange / merchant in-container generators +# - Optional: container RSS/loadavg merge +# - Publish: podman into container landings and/or host dirs (shared bank stats) +# - Never wipes a good stats.json on failure (stats-run.json only) # # Install: scripts/taler-landing/install-landing-stats-host.sh set -euo pipefail @@ -12,6 +16,20 @@ set -euo pipefail export TZ="${TZ:-Europe/Zurich}" export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin${PATH:+:$PATH}" +# Optional stack profile: ~/.config/taler-landing/stack.env or STACK_ENV_FILE +if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then + # shellcheck disable=SC1090 + set -a + # shellcheck source=/dev/null + . "${STACK_ENV_FILE}" + set +a +elif [ -f "${HOME}/.config/taler-landing/stack.env" ]; then + set -a + # shellcheck source=/dev/null + . "${HOME}/.config/taler-landing/stack.env" + set +a +fi + log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; } ROOT="$(cd "$(dirname "$0")" && pwd)" @@ -22,6 +40,8 @@ elif [ -f "${HOME}/.local/lib/taler-landing/collect_bank_stats.py" ]; then LIB="${HOME}/.local/lib/taler-landing" elif [ -f "${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then LIB="${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing" +elif [ -f "${HOME}/taler/src/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then + LIB="${HOME}/taler/src/koopa-admin-log/scripts/taler-landing" else LIB="$ROOT" fi @@ -29,6 +49,13 @@ fi ADMIN_LOG="${ADMIN_LOG:-${HOME}/src/koopa/koopa-admin-log}" SECRETS_BANK="${SECRETS_BANK:-${HOME}/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank}" +# Which phases to run (1/0) +COLLECT_BANK="${COLLECT_BANK:-1}" +COLLECT_EXCHANGE="${COLLECT_EXCHANGE:-1}" +COLLECT_MERCHANT="${COLLECT_MERCHANT:-1}" +COLLECT_RESOURCES="${COLLECT_RESOURCES:-1}" + +# Containers (empty = skip podman publish for that role) BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}" EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}" MER_CTR="${MER_CTR:-taler-hacktivism}" @@ -36,10 +63,19 @@ MER_CTR="${MER_CTR:-taler-hacktivism}" BANK_URL="${BANK_URL:-http://127.0.0.1:9012}" BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}" EXCHANGE_CONFIG_URL="${EXCHANGE_CONFIG_URL:-https://exchange.hacktivism.ch/config}" +BANK_CURRENCY="${BANK_CURRENCY:-}" # empty → auto from bank/exchange /config +STATS_SOURCE_LABEL="${STATS_SOURCE_LABEL:-host collect_bank_stats.py}" +# In-container landing roots (podman publish) BANK_LANDING_IN="${BANK_LANDING_IN:-/var/www/bank-landing}" EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-landing}" MER_LANDING_IN="${MER_LANDING_IN:-/var/www/merchant-landing}" +PUBLISH_PODMAN="${PUBLISH_PODMAN:-1}" + +# Host dirs that should get the *same* bank stats.json (shared public feed). +# Colon-separated (systemd EnvironmentFile-safe). Example stage: +# HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant +HOST_STATS_DIRS="${HOST_STATS_DIRS:-}" WORKDIR="${LANDING_STATS_WORKDIR:-${XDG_RUNTIME_DIR:-/tmp}/taler-landing-stats}" mkdir -p "$WORKDIR" @@ -64,7 +100,9 @@ MEM_SRC="${MEM_SNAPSHOT_SRC:-$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh}" merge_container_resources() { local label="$1" ctr="$2" stats_file="$3" + [ "${COLLECT_RESOURCES}" = "1" ] || return 0 [ -f "$stats_file" ] || return 0 + [ -n "$ctr" ] || return 0 if ! ctr_running "$ctr"; then log "WARN: $label resources: $ctr not running" return 1 @@ -95,6 +133,36 @@ merge_container_resources() { return 0 } +# Write stats.json into host directories (shared feed: same file everywhere). +publish_host_stats() { + local src_stats="$1" src_run="${2:-}" + [ -n "${HOST_STATS_DIRS:-}" ] || return 0 + [ -f "$src_stats" ] || return 0 + local d paths + # Accept colon or space separators + paths=$(printf '%s' "$HOST_STATS_DIRS" | tr ': ' '\n' | sed '/^$/d') + while IFS= read -r d; do + [ -n "$d" ] || continue + if [ -d "$d" ] || mkdir -p "$d" 2>/dev/null; then + if cp -f "$src_stats" "${d}/stats.json" 2>/dev/null; then + chmod a+r "${d}/stats.json" 2>/dev/null || true + [ -n "$src_run" ] && [ -f "$src_run" ] && cp -f "$src_run" "${d}/stats-run.json" 2>/dev/null || true + log "host: published ${d}/stats.json" + continue + fi + fi + if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then + sudo mkdir -p "$d" 2>/dev/null || true + sudo cp -f "$src_stats" "${d}/stats.json" + sudo chmod a+r "${d}/stats.json" 2>/dev/null || true + [ -n "$src_run" ] && [ -f "$src_run" ] && sudo cp -f "$src_run" "${d}/stats-run.json" 2>/dev/null || true + log "host: published ${d}/stats.json (sudo)" + else + log "WARN: cannot write ${d}/stats.json (need write or passwordless sudo)" + fi + done <<<"$paths" +} + read_pass() { local name="$1" f for f in \ @@ -107,11 +175,25 @@ read_pass() { return 0 fi done - # container (hernani can podman exec root files inside owned containers) - if podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then - if podman exec "$BANK_CTR" test -r "/root/${name}" 2>/dev/null; then - podman exec "$BANK_CTR" cat "/root/${name}" 2>/dev/null | tr -d '\n' - return 0 + # container secrets: /root/… (GOA) or /data/secrets/… (FrancPaysan stage) + if [ -n "${BANK_CTR:-}" ] && podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then + for path in "/root/${name}" "/data/secrets/${name}" "/data/secrets/bank-admin-password.txt"; do + case "$path" in + *bank-admin-password.txt) + [ "$name" = "bank-admin-password.txt" ] || continue + ;; + esac + if podman exec "$BANK_CTR" test -r "$path" 2>/dev/null; then + podman exec "$BANK_CTR" cat "$path" 2>/dev/null | tr -d '\n\r' + return 0 + fi + done + # stage explorer secret naming + if [ "$name" = "bank-explorer-password.txt" ]; then + if podman exec "$BANK_CTR" test -r /data/secrets/bank-explorer-password.txt 2>/dev/null; then + podman exec "$BANK_CTR" cat /data/secrets/bank-explorer-password.txt 2>/dev/null | tr -d '\n\r' + return 0 + fi fi fi return 1 @@ -123,6 +205,8 @@ ctr_running() { publish_json() { local ctr="$1" dest_dir="$2" src_stats="$3" src_run="$4" + [ "${PUBLISH_PODMAN}" = "1" ] || return 0 + [ -n "$ctr" ] || return 0 if ! ctr_running "$ctr"; then log "WARN: $ctr not running — skip publish $dest_dir" return 1 @@ -166,6 +250,7 @@ collect_bank() { BANK_URL="$BANK_URL" \ BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \ EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \ + BANK_CURRENCY="${BANK_CURRENCY:-}" \ BANK_ADMIN_PASS="$admin_pass" \ BANK_EXPLORER_PASS="$explorer_pass" \ DEMO_DIR="${demo_dir}" \ @@ -173,6 +258,7 @@ collect_bank() { TX_PAGE="${TX_PAGE:-500}" \ MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \ ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \ + STATS_SOURCE_LABEL="$STATS_SOURCE_LABEL" \ "$PY" "$LIB/collect_bank_stats.py" \ --out "$WORKDIR/bank-stats.json" \ --run-out "$WORKDIR/bank-stats-run.json" \ @@ -181,11 +267,13 @@ collect_bank() { set -e if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then - # Always attach in-container RSS/loadavg (not host /proc) + # Optional: attach in-container RSS/loadavg (not host /proc) merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true publish_json "$BANK_CTR" "$BANK_LANDING_IN" \ "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json" - log "bank: OK → ${BANK_CTR}:${BANK_LANDING_IN}/stats.json" + # Shared public feed: same bank stats on all HOST_STATS_DIRS (bank/ex/merchant landings) + publish_host_stats "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json" + log "bank: OK (podman=${BANK_CTR:-none} host_dirs=${HOST_STATS_DIRS:-none})" else log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept" [ -f "$WORKDIR/bank-stats-run.json" ] || \ @@ -272,13 +360,26 @@ collect_merchant() { # --------------------------------------------------------------------------- main() { - log "=== taler-landing-stats start (user=$(id -un) lib=$LIB) ===" - collect_bank || ec_bank=$? - collect_exchange || ec_ex=$? - collect_merchant || ec_mer=$? + log "=== taler-landing-stats start (user=$(id -un) lib=$LIB currency=${BANK_CURRENCY:-auto}) ===" + log "phases bank=${COLLECT_BANK} exchange=${COLLECT_EXCHANGE} merchant=${COLLECT_MERCHANT} resources=${COLLECT_RESOURCES}" + if [ "${COLLECT_BANK}" = "1" ]; then + collect_bank || ec_bank=$? + else + log "skip bank collect" + fi + if [ "${COLLECT_EXCHANGE}" = "1" ]; then + collect_exchange || ec_ex=$? + else + log "skip exchange collect" + fi + if [ "${COLLECT_MERCHANT}" = "1" ]; then + collect_merchant || ec_mer=$? + else + log "skip merchant collect" + fi log "=== done bank=$ec_bank exchange=$ec_ex merchant=$ec_mer ===" # non-zero if bank failed (primary public flow numbers); soft on ex/mer - if [ "$ec_bank" -ne 0 ]; then + if [ "${COLLECT_BANK}" = "1" ] && [ "$ec_bank" -ne 0 ]; then return 1 fi return 0 diff --git a/scripts/taler-landing/collect_bank_stats.py b/scripts/taler-landing/collect_bank_stats.py index 096eae7..b8d4398 100755 --- a/scripts/taler-landing/collect_bank_stats.py +++ b/scripts/taler-landing/collect_bank_stats.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 """Full-scan bank landing stats (all accounts, all ledger money). -Run on koopa as hernani (or anywhere with admin API access). Writes stats.json -compatible with bank.hacktivism.ch/intro/ plus amount_alt fields for compact UI. +Generic for any currency stack (GOA, TESTPAYSAN, KUDOS, …). Run on the ops host +(with admin API access). Writes stats.json for public landings + amount_alt. Env (selected): BANK_URL default http://127.0.0.1:9012 + BANK_CURRENCY optional; else from exchange /config or first ledger amount BANK_ADMIN_USER default admin BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file BANK_EXPLORER_USER default explorer BANK_EXPLORER_PASS optional (balance_explorer) - EXCHANGE_CONFIG_URL for alt_unit_names (default https://exchange.hacktivism.ch/config) + EXCHANGE_CONFIG_URL for alt_unit_names + currency + BANK_PUBLIC_URL public base for latency probes OUT output path (default stdout if -) SCAN_SKIP comma usernames excluded from flow (default: exchange) - TX_PAGE page size for transactions delta (default 500) - MAX_TX_PAGES 0 = unlimited pages per account (default 0) - ACCOUNTS_DELTA GET /accounts?delta= (default -10000) - RECENT_WD_N recent withdraws (default 10) - TZ default Europe/Zurich + TX_PAGE / MAX_TX_PAGES / ACCOUNTS_DELTA / RECENT_WD_N / TZ + STATS_SOURCE_LABEL optional string in stats.source field """ from __future__ import annotations @@ -343,6 +342,11 @@ def main() -> int: "--exchange-config", default=env("EXCHANGE_CONFIG_URL", "https://exchange.hacktivism.ch/config"), ) + ap.add_argument( + "--currency", + default=env("BANK_CURRENCY", ""), + help="ledger currency code (optional; else exchange /config or first amount)", + ) ap.add_argument("--out", default=env("OUT", "-")) ap.add_argument("--run-out", default=env("RUN_OUT", "")) ap.add_argument( @@ -364,6 +368,11 @@ def main() -> int: ) ap.add_argument("--recent", type=int, default=int(env("RECENT_WD_N", "10") or "10")) ap.add_argument("--public-base", default=env("BANK_PUBLIC_URL", "https://bank.hacktivism.ch")) + ap.add_argument( + "--source-label", + default=env("STATS_SOURCE_LABEL", "host collect_bank_stats.py"), + help="stats.source field (identify stack / collector)", + ) args = ap.parse_args() tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich" @@ -378,6 +387,7 @@ def main() -> int: bank = args.bank.rstrip("/") skip = {s.strip() for s in (args.skip or "").split(",") if s.strip()} + currency_hint = (args.currency or "").strip().upper() def write_run(ok: bool, err: Optional[str] = None) -> None: if not args.run_out: @@ -397,6 +407,24 @@ def main() -> int: raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)") token = get_token(bank, args.admin_user, admin_pass) alt = load_alt_from_config(args.exchange_config) + # Currency: explicit → exchange /config → first ledger amount → GOA + currency = currency_hint + if not currency: + try: + code_c, cfg_d, _ = http_json(args.exchange_config, timeout=12) + if code_c == 200 and isinstance(cfg_d, dict) and cfg_d.get("currency"): + currency = str(cfg_d["currency"]).strip().upper() + except Exception: + pass + if not currency: + try: + code_c, cfg_d, _ = http_json(f"{bank}/config", timeout=12) + if code_c == 200 and isinstance(cfg_d, dict) and cfg_d.get("currency"): + currency = str(cfg_d["currency"]).strip().upper() + except Exception: + pass + if not currency: + currency = "GOA" if not alt: alt = dict(DEFAULT_ALT) @@ -442,13 +470,24 @@ def main() -> int: except Exception: continue low = (subject or "").lower() + # Normalise bare numbers to stack currency + if ":" not in str(amount): + amount = f"{currency}:{amount}" + else: + # adopt ledger currency if we only had a hint + try: + cur0, _n0 = parse_amount(amount) + if cur0 and currency_hint == "" and cur0.upper() != "GOA": + currency = cur0.upper() + except Exception: + pass if direction == "credit": total_in += n n_incoming += 1 incomings.append( { "kind": "incoming", - "amount": amount if ":" in amount else f"GOA:{amount}", + "amount": amount, "at_unix": ts, "account": uname, "subject": subject, @@ -460,7 +499,7 @@ def main() -> int: withdraws.append( { "kind": "withdraw", - "amount": amount if ":" in amount else f"GOA:{amount}", + "amount": amount, "at_unix": ts, "account": uname, "subject": subject, @@ -503,7 +542,7 @@ def main() -> int: users_n = sum(1 for u in accounts if u not in ("admin", "exchange")) def pack_amt(d: Decimal) -> Dict[str, Any]: - f = format_amount_alt(f"GOA:{d}", alt) + f = format_amount_alt(f"{currency}:{d}", alt) return { "amount": f["amount"], "amount_alt": f["amount_alt"], @@ -567,10 +606,11 @@ def main() -> int: } ) - # explorer balance - balance = "GOA:0" - bal_alt = "GOA:0" - bal_full = "GOA:0" + # explorer / pool balance (shared demo account when present) + zero = f"{currency}:0" + balance = zero + bal_alt = zero + bal_full = zero if explorer_pass: try: etok = get_token(bank, args.explorer_user, explorer_pass) @@ -583,7 +623,7 @@ def main() -> int: balance = str( data.get("balance", {}).get("amount") if isinstance(data.get("balance"), dict) - else data.get("amount") or data.get("balance") or "GOA:0" + else data.get("amount") or data.get("balance") or zero ) # sometimes balance is object with amount if isinstance(data.get("balance"), dict) and data["balance"].get( @@ -608,7 +648,7 @@ def main() -> int: ) if code == 200 and isinstance(data, dict): if isinstance(data.get("balance"), dict): - balance = str(data["balance"].get("amount") or "GOA:0") + balance = str(data["balance"].get("amount") or zero) elif data.get("amount"): balance = str(data["amount"]) bf = format_amount_alt(balance, alt) @@ -637,12 +677,12 @@ def main() -> int: stats = { "ok": True, - "currency": "GOA", + "currency": currency, "timezone": tz_name, "generated_at": gen_iso, "generated_at_human": gen_human, "generated_at_unix": now_u, - "source": "host collect_bank_stats.py (hernani systemd)", + "source": str(args.source_label or "host collect_bank_stats.py"), "bank_url": bank, "scan": { "tx_page": args.tx_page, diff --git a/scripts/taler-landing/install-landing-stats-host.sh b/scripts/taler-landing/install-landing-stats-host.sh index 7c572f0..4a26217 100755 --- a/scripts/taler-landing/install-landing-stats-host.sh +++ b/scripts/taler-landing/install-landing-stats-host.sh @@ -1,35 +1,41 @@ #!/usr/bin/env bash -# Install central landing-stats collector as hernani user systemd timer. +# Install generic landing-stats collector as user systemd timer. # -# On koopa: +# GOA / koopa (default): # cd ~/src/koopa/koopa-admin-log # ./scripts/taler-landing/install-landing-stats-host.sh -# systemctl --user start taler-landing-stats.service # one-shot now -# systemctl --user status taler-landing-stats.timer # -# Requires: linger enabled for hernani (loginctl enable-linger hernani) -# so the timer runs without an interactive login. +# Stage TESTPAYSAN (stagepaysan on francpaysan host): +# STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh +# # or from francpaysan-admin-log: ./scripts/stagepaysan/install-landing-stats.sh +# +# Optional: STACK_ENV_FILE=/path/to/profile.env +# Requires linger: sudo loginctl enable-linger $USER set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" ADMIN_LOG="${ADMIN_LOG:-$ROOT}" SRC="$ADMIN_LOG/scripts/taler-landing" UNIT_SRC="$ADMIN_LOG/configs/systemd/user" +PROFILE="${STACK_PROFILE:-goa}" BIN_DST="${HOME}/.local/bin" LIB_DST="${HOME}/.local/lib/taler-landing" UNIT_DST="${HOME}/.config/systemd/user" STATE_DST="${HOME}/.local/state/taler-landing-stats" +CFG_DST="${HOME}/.config/taler-landing" -mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" +mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" "$CFG_DST" install -m 0755 "$SRC/collect-landing-stats.sh" "$BIN_DST/collect-landing-stats.sh" install -m 0755 "$SRC/collect_bank_stats.py" "$LIB_DST/collect_bank_stats.py" install -m 0755 "$SRC/enrich_stats_alt.py" "$LIB_DST/enrich_stats_alt.py" install -m 0755 "$SRC/merge_resources.py" "$LIB_DST/merge_resources.py" install -m 0755 "$SRC/collect_container_resources.sh" "$LIB_DST/collect_container_resources.sh" -install -m 0755 "$SRC/test-landing-stats.sh" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \ - install -m 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh" +if [ -f "$SRC/test-landing-stats.sh" ]; then + install -m 0755 "$SRC/test-landing-stats.sh" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \ + install -m 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh" +fi install -m 0644 "$SRC/goa_amounts.py" "$LIB_DST/goa_amounts.py" # mem-snapshot helper lives in taler-shared (copied into containers at collect time) if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then @@ -37,18 +43,34 @@ if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then "$LIB_DST/mem-snapshot.sh" fi -# Wrapper always uses installed lib next to itself when LIB discovery works; -# also point ADMIN_LOG for in-container script refresh. +# Stack profile → ~/.config/taler-landing/stack.env (sourced by collector) +if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then + install -m 0644 "${STACK_ENV_FILE}" "$CFG_DST/stack.env" +elif [ -f "$SRC/profiles/${PROFILE}.env" ]; then + install -m 0644 "$SRC/profiles/${PROFILE}.env" "$CFG_DST/stack.env" +else + echo "WARN: no profile ${PROFILE}.env — collector uses built-in GOA defaults" >&2 +fi + install -m 0644 "$UNIT_SRC/taler-landing-stats.service" "$UNIT_DST/" install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/" -# Rewrite ExecStart to installed binary if unit uses %h path — units already do. +# Point unit EnvironmentFile at stack profile +if grep -q '^EnvironmentFile=' "$UNIT_DST/taler-landing-stats.service" 2>/dev/null; then + : +else + # inject after [Service] + sed -i '/^\[Service\]/a EnvironmentFile=-%h/.config/taler-landing/stack.env' \ + "$UNIT_DST/taler-landing-stats.service" +fi + systemctl --user daemon-reload systemctl --user enable --now taler-landing-stats.timer -echo "Installed (user=$(id -un)):" +echo "Installed (user=$(id -un) profile=${PROFILE}):" echo " $BIN_DST/collect-landing-stats.sh" echo " $LIB_DST/{collect_bank_stats,enrich_stats_alt,goa_amounts}.py" +echo " $CFG_DST/stack.env" echo " $UNIT_DST/taler-landing-stats.{service,timer} (timer enabled)" echo " logs: $STATE_DST/" echo @@ -61,8 +83,6 @@ echo "Run once now:" echo " systemctl --user start taler-landing-stats.service" echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager" echo -echo "Optional secrets (if not readable via podman exec bank /root/…):" -echo " mkdir -p ~/.config/taler-landing" -echo " cp …/bank-admin-password.txt ~/.config/taler-landing/" -echo " cp …/bank-explorer-password.txt ~/.config/taler-landing/" -echo " chmod 600 ~/.config/taler-landing/*" +echo "Secrets (if not via podman exec bank):" +echo " mkdir -p ~/.config/taler-landing && chmod 700 ~/.config/taler-landing" +echo " # bank-admin-password.txt bank-explorer-password.txt" diff --git a/scripts/taler-landing/profiles/goa.env b/scripts/taler-landing/profiles/goa.env new file mode 100644 index 0000000..08c357a --- /dev/null +++ b/scripts/taler-landing/profiles/goa.env @@ -0,0 +1,18 @@ +# Stack profile: GOA / hacktivism +# systemd EnvironmentFile: KEY=value only (no export) +BANK_CTR=taler-hacktivism-bank +EX_CTR=taler-hacktivism-exchange-ansible +MER_CTR=taler-hacktivism +BANK_URL=http://127.0.0.1:9012 +BANK_PUBLIC_URL=https://bank.hacktivism.ch +EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config +BANK_CURRENCY=GOA +BANK_LANDING_IN=/var/www/bank-landing +EX_LANDING_IN=/var/www/exchange-landing +MER_LANDING_IN=/var/www/merchant-landing +COLLECT_BANK=1 +COLLECT_EXCHANGE=1 +COLLECT_MERCHANT=1 +COLLECT_RESOURCES=1 +PUBLISH_PODMAN=1 +STATS_SOURCE_LABEL="host collect_bank_stats.py goa hacktivism" diff --git a/scripts/taler-landing/profiles/stage-testpaysan.env b/scripts/taler-landing/profiles/stage-testpaysan.env new file mode 100644 index 0000000..b63a58c --- /dev/null +++ b/scripts/taler-landing/profiles/stage-testpaysan.env @@ -0,0 +1,22 @@ +# Stack profile: FrancPaysan stage / TESTPAYSAN (ops user stagepaysan) +# systemd EnvironmentFile: KEY=value only (no export, no shell expansion) +BANK_CTR=stage-lfp-bank +EX_CTR=stage-lfp-exchange-ansible +MER_CTR=stage-lfp-merchant +BANK_URL=http://127.0.0.1:9032 +BANK_PUBLIC_URL=https://stage.bank.lefrancpaysan.ch +EXCHANGE_CONFIG_URL=https://stage.exchange.lefrancpaysan.ch/config +BANK_CURRENCY=TESTPAYSAN +SCAN_SKIP=exchange +BANK_LANDING_IN=/data/var/www +EX_LANDING_IN=/data/var/www +MER_LANDING_IN=/data/var/www +# Shared public feed: one bank scan → same stats.json on all three Caddy landings +# Colon-separated for systemd EnvironmentFile +HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant +COLLECT_BANK=1 +COLLECT_EXCHANGE=0 +COLLECT_MERCHANT=0 +COLLECT_RESOURCES=0 +PUBLISH_PODMAN=1 +STATS_SOURCE_LABEL="host collect_bank_stats.py stage TESTPAYSAN shared" From d2113e73624e66d2fb06e51919436fc026b2560c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Fri, 17 Jul 2026 16:49:17 +0200 Subject: [PATCH 5/8] monitoring: drop remaining run-until-done from GOA ladder --- scripts/taler-monitoring/check_goa_ladder.sh | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/scripts/taler-monitoring/check_goa_ladder.sh b/scripts/taler-monitoring/check_goa_ladder.sh index bcea26a..8b311ce 100755 --- a/scripts/taler-monitoring/check_goa_ladder.sh +++ b/scripts/taler-monitoring/check_goa_ladder.sh @@ -663,12 +663,7 @@ else: fi } - # Short bounded select assist (≤8s) — not a hang path; helps attach fresh reserve_pub - if command -v perl >/dev/null 2>&1 && [ -f "$CLI_JS" ]; then - perl -e 'alarm shift; exec @ARGV' 8 \ - node "$CLI_JS" --wallet-db="$WDB" --no-throttle run-until-done \ - >"$SCRATCH/select-$tag.out" 2>&1 || true - fi + # No run-until-done (hangs / banned). Read transactions only for reserve_pub candidates. wcli transactions >"$SCRATCH/tx-$tag.json" 2>&1 || true t0=$(now_ms) @@ -965,19 +960,12 @@ except Exception: print("") ms_handle=$(elapsed_ms "$t0") ok "handle-uri $PAMT ${ms_handle}ms" - # Short settle polls (bounded — no infinite hang) + # Short settle polls: merchant order + wallet tx only — never run-until-done t0=$(now_ms) settled=0 r=0 while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do r=$((r + 1)) - if command -v perl >/dev/null 2>&1; then - perl -e 'alarm shift; exec @ARGV' 10 \ - "$CLI_JS" --wallet-db="$WDB" --no-throttle run-until-done \ - >"$SCRATCH/pay-run-$ptag.out" 2>&1 || true - else - wcli run-until-done >"$SCRATCH/pay-run-$ptag.out" 2>&1 || true - fi wcli transactions >"$SCRATCH/tx-$ptag.out" 2>&1 || true curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \ "${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true @@ -989,6 +977,7 @@ sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="p settled=1 break fi + sleep 0.5 done ms_psettle=$(elapsed_ms "$t0") ms_total=$(elapsed_ms "$t_pay") From 62d0e426f4a72ed63b4fb4396f2f19d140da4357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Fri, 17 Jul 2026 16:49:17 +0200 Subject: [PATCH 6/8] monitoring: e2e without run-until-done (wallet serve + poll) --- scripts/taler-monitoring/check_e2e.sh | 332 ++++++++++++++++++++------ scripts/taler-monitoring/lib.sh | 7 + 2 files changed, 271 insertions(+), 68 deletions(-) diff --git a/scripts/taler-monitoring/check_e2e.sh b/scripts/taler-monitoring/check_e2e.sh index 7935d7a..ee4ea08 100755 --- a/scripts/taler-monitoring/check_e2e.sh +++ b/scripts/taler-monitoring/check_e2e.sh @@ -23,18 +23,36 @@ e2e_over() { [ "$(e2e_left)" -le 0 ]; } # stdout only for JSON; logs go to stderr file so balance parse stays clean wcli_bal_snap() { local out="$1" - if [ -z "${WALLET_CLI:-}" ] || [ ! -f "${WDB:-}" ]; then + if [ -z "${WALLET_CLI:-}" ]; then echo "(no wallet)" >"$out" return 1 fi + # one-shot needs DB file; serve mode needs socket + if [ -z "${WSERVE_PID:-}" ] || [ ! -S "${WSOCK:-}" ]; then + if [ ! -f "${WDB:-}" ]; then + echo "(no wallet)" >"$out" + return 1 + fi + fi local err="${out}.err" if command -v perl >/dev/null 2>&1; then - perl -e 'alarm shift; exec @ARGV' 10 \ - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ - >"$out" 2>"$err" || true + if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then + perl -e 'alarm shift; exec @ARGV' 10 \ + node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \ + >"$out" 2>"$err" || true + else + perl -e 'alarm shift; exec @ARGV' 10 \ + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ + >"$out" 2>"$err" || true + fi else - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ - >"$out" 2>"$err" || true + if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then + node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \ + >"$out" 2>"$err" || true + else + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ + >"$out" 2>"$err" || true + fi fi [ -s "$out" ] } @@ -104,9 +122,9 @@ wait_wallet_balance() { local rounds="${2:-15}" local sleep_s="${3:-3}" local r av - info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (wirewatch lag is common)" + # Poll balance only — never run-until-done (hangs; banned in monitoring). + info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (poll balance only; no run-until-done)" for r in $(seq 1 "$rounds"); do - wcli run-until-done >"$SCRATCH/settle-run.out" 2>&1 || true wcli_bal_snap "$SCRATCH/bal-live.out" || wcli balance >"$SCRATCH/bal-live.out" 2>&1 || true av=$(wallet_avail_num "$SCRATCH/bal-live.out") if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > float(sys.argv[2]) else 1)" "$av" "$min_n" 2>/dev/null; then @@ -221,12 +239,22 @@ except Exception: print("")' 2>/dev/null || true) if [ "$E2E_REMOTE" = "1" ]; then # remote: still ATM-shaped but smaller notes; keep cheap : "${E2E_WITHDRAW_VALUES:=10 20 50}" - : "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}" + if [ "$CUR" = "TESTPAYSAN" ]; then + # match fixed public templates e2e-001 / e2e-005 / e2e-1 + : "${E2E_PAY_VALUES:=0.01 0.05 1}" + else + : "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}" + fi else # local GOA: classic ATM notes + 4200 for paivana paywall template : "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}" : "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}" fi +# Public template pays (landing/shop style) — default on for TESTPAYSAN stage +if [ -z "${E2E_USE_TEMPLATES:-}" ]; then + if [ "$CUR" = "TESTPAYSAN" ]; then E2E_USE_TEMPLATES=1; else E2E_USE_TEMPLATES=0; fi +fi +: "${E2E_USE_TEMPLATES:=0}" # Build CUR:amount lists build_amt_list() { @@ -300,12 +328,27 @@ BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BA SCRATCH=$(mktemp -d) WDB="$SCRATCH/wallet.sqlite3" +WSOCK="$SCRATCH/wallet.sock" +WSERVE_PID="" +# Client/server wallet (advanced serve): long-lived shepherd without run-until-done. +# Default on — wallet-cli only completes withdraw/pay while a serve process is up. +: "${E2E_WALLET_SERVE:=1}" METRICS_DIR="${METRICS_DIR:-$SCRATCH/metrics}" mkdir -p "$METRICS_DIR" export METRICS_DIR CUR="${CUR:-${EXPECT_CURRENCY:-GOA}}" WDB export PATH="${HOME}/.local/bin:${HOME}/taler/opt/taler-wallet-cli/usr/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" + +stop_wallet_serve() { + if [ -n "${WSERVE_PID:-}" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then + kill "$WSERVE_PID" 2>/dev/null || true + wait "$WSERVE_PID" 2>/dev/null || true + fi + WSERVE_PID="" + rm -f "${WSOCK:-}" 2>/dev/null || true +} + # Always dump balances on any exit (timeout, blocker, signal, success) -trap 'ec=$?; e2e_finish "$ec"; rm -rf "$SCRATCH"; exit "$ec"' EXIT +trap 'ec=$?; e2e_finish "$ec"; stop_wallet_serve; rm -rf "$SCRATCH"; exit "$ec"' EXIT # Abort cleanly on login / KYC / registration barriers (esp. remote domains) e2e_abort_auth() { @@ -405,13 +448,29 @@ else exit 1 fi if [ -n "$MPW" ]; then + # Secrets often store full "secret-token:…"; strip before re-prefixing Bearer. + MPW=$(printf '%s' "$MPW" | tr -d '\n\r') + case "$MPW" in + secret-token:*) MPW_TOKEN="$MPW" ;; + *) MPW_TOKEN="secret-token:${MPW}" ;; + esac ok "merchant secret (instance ${MERCHANT_INSTANCE})" elif [ "$E2E_REMOTE" = "1" ]; then - warn "merchant secret" "missing — order create may fail (set E2E_MERCHANT_TOKEN)" + MPW_TOKEN="" + warn "merchant secret" "missing — private orders fail unless E2E_USE_TEMPLATES=1 (set E2E_MERCHANT_TOKEN)" else blocker "prereq" "merchant instance password missing" exit 1 fi +# Authorization header for merchant private API (never double secret-token:) +merchant_auth_header() { + local t="${1:-${MPW_TOKEN:-}}" + [ -n "$t" ] || return 1 + case "$t" in + secret-token:*) printf 'Authorization: Bearer %s' "$t" ;; + *) printf 'Authorization: Bearer secret-token:%s' "$t" ;; + esac +} # Public reachability gates (remote: soft-skip if bank/merchant absent) for pair in \ @@ -455,7 +514,31 @@ info "e2e user" "$USER withdraw=$WITHDRAW_AMT pay=$PAY_AMT" BANK="$BANK_PUBLIC" INST="$MERCHANT_INSTANCE" +start_wallet_serve() { + [ "${E2E_WALLET_SERVE}" = "1" ] || return 0 + [ -n "${WALLET_CLI:-}" ] && [ -f "${WALLET_CLI}" ] || return 1 + stop_wallet_serve + rm -f "$WSOCK" + # serve holds the DB open; clients use --wallet-connection only + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults \ + advanced serve --unix-path "$WSOCK" \ + >"$SCRATCH/wallet-serve.log" 2>&1 & + WSERVE_PID=$! + local i + for i in $(seq 1 40); do + if [ -S "$WSOCK" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then + ok "wallet serve" "pid=$WSERVE_PID sock=$WSOCK (no run-until-done)" + return 0 + fi + sleep 0.15 + done + warn "wallet serve" "socket not ready — falling back to one-shot CLI (coins may stay pending)" + stop_wallet_serve + return 1 +} + # $1 = max seconds for this call (optional); rest = wallet-cli args +# Prefer live serve socket so the shepherd stays up (alternative to run-until-done). wcli() { local maxc=12 if [[ "${1:-}" =~ ^[0-9]+$ ]]; then @@ -467,11 +550,20 @@ wcli() { cap=$(e2e_left) [ "$cap" -gt "$maxc" ] && cap=$maxc [ "$cap" -lt 3 ] && return 124 - if command -v perl >/dev/null 2>&1; then - perl -e 'alarm shift; exec @ARGV' "$cap" \ - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" + if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then + if command -v perl >/dev/null 2>&1; then + perl -e 'alarm shift; exec @ARGV' "$cap" \ + node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@" + else + node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@" + fi else - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" + if command -v perl >/dev/null 2>&1; then + perl -e 'alarm shift; exec @ARGV' "$cap" \ + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" + else + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" + fi fi } @@ -489,11 +581,20 @@ wcli_pay() { cap=12 fi info "pay wallet cap" "${cap}s" - if command -v perl >/dev/null 2>&1; then - perl -e 'alarm shift; exec @ARGV' "$cap" \ - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" + if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then + if command -v perl >/dev/null 2>&1; then + perl -e 'alarm shift; exec @ARGV' "$cap" \ + node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@" + else + node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@" + fi else - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" + if command -v perl >/dev/null 2>&1; then + perl -e 'alarm shift; exec @ARGV' "$cap" \ + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" + else + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" + fi fi } @@ -686,6 +787,8 @@ fi set_group wallet section "e2e · wallet setup (exchange + ToS once)" # --------------------------------------------------------------------------- +# Long-lived serve = client/server wallet (shepherd keeps running; no run-until-done). +start_wallet_serve || true if ! wcli exchanges add "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-add.out" 2>&1; then if ! grep -qiE 'already|ok' "$SCRATCH/ex-add.out"; then blocker "wallet-exchange" "exchanges add failed — $(tail -c 160 "$SCRATCH/ex-add.out" | tr '\n' ' ')" @@ -780,12 +883,13 @@ e2e_one_withdraw() { fi cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out" + # Poll bank withdrawal status only (no run-until-done). force-select if stuck pending. st="" - for i in 1 2 3 4; do + for i in 1 2 3 4 5 6 8 10; do e2e_over && break - wcli run-until-done >"$SCRATCH/sel-$tag-$i.out" 2>&1 || true st=$(wd_status) case "$st" in selected|confirmed|aborted) break ;; esac + sleep 0.4 done if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true @@ -830,10 +934,9 @@ else: fi fake_incoming_speedup - # Short per-ATM poll; full settle wait happens after the ladder (avoids false FAIL) - local ok_bal=0 r av - for r in 1 2 3 4 5 6; do - wcli run-until-done >"$SCRATCH/run-$tag-$r.out" 2>&1 || true + # Short per-ATM settle: poll balance + bank transfer_done only (never run-until-done) + local ok_bal=0 r av xfer + for r in 1 2 3 4 5 6 8 10; do wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true av=$(wallet_avail_num "$SCRATCH/bal-$tag.out") if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then @@ -842,6 +945,13 @@ else: info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")" break fi + xfer=$(curl -sS -m 8 "$BANK/taler-integration/withdrawal-operation/${WID}" 2>/dev/null \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("transfer_done"), d.get("status"))' 2>/dev/null || echo "?") + if echo "$xfer" | grep -qi True; then + ok "ATM $WITHDRAW_AMT bank transfer_done" "wallet avail=${CUR}:${av} ($xfer) — no run-until-done" + ok_bal=1 + break + fi sleep 2 done metrics_report_coins "after-ATM-${tag}" || true @@ -859,8 +969,28 @@ else: return 1 } +# Map CUR:amount → public template id (TESTPAYSAN stage e2e-* templates). +# Override with E2E_TEMPLATE_MAP="0.01=e2e-001 0.05=e2e-005 1=e2e-1" +e2e_template_id_for_amount() { + local amt="$1" + local val map_entry k v + val=$(printf '%s' "$amt" | awk -F: '{print $NF}') + : "${E2E_TEMPLATE_MAP:=0.01=e2e-001 0.05=e2e-005 1=e2e-1 1.0=e2e-1}" + for map_entry in $E2E_TEMPLATE_MAP; do + k="${map_entry%%=*}" + v="${map_entry#*=}" + if [ "$k" = "$val" ]; then + printf '%s' "$v" + return 0 + fi + done + return 1 +} + # One payment: e2e_one_pay AMOUNT [SUMMARY] [TAG] # SUMMARY defaults to "monitoring pay $AMOUNT"; TAG defaults from amount. +# When E2E_USE_TEMPLATES=1 and amount maps to a public template, uses +# POST /instances/{inst}/templates/{id} (same as landings/shops). e2e_one_pay() { PAY_AMT="$1" local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}" @@ -873,11 +1003,28 @@ e2e_one_pay() { section "e2e · pay $PAY_AMT · $(format_amount_alt "$PAY_AMT") · $PAY_SUM" e2e_over && { warn "pay" "budget exhausted — skip $PAY_AMT ($PAY_SUM)"; return 1; } metrics_report_coins "before-pay-${tag}" || true - if [ -z "${MPW:-}" ]; then + + # Prefer public templates when enabled (stage TESTPAYSAN / shop-style orders) + local tpl_id="" + if [ "${E2E_USE_TEMPLATES:-0}" = "1" ]; then + tpl_id=$(e2e_template_id_for_amount "$PAY_AMT" 2>/dev/null || true) + if [ -n "$tpl_id" ]; then + info "pay $PAY_AMT" "via public template $tpl_id (E2E_USE_TEMPLATES=1)" + e2e_one_pay_public_template "$tpl_id" "$PAY_SUM" "$PAY_AMT" + return $? + fi + warn "pay $PAY_AMT" "no template map for amount — fall back to private order" + fi + + if [ -z "${MPW:-}" ] && [ -z "${MPW_TOKEN:-}" ]; then warn "pay $PAY_AMT" "no merchant token" return 1 fi - AUTH="Authorization: Bearer secret-token:${MPW}" + local AUTH + AUTH=$(merchant_auth_header) || { + warn "pay $PAY_AMT" "no merchant token" + return 1 + } # JSON-escape summary for curl -d local SUM_JSON SUM_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$PAY_SUM" 2>/dev/null || printf '"%s"' "$PAY_SUM") @@ -887,8 +1034,9 @@ e2e_one_pay() { "${MERCHANT_PUBLIC}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$tag.err" || true if ! grep -q order_id "$SCRATCH/ord-$tag.json" 2>/dev/null; then if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then + local mpw_raw="${MPW_TOKEN:-secret-token:${MPW}}" koopa_ssh_run 20 \ - "curl -skS -m 12 -X POST -H 'Authorization: Bearer secret-token:${MPW}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \ + "curl -skS -m 12 -X POST -H 'Authorization: Bearer ${mpw_raw}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \ >"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true fi fi @@ -932,23 +1080,37 @@ except Exception: print("") return 1 fi ok "wallet handle pay $PAY_AMT ($PAY_SUM)" + # Settle: poll merchant order + wallet transactions only (never run-until-done) local r - for r in 1 2 3; do - wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true + for r in 1 2 3 4 5 6; do wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true - curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \ - "${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true + if [ -n "${AUTH:-}" ]; then + curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \ + "${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true + fi + if [ -n "${OTOK:-}" ]; then + curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" \ + "${MERCHANT_PUBLIC}/instances/${INST}/orders/$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OID")?token=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OTOK")" \ + 2>/dev/null || true + fi if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \ || grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \ || python3 -c 'import json,sys -d=json.load(open(sys.argv[1])) -sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1) -' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then +for p in sys.argv[1:]: + try: + d=json.load(open(p)) + if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid": + sys.exit(0) + except Exception: + pass +sys.exit(1) +' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null; then ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)" metrics_report_coins "after-pay-${tag}" || true metrics_record_flow spent "$PAY_AMT" || true return 0 fi + sleep 1 done metrics_report_coins "after-pay-fail-${tag}" || true warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID" @@ -1051,33 +1213,34 @@ except Exception: print("") fi ok "shop $pname" "wallet accepted pay URI" + # Settle: public order status + transactions only (never run-until-done) local r AUTH="" - if [ -n "${MPW:-}" ]; then - AUTH="Authorization: Bearer secret-token:${MPW}" - fi - for r in 1 2 3; do - wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true + AUTH=$(merchant_auth_header 2>/dev/null) || AUTH="" + for r in 1 2 3 4 5 6; do wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true if [ -n "$AUTH" ]; then curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \ "${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true - if python3 -c 'import json,sys -d=json.load(open(sys.argv[1])) -sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1) -' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then - ok "shop $pname" "payment settled ($pamt · order $OID)" - metrics_report_coins "after-shop-${tag}" || true - metrics_record_flow spent "$pamt" || true - return 0 - fi fi - if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \ + curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" "$statusUrl" 2>/dev/null || true + if python3 -c 'import json,sys +for p in sys.argv[1:]: + try: + d=json.load(open(p)) + if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid": + sys.exit(0) + except Exception: + pass +sys.exit(1) +' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null \ + || grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \ || grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null; then - ok "shop $pname" "payment settled via wallet tx ($pamt · order $OID)" + ok "shop $pname" "payment settled ($pamt · order $OID)" metrics_report_coins "after-shop-${tag}" || true metrics_record_flow spent "$pamt" || true return 0 fi + sleep 1 done metrics_report_coins "after-shop-fail-${tag}" || true warn "shop $pname ($pid)" "not settled for order $OID" @@ -1171,13 +1334,16 @@ PAY_OK_N=0 PAY_FAIL_N=0 PAY_SKIP_N=0 PAY_REPORT="" -if [ -z "${MPW:-}" ]; then +if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" != "1" ]; then if [ "$E2E_REMOTE" = "1" ]; then - e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN" + e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN (or E2E_USE_TEMPLATES=1)" fi blocker "merchant-order" "no merchant token" exit 1 fi +if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" = "1" ]; then + info "pay" "no merchant token — public templates only (E2E_USE_TEMPLATES=1)" +fi for PAY_AMT in $PAY_LIST; do e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; } @@ -1219,18 +1385,46 @@ fi # --------------------------------------------------------------------------- set_group shop -section "e2e · GOA shop products (merchant landing catalog)" +section "e2e · shop products (public templates)" # --------------------------------------------------------------------------- -# Public template POST + wallet pay — full catalog list, random pick of N products. +# Public template POST + wallet pay — catalog random pick of N products. +# GOA: goa-shop on hacktivism. TESTPAYSAN: farmer shops (jardin / fermes). SHOP_OK=0 SHOP_OK_N=0 SHOP_FAIL_N=0 SHOP_SKIP_N=0 SHOP_REPORT="" -if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then - info "goa-shop" "SKIPPED (remote or non-GOA currency)" +# Auto catalog for stage TESTPAYSAN when not overridden +if [ "${CUR:-}" = "TESTPAYSAN" ] && [ -z "${E2E_SHOP_PRODUCTS_SET:-}" ] && [ -z "${E2E_SHOP_FORCE_GOA:-}" ]; then + # id|name|amount|instance — force stage catalog (GOA default is set earlier) + E2E_SHOP_PRODUCTS="oeufs-6|Œufs plein air × 6|TESTPAYSAN:5|jardin-du-creux +pain-seigle|Pain de seigle 800 g|TESTPAYSAN:4.5|jardin-du-creux +jus-pomme|Jus de pomme 1 L|TESTPAYSAN:6|jardin-du-creux +panier-legumes|Panier légumes|TESTPAYSAN:25|fermes-des-collines +fromage-chevre|Fromage de chèvre 200 g|TESTPAYSAN:8.5|fermes-des-collines +miel-printemps|Miel de printemps 250 g|TESTPAYSAN:12|fermes-des-collines" + # Force stage defaults (GOA goa-shop is already set earlier via :=) + if [ -z "${E2E_SHOP_INSTANCE_SET:-}" ]; then + E2E_SHOP_INSTANCE=jardin-du-creux + fi + if [ -z "${E2E_SHOP_PAYTO_SET:-}" ]; then + E2E_SHOP_PAYTO="payto://x-taler-bank/stage.bank.lefrancpaysan.ch/jardin-du-creux" + fi + E2E_SHOP_PICK_N="${E2E_SHOP_PICK_N:-2}" + E2E_SHOP_ENABLE=1 +fi +# GOA local only by default; remote non-GOA shops need E2E_SHOP_ENABLE=1 or TESTPAYSAN auto above +if [ -z "${E2E_SHOP_ENABLE:-}" ]; then + if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then + E2E_SHOP_ENABLE=0 + else + E2E_SHOP_ENABLE=1 + fi +fi +if [ "${E2E_SHOP_ENABLE}" != "1" ]; then + info "shop" "SKIPPED (set E2E_SHOP_ENABLE=1 or use TESTPAYSAN stage catalog)" else - # Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount lines) + # Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount[|instance] lines) SHOP_CATALOG=() while IFS= read -r line; do [ -z "$line" ] && continue @@ -1244,7 +1438,7 @@ EOF case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi if [ "$SHOP_CAT_N" -eq 0 ]; then - warn "goa-shop" "empty E2E_SHOP_PRODUCTS catalog" + warn "shop" "empty E2E_SHOP_PRODUCTS catalog" else if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi # Shuffle catalog, take first PICK_N (portable: python) @@ -1257,24 +1451,26 @@ random.shuffle(lines) print('\n'.join(lines[:n])) " "$PICK_N" ) - info "goa-shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · instance ${E2E_SHOP_INSTANCE}" - info "goa-shop payto" "$E2E_SHOP_PAYTO" - info "goa-shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')" - # e2e_one_pay_public_template uses global INST — temporarily pin shop instance + info "shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · default instance ${E2E_SHOP_INSTANCE}" + info "shop payto" "$E2E_SHOP_PAYTO" + info "shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')" + # e2e_one_pay_public_template uses global INST — pin per product if 4th field set _E2E_INST_SAVE="$INST" INST="${E2E_SHOP_INSTANCE}" while IFS= read -r line; do [ -z "$line" ] && continue - e2e_over && { warn "goa-shop" "time budget low — stopping product pays"; break; } + e2e_over && { warn "shop" "time budget low — stopping product pays"; break; } pid=$(printf '%s' "$line" | cut -d'|' -f1) pname=$(printf '%s' "$line" | cut -d'|' -f2) pamt=$(printf '%s' "$line" | cut -d'|' -f3) + pinst=$(printf '%s' "$line" | cut -d'|' -f4) [ -z "$pid" ] || [ -z "$pamt" ] && continue [ -z "$pname" ] && pname="$pid" + INST="${pinst:-${E2E_SHOP_INSTANCE}}" av_now=$(wallet_avail_num) pay_n=$(python3 -c 'import sys; print(float(sys.argv[1].split(":",1)[-1]))' "$pamt" 2>/dev/null || echo 0) if ! python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) + 1e-12 >= float(sys.argv[2]) else 1)" "$av_now" "$pay_n" 2>/dev/null; then - warn "goa-shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})" + warn "shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})" SHOP_SKIP_N=$((SHOP_SKIP_N + 1)) SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)" continue @@ -1298,9 +1494,9 @@ $SHOP_PICKED EOF INST="$_E2E_INST_SAVE" unset _E2E_INST_SAVE - info "goa-shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)" + info "shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)" if [ "$SHOP_OK" != "1" ] && [ "$SHOP_FAIL_N" -gt 0 ]; then - warn "goa-shop" "no sample product payment succeeded ($SHOP_REPORT)" + warn "shop" "no sample product payment succeeded ($SHOP_REPORT)" fi section "e2e · load snapshot (after shop pays)" metrics_report_load "${METRICS_DIR}/load-after-shop.json" "after-shop" || true @@ -1406,7 +1602,7 @@ section "e2e · report" info "user" "$USER" info "ATM withdraws" "$WITHDRAW_REPORT" info "payments" "$PAY_REPORT" -info "goa-shop" "${SHOP_REPORT:-(n/a)}" +info "shop" "${SHOP_REPORT:-(n/a)}" info "paivana" "${PAIVANA_REPORT:-(n/a)}" info "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")" info "scratch" "$SCRATCH" diff --git a/scripts/taler-monitoring/lib.sh b/scripts/taler-monitoring/lib.sh index 5732d1d..65c6005 100755 --- a/scripts/taler-monitoring/lib.sh +++ b/scripts/taler-monitoring/lib.sh @@ -193,6 +193,13 @@ apply_taler_domain() { PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}" CREDIT_AMT="${CREDIT_AMT:-${EXPECT_CURRENCY}:100}" ;; + TESTPAYSAN) + # Stage FrancPaysan: default instance + public templates (e2e-001/005/1) + MERCHANT_INSTANCE="${MERCHANT_INSTANCE:-default}" + WITHDRAW_AMT="${WITHDRAW_AMT:-TESTPAYSAN:20}" + PAY_AMT="${PAY_AMT:-TESTPAYSAN:0.01}" + CREDIT_AMT="${CREDIT_AMT:-TESTPAYSAN:100}" + ;; CHF) WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}" PAY_AMT="${PAY_AMT:-CHF:0.01}" From 3d5f412af32346e42c7c673bad360273144211b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Fri, 17 Jul 2026 16:56:09 +0200 Subject: [PATCH 7/8] monitoring: stats.json freshness + display fields outside-in --- .../taler-exchange/landing-stats-exchange.sh | 2 + .../taler-merchant/landing-stats-merchant.sh | 2 + scripts/taler-monitoring/README.md | 19 ++ scripts/taler-monitoring/TESTS.md | 2 +- scripts/taler-monitoring/check_urls.sh | 308 ++++++++++++++---- 5 files changed, 273 insertions(+), 60 deletions(-) diff --git a/scripts/taler-exchange/landing-stats-exchange.sh b/scripts/taler-exchange/landing-stats-exchange.sh index 35452d4..987e631 100644 --- a/scripts/taler-exchange/landing-stats-exchange.sh +++ b/scripts/taler-exchange/landing-stats-exchange.sh @@ -238,6 +238,7 @@ fi GEN=$(now_iso) HUMAN=$(now_human) +GEN_UNIX=$(date +%s) num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; } MEM_JSON='"container_rss_human": "—"' @@ -257,6 +258,7 @@ cat >"$TMP" < this (default 900 = 15m; timer often 5–15m) +# STATS_FAIL_SECS hard fail if age > this (default 3600); 0 = never hard-fail on age # --------------------------------------------------------------------------- -: "${STATS_STALE_SECS:=900}" # warn if generated_at older than 15m +: "${STATS_STALE_SECS:=900}" +: "${STATS_FAIL_SECS:=3600}" _fetch_landing_stats_json() { # $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok @@ -326,82 +333,265 @@ _fetch_landing_stats_json() { return 1 } -report_landing_load_stats() { +# Validate one stats.json for monitoring display + outside age measurement. +# Prints lines: OK|WARN|ERR|STALE|FAIL +# Sets $tmp/stats-meta-$name.txt with gen_unix|source for shared-feed compare. +check_landing_stats_json() { local name="$1" base="$2" local f="$tmp/stats-${name}.json" - local line age_s + local line hard_missing=0 if ! _fetch_landing_stats_json "$name" "$base"; then if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then - warn "perf ${name} landing-stats" "stats.json not available (public or SSH)" + fail "stats ${name}" "HTTP/body missing · ${base}/intro/stats.json" + else + warn "stats ${name}" "stats.json not available" fi - return + return 1 fi + ok "stats ${name} reachable" "${base}/intro/stats.json" + + # Multi-line verdict from python (STATUS detail) + while IFS= read -r line; do + case "$line" in + META\ *) + printf '%s\n' "${line#META }" >"$tmp/stats-meta-${name}.txt" + ;; + OK\ *) + ok "stats ${name}" "${line#OK }" + ;; + WARN\ *) + warn "stats ${name}" "${line#WARN }" + ;; + STALE\ *) + warn "stats ${name} freshness" "stale · ${line#STALE }" + ;; + FAIL\ *) + fail "stats ${name}" "${line#FAIL }" + hard_missing=1 + ;; + ERR\ *) + if [ "${LOCAL_STACK:-0}" = "1" ] || [ "${CHECK_LANDING:-1}" = "1" ]; then + fail "stats ${name}" "${line#ERR }" + else + warn "stats ${name}" "${line#ERR }" + fi + hard_missing=1 + ;; + *) + [ -n "$line" ] && warn "stats ${name}" "unparsed · $line" + ;; + esac + done < <(python3 - "$f" "$name" "${STATS_STALE_SECS}" "${STATS_FAIL_SECS}" <<'PY' 2>/dev/null || echo "ERR python check failed" +import json, sys, time, re +from datetime import datetime, timezone + +path, role, stale_s, fail_s = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]) +now = int(time.time()) + +def out(kind, msg): + print("%s %s" % (kind, msg)) - line=$(python3 - "$f" "${STATS_STALE_SECS}" <<'PY' 2>/dev/null || true -import json, sys, time -path, stale = sys.argv[1], int(sys.argv[2]) try: d = json.load(open(path)) except Exception as e: - print("ERR parse:" + str(e)[:80]) + out("ERR", "JSON parse: %s" % str(e)[:100]) sys.exit(0) -if not d.get("ok", True): - print("ERR ok=false " + str(d.get("error") or "")[:80]) + +if not isinstance(d, dict): + out("ERR", "body is not a JSON object") sys.exit(0) -p = d.get("performance") or {} -if not isinstance(p, dict) or not p: - print("ERR no performance block") + +if d.get("ok") is False: + out("ERR", "ok=false · %s" % str(d.get("error") or d.get("hint") or "")[:80]) sys.exit(0) -mem = p.get("memory") if isinstance(p.get("memory"), dict) else {} -rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "?" -load = p.get("loadavg") or "?" -# latency fields differ by site -bits = [] -for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"): - if k in p and p[k] is not None: - bits.append("%s=%sms" % (k.replace("_ms", ""), p[k])) -lat = " ".join(bits) if bits else "latency=?" -gen = d.get("generated_at_human") or d.get("generated_at") or "?" -# staleness -age = "" -try: - gu = d.get("generated_at_unix") - if gu is not None: - age_s = int(time.time()) - int(gu) - age = " age=%ss" % age_s - if age_s > stale: - print("STALE loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age)) - sys.exit(0) -except Exception: - pass -print("OK loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age)) + +# --- timestamps (outside-in age measurement) --- +gu = d.get("generated_at_unix") +giso = d.get("generated_at") or d.get("generated_at_iso") +ghum = d.get("generated_at_human") +age = None +src_u = None + +if gu is not None and str(gu).strip() != "": + try: + src_u = int(float(gu)) + # tolerate ms accidental timestamps + if src_u > 10_000_000_000: + src_u //= 1000 + age = now - src_u + except Exception: + out("ERR", "generated_at_unix not numeric: %r" % gu) + src_u = None +elif giso: + # parse ISO when unix missing (legacy exchange/merchant before unix field) + try: + s = str(giso).strip() + if s.endswith("Z"): + s = s[:-1] + "+00:00" + # allow +0200 without colon + s = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", s) + dt = datetime.fromisoformat(s) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + src_u = int(dt.timestamp()) + age = now - src_u + out("WARN", "no generated_at_unix — derived age from generated_at ISO (deploy collector with unix)") + except Exception as e: + out("ERR", "cannot parse generated_at=%r (%s)" % (giso, e)) +else: + out("ERR", "missing generated_at_unix and generated_at — cannot measure freshness outside") + +if not ghum and not giso: + out("ERR", "missing generated_at_human/generated_at — UI cannot show “updated” time") +elif ghum: + out("OK", "timestamp human=%s" % ghum) +else: + out("OK", "timestamp iso=%s" % giso) + +if src_u is not None: + out("META", "%s|%s|%s" % (src_u, d.get("source") or "", d.get("currency") or "")) + if age is not None and age < -120: + out("WARN", "clock skew · generated_at_unix in the future by %ss" % (-age)) + if age is not None: + out("OK", "generated_at_unix=%s age=%ss (now=%s)" % (src_u, age, now)) + if fail_s > 0 and age > fail_s: + out("FAIL", "age=%ss > STATS_FAIL_SECS=%s · collector not updating" % (age, fail_s)) + elif age > stale_s: + out("STALE", "age=%ss > STATS_STALE_SECS=%s · timer lag or stuck publish" % (age, stale_s)) + +# --- display fields for landings / monitoring --- +src = str(d.get("source") or "") +# Bank-shaped (incl. stage shared feed on all three landings) +bankish = ( + role == "bank" + or "collect_bank_stats" in src + or "shared" in src + or isinstance(d.get("withdraws"), dict) + or isinstance(d.get("bank_accounts"), dict) +) +if bankish: + missing = [] + if not isinstance(d.get("withdraws"), dict): + missing.append("withdraws") + ba = d.get("bank_accounts") + wl = d.get("wallets") + if not isinstance(ba, dict) and not isinstance(wl, dict): + missing.append("bank_accounts|wallets") + if d.get("currency") in (None, ""): + missing.append("currency") + if missing: + out("ERR", "bank-shaped display missing: %s" % ",".join(missing)) + else: + w = d.get("withdraws") or {} + ba = ba if isinstance(ba, dict) else {} + n_acc = ba.get("total") if ba.get("total") is not None else ba.get("users") + out( + "OK", + "display bank-shape currency=%s accounts=%s withdraws.count=%s source=%s" + % (d.get("currency"), n_acc, w.get("count"), (src or "?")[:48]), + ) +elif src == "exchange-db" or (role == "exchange" and "reserves" in d): + need = [] + for k in ("reserves", "wire_in_count", "known_coins"): + if k not in d and k.replace("known_coins", "coins_live") not in d: + if k == "known_coins" and ("coins_live" in d or "coins_remaining_amount" in d): + continue + need.append(k) + if need: + out("WARN", "exchange display keys missing: %s" % ",".join(need)) + else: + out( + "OK", + "display exchange-db reserves=%s wire_in=%s coins=%s" + % (d.get("reserves"), d.get("wire_in_count"), d.get("known_coins") or d.get("coins_live")), + ) +elif src == "merchant-db" or (role == "merchant" and "instances" in d): + if "instances" not in d and "orders" not in d: + out("ERR", "merchant display missing instances/orders") + else: + out( + "OK", + "display merchant-db instances=%s orders=%s paid=%s" + % (d.get("instances"), d.get("orders"), d.get("paid")), + ) +else: + out("WARN", "unknown stats schema role=%s source=%s keys=%s" % (role, src[:40], ",".join(list(d.keys())[:8]))) + +# performance block optional (stage shared bank feed may omit heavy probes) +p = d.get("performance") +if isinstance(p, dict) and p: + mem = p.get("memory") if isinstance(p.get("memory"), dict) else {} + rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "—" + load = p.get("loadavg") or "—" + bits = [] + for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"): + if k in p and p[k] is not None: + bits.append("%s=%s" % (k.replace("_ms", ""), p[k])) + lat = " ".join(bits) if bits else "latency=—" + out("OK", "performance loadavg=%s RSS=%s %s" % (load, rss, lat)) +else: + out("OK", "performance block optional/absent") PY ) - case "$line" in - OK\ *) - ok "perf ${name} landing-stats" "${line#OK }" - ;; - STALE\ *) - warn "perf ${name} landing-stats" "stale · ${line#STALE }" - ;; - ERR\ *) - warn "perf ${name} landing-stats" "${line#ERR }" - ;; - *) - warn "perf ${name} landing-stats" "unreadable stats.json" - ;; - esac + return 0 } if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then set_group stats - section "www · performance · landing load stats (stats.json · in-container probes)" - report_landing_load_stats "bank" "$BANK_PUBLIC" - report_landing_load_stats "exchange" "$EXCHANGE_PUBLIC" - report_landing_load_stats "merchant" "$MERCHANT_PUBLIC" - info "perf landing-stats note" "from /intro/stats.json (public); SSH container fallback if needed" + section "www · landing stats.json (freshness + display fields · outside-in)" + info "stats policy" "STALE≥${STATS_STALE_SECS}s WARN · FAIL≥${STATS_FAIL_SECS}s ERROR (0=disable fail) · need generated_at_unix" + check_landing_stats_json "bank" "$BANK_PUBLIC" || true + check_landing_stats_json "exchange" "$EXCHANGE_PUBLIC" || true + check_landing_stats_json "merchant" "$MERCHANT_PUBLIC" || true + + # Shared feed: stage-style publish copies same bank stats to all three URLs + if [ -f "$tmp/stats-meta-bank.txt" ] && [ -f "$tmp/stats-meta-exchange.txt" ] && [ -f "$tmp/stats-meta-merchant.txt" ]; then + shared_line=$(python3 - "$tmp/stats-meta-bank.txt" "$tmp/stats-meta-exchange.txt" "$tmp/stats-meta-merchant.txt" <<'PY' 2>/dev/null || true +import sys +def parse(p): + try: + t = open(p).read().strip().split("|", 2) + return t[0], (t[1] if len(t) > 1 else ""), (t[2] if len(t) > 2 else "") + except Exception: + return "", "", "" +b, bs, bc = parse(sys.argv[1]) +e, es, ec = parse(sys.argv[2]) +m, ms, mc = parse(sys.argv[3]) +if not b or not e or not m: + print("SKIP incomplete meta") +elif b == e == m: + print("SHARED gen_unix=%s bank_src=%s" % (b, (bs or "?")[:50])) +else: + # Shared feed only when all three claim the same bank-collector source + # (e.g. stage TESTPAYSAN "… shared"). GOA uses independent exchange-db / + # merchant-db collectors — different gen_unix is expected. + def bankish(s): + s = (s or "").lower() + return "shared" in s or "collect_bank_stats" in s + if bankish(bs) and bankish(es) and bankish(ms): + print("DRIFT bank=%s exchange=%s merchant=%s (expected equal for shared feed)" % (b, e, m)) + else: + print("INDEPENDENT bank=%s exchange=%s merchant=%s" % (b, e, m)) +PY +) + case "$shared_line" in + SHARED\ *) + ok "stats shared feed" "${shared_line#SHARED }" + ;; + DRIFT\ *) + warn "stats shared feed" "timestamps differ · ${shared_line#DRIFT }" + ;; + INDEPENDENT\ *) + info "stats feeds" "independent collectors · ${shared_line#INDEPENDENT }" + ;; + *) + info "stats feeds" "${shared_line:-n/a}" + ;; + esac + fi + info "stats note" "public GET /intro/stats.json; measure age via generated_at_unix vs wall clock" fi # Rollup + optional JSON for metrics_print_overall if [ -s "$PERF_TSV" ]; then From e46aeb408af803cbf08ee295f6948f8ab647dc16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Fri, 17 Jul 2026 17:03:37 +0200 Subject: [PATCH 8/8] monitoring: soft-skip GOA-only landing assets off-GOA --- scripts/taler-monitoring/TESTS.md | 4 +- scripts/taler-monitoring/check_urls.sh | 55 +++++++++++++++++++------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/scripts/taler-monitoring/TESTS.md b/scripts/taler-monitoring/TESTS.md index d56d3c4..022de8b 100644 --- a/scripts/taler-monitoring/TESTS.md +++ b/scripts/taler-monitoring/TESTS.md @@ -45,10 +45,10 @@ Numbering follows **executed** checks (early skip may shift later NN inside the | **www.exchange-** | `/config`, currency, **alt_unit_names**; `/keys` (+ alt soft); `/intro/`, `/`; **`/terms`**, **`/privacy`**, `/terms/` | | **www.perf-** | outside-in RTT: bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) | | **www.stats-** | `/intro/stats.json` **reachable**; **timestamps** (`generated_at_unix` + human/ISO); **freshness** vs wall clock (`STATS_STALE_SECS` WARN, `STATS_FAIL_SECS` ERROR); **display fields** (bank-shape withdraws/accounts, or exchange-db / merchant-db keys); optional performance block; shared-feed equal `gen_unix` when bank collector is published to all three | -| **www.bank-** | `/config`, currency, alt_unit_names; integration/webui/intro; **auto-account.json**; `/terms`, `/privacy` | +| **www.bank-** | `/config`, currency, alt_unit_names; integration/webui/intro; **auto-account.json** (required GOA/local; skip/soft off-GOA e.g. TESTPAYSAN); `/terms`, `/privacy` | | **www.merchant-** | `/config` currency + currencies alt_unit_names; listed exchanges alt; webui/intro; **`/terms`**, **`/privacy`** | | **www.paivana-** | local GOA paywall front (redirect to template) | -| **www.landing-** | every own-stack link on bank/merchant/exchange intros; static assets; demo-withdraw / cross-links | +| **www.landing-** | own-stack intro links; static assets (`qrcode.min.js` hard; `og-goa-shop.png` hard only GOA/local); **demo-withdraw.json** GOA-only; shop-pay soft; cross-links local | **Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. Local stack may require content needle (terms/privacy/FADP/GOA…). diff --git a/scripts/taler-monitoring/check_urls.sh b/scripts/taler-monitoring/check_urls.sh index ac0c4dc..55ed1b8 100755 --- a/scripts/taler-monitoring/check_urls.sh +++ b/scripts/taler-monitoring/check_urls.sh @@ -705,7 +705,7 @@ if [ "$code" = "200" ]; then check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/" check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/" check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/" - # Auto-account: credentials + shared-pool taler://withdraw (like step 2) + # Auto-account: GOA shared-pool mint (hacktivism). Not used on TESTPAYSAN stage. aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json") case "$aa_code" in 200) @@ -715,11 +715,22 @@ if [ "$code" = "200" ]; then fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))" fi ;; - 405|501|404|502|503|000) - fail "bank /intro/auto-account.json" "HTTP $aa_code (want 200; 405/501 = broken)" + 404|405|501) + if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then + fail "bank /intro/auto-account.json" "HTTP $aa_code (want 200; 405/501 = broken)" + else + info "bank /intro/auto-account.json" "HTTP $aa_code — skip (not a GOA auto-account stack)" + fi + ;; + 502|503|000) + fail "bank /intro/auto-account.json" "HTTP $aa_code want 200" ;; *) - fail "bank /intro/auto-account.json" "HTTP $aa_code want 200" + if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then + fail "bank /intro/auto-account.json" "HTTP $aa_code want 200" + else + warn "bank /intro/auto-account.json" "HTTP $aa_code (optional off-GOA)" + fi ;; esac fi @@ -1003,18 +1014,24 @@ check_one_landing() { return fi - # Required static assets (hard: qrcode + og; soft: qr-logo) - for url in \ - "${base}/intro/qrcode.min.js" \ - "${base}/intro/og-goa-shop.png" - do - if _landing_probe "$url"; then - a_ok=$((a_ok + 1)) - else + # Required static: qrcode.min.js. GOA og image is soft off-GOA (stage has no og-goa-shop.png). + if _landing_probe "${base}/intro/qrcode.min.js"; then + a_ok=$((a_ok + 1)) + else + a_fail=$((a_fail + 1)) + fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/qrcode.min.js" + fi + if _landing_probe "${base}/intro/og-goa-shop.png"; then + a_ok=$((a_ok + 1)) + else + if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then a_fail=$((a_fail + 1)) - fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} $url" + fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/og-goa-shop.png" + else + a_soft=$((a_soft + 1)) + soft_sample="${soft_sample}${soft_sample:+; }HTTP ${_landing_code} og-goa-shop.png (GOA asset)" fi - done + fi if _landing_probe "${base}/intro/qr-logo.png"; then a_ok=$((a_ok + 1)) else @@ -1142,7 +1159,15 @@ if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw ($(tr '\n' ' ' <"$tmp/dw-val" | sed 's/[[:space:]]*$//'))" fi ;; - 405|501|404|502|503|000) + 404|405|501) + # GOA bank landing mints demo-withdraw.json; stage TESTPAYSAN uses ATM/wallet guide only + if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then + fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)" + else + info "landing bank demo-withdraw" "HTTP $dw_code — skip (not a GOA demo-withdraw stack)" + fi + ;; + 502|503|000) fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)" ;; *)