All checks were successful
CI / changes (push) Successful in 13s
CI / changes (pull_request) Successful in 12s
CI / frontend-tests (push) Successful in 53s
CI / openapi-codegen-check (push) Successful in 1m58s
CI / frontend-tests (pull_request) Successful in 47s
CI / openapi-codegen-check (pull_request) Successful in 1m50s
CI / backend-tests (push) Successful in 8m54s
CI / backend-tests (pull_request) Successful in 8m45s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 2m50s
Deploy / build-frontend (push) Successful in 3m11s
Deploy / build-worker (push) Successful in 4m38s
Deploy / deploy (push) Successful in 2m1s
#68: pytest-cov + [tool.coverage] fail_under=65 (baseline 71%), backend-tests --cov + xml. #69: openapi-codegen-check job (dump app.openapi() → openapi-typescript → git diff). Job использует node_modules-pinned openapi-typescript/prettier (НЕ npx --yes latest — иначе version-mismatch ложный diff). + regenerated api-types.ts из СВЕЖЕГО main openapi (8288 строк, включая #73 /freshness — синхронен). #75: Uptime Kuma isolated stack + status.gendsgn.ru Caddy + external Telegram watchdog. #77: no change — build cache достаточен. Closes #68 Closes #69 Closes #75 Closes #77
142 lines
5.4 KiB
Bash
Executable file
142 lines
5.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# External uptime watchdog for gendesign (#75 B6-1, lightweight fallback).
|
|
#
|
|
# WHY THIS EXISTS alongside Uptime Kuma (docker-compose.uptime.yml): Kuma runs
|
|
# ON the prod VM, so if the whole VM dies it can't alert. This script is meant to
|
|
# run from cron on a DIFFERENT host (your laptop, a tiny free-tier box, Beget
|
|
# shared-host cron) and hit the PUBLIC URLs over the internet — last-resort
|
|
# "весь хост лёг" detection. Kuma covers rich per-endpoint/SSL/latency monitoring;
|
|
# this covers the case Kuma structurally can't.
|
|
#
|
|
# Self-contained: only needs `curl` + `bash`. No docker, no repo checkout.
|
|
# State (last-known status per check) lives in a file so we alert on TRANSITIONS
|
|
# (up→down, down→up) — not every run — to avoid Telegram spam.
|
|
#
|
|
# Usage (cron — note `bash`, not a bare path, so a missing +x bit can't break it):
|
|
# * * * * * bash /path/to/uptime-healthcheck.sh >> /var/log/gendesign-uptime.log 2>&1
|
|
#
|
|
# Telegram alerting — set these in an env file (NOT in git, chmod 600):
|
|
# TELEGRAM_BOT_TOKEN=123456:ABC...
|
|
# TELEGRAM_CHAT_ID=123456789
|
|
# Default env path: /etc/default/gendesign-uptime (override via UPTIME_ENV_FILE).
|
|
# A redacted template lives at ops/gendesign-uptime.default.example.
|
|
# Without a token set, the script still logs up/down but sends no alert.
|
|
|
|
set -euo pipefail
|
|
|
|
# --- config (env-overridable) ---
|
|
UPTIME_ENV_FILE="${UPTIME_ENV_FILE:-/etc/default/gendesign-uptime}"
|
|
# shellcheck source=/dev/null
|
|
[[ -f "$UPTIME_ENV_FILE" ]] && source "$UPTIME_ENV_FILE"
|
|
|
|
BASE_URL="${BASE_URL:-https://gendsgn.ru}"
|
|
STATE_FILE="${STATE_FILE:-/var/tmp/gendesign-uptime-state}"
|
|
CURL_TIMEOUT="${CURL_TIMEOUT:-15}" # seconds per request (connect+read)
|
|
RETRIES="${RETRIES:-2}" # extra attempts before declaring DOWN
|
|
RETRY_SLEEP="${RETRY_SLEEP:-5}" # seconds between attempts
|
|
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
|
|
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}"
|
|
|
|
# Checks to probe. Format per line: "<label>|<path>|<expected_http_status>".
|
|
# Override the whole list via CHECKS env (same newline-separated format).
|
|
#
|
|
# DEFAULT = /health ONLY. WHY: this watchdog is UNAUTHENTICATED, and in the
|
|
# Caddyfile only /health (and /preview/*) are public — ALL /api/* sits behind
|
|
# the Basic-Auth gate (import caddy/users.caddy.snippet) and returns 401 to an
|
|
# anonymous client. So the issue's /api/v1/analytics/market-pulse and
|
|
# /parcels/{cad}/analyze monitors CANNOT be probed anonymously — they belong in
|
|
# Uptime Kuma, which can attach the Basic-Auth header (see README "Kuma monitors"
|
|
# in docker-compose.uptime.yml). Probing them here would false-alarm forever.
|
|
CHECKS="${CHECKS:-health|/health|200}"
|
|
|
|
log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*"; }
|
|
|
|
# --- telegram (no-op if token/chat unset) ---
|
|
notify() {
|
|
local text="$1"
|
|
if [[ -z "$TELEGRAM_BOT_TOKEN" || -z "$TELEGRAM_CHAT_ID" ]]; then
|
|
log "NOTIFY (telegram disabled — no token/chat): $text"
|
|
return 0
|
|
fi
|
|
curl -fsS --max-time "$CURL_TIMEOUT" \
|
|
-X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
|
-d "chat_id=${TELEGRAM_CHAT_ID}" \
|
|
-d "disable_web_page_preview=true" \
|
|
--data-urlencode "text=${text}" \
|
|
>/dev/null 2>&1 \
|
|
|| log "WARN: telegram sendMessage failed"
|
|
}
|
|
|
|
# --- state helpers (last status per check) ---
|
|
prev_status() {
|
|
local label="$1"
|
|
[[ -f "$STATE_FILE" ]] || { echo "unknown"; return; }
|
|
# Line format: "<label> <status>". grep the latest for this label.
|
|
local v
|
|
v="$(grep -E "^${label} " "$STATE_FILE" 2>/dev/null | tail -1 | awk '{print $2}')"
|
|
echo "${v:-unknown}"
|
|
}
|
|
|
|
set_status() {
|
|
local label="$1" status="$2" tmp
|
|
tmp="$(mktemp)"
|
|
# Drop any prior line for this label, then append the fresh one.
|
|
if [[ -f "$STATE_FILE" ]]; then
|
|
grep -vE "^${label} " "$STATE_FILE" > "$tmp" 2>/dev/null || true
|
|
fi
|
|
echo "${label} ${status}" >> "$tmp"
|
|
mv "$tmp" "$STATE_FILE"
|
|
}
|
|
|
|
# --- probe one URL with retries; echoes "up" or "down code=NNN" ---
|
|
probe() {
|
|
local url="$1" expect="$2" attempt code
|
|
# NB: NO `-f` here. `-w %{http_code}` always prints exactly a 3-digit status
|
|
# (or 000 on connect/timeout failure), so we judge by the code ourselves and
|
|
# never need a `|| echo` fallback (which previously concatenated → "401000").
|
|
for attempt in $(seq 1 "$((RETRIES + 1))"); do
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time "$CURL_TIMEOUT" "$url" 2>/dev/null)"
|
|
code="${code:-000}"
|
|
if [[ "$code" == "$expect" ]]; then
|
|
echo "up"
|
|
return 0
|
|
fi
|
|
[[ "$attempt" -le "$RETRIES" ]] && sleep "$RETRY_SLEEP"
|
|
done
|
|
# Report the last code seen for the alert body.
|
|
echo "down code=${code}"
|
|
return 0
|
|
}
|
|
|
|
# --- run ---
|
|
overall_rc=0
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
label="${line%%|*}"
|
|
rest="${line#*|}"
|
|
path="${rest%%|*}"
|
|
expect="${rest##*|}"
|
|
url="${BASE_URL}${path}"
|
|
|
|
result="$(probe "$url" "$expect")"
|
|
now="up"
|
|
[[ "$result" == up ]] || now="down"
|
|
prev="$(prev_status "$label")"
|
|
|
|
if [[ "$now" == "up" ]]; then
|
|
log "OK $label ($url)"
|
|
if [[ "$prev" == "down" ]]; then
|
|
notify "✅ RECOVERED: gendesign $label is back UP ($url)"
|
|
fi
|
|
else
|
|
overall_rc=1
|
|
log "DOWN $label ($url) — $result"
|
|
# Alert on transition into down (or first-ever observation that is down).
|
|
if [[ "$prev" != "down" ]]; then
|
|
notify "🔴 DOWN: gendesign $label — ${url} (${result#down }). $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
fi
|
|
fi
|
|
set_status "$label" "$now"
|
|
done <<< "$CHECKS"
|
|
|
|
exit "$overall_rc"
|