#!/usr/bin/env bash # ingress-watchdog.sh — алерт обрыва связи ингриса с MQTT-брокером (Milestone B) # stdlib/bash only. Аддитивно. НЕ трогает приёмники/буферизацию/маршруты. # # Проба: TCP-connect к брокеру vm-mts1 10.50.0.48:1883 (таймаут 5с). # Дебаунс: DOWN только после непрерывной недоступности > DOWN_THRESHOLD (60с). # Алерт ТОЛЬКО на переходе состояния (up->down = ОБРЫВ, down->up = ВОССТАНОВЛЕНО). # Каналы: всегда лог + статус-файл; Telegram — если в .env есть TG_TOKEN и TG_CHAT_ID. set -u # --- Конфиг (можно переопределить через .env) --- BROKER_HOST="10.50.0.48" BROKER_PORT="1883" DOWN_THRESHOLD=60 # сек непрерывной недоступности до алерта ОБРЫВ PROBE_TIMEOUT=5 # сек на TCP-пробу STATE_FILE="/var/run/ingress-watchdog.state" STATUS_FILE="/run/ingress-watchdog.status" LOG_FILE="/var/log/ingress-watchdog.log" ENV_FILE="/opt/ingress-watchdog/.env" NODE_NAME="$(hostname)" SPOOL_FILE="" # путь к queue.jsonl (задаётся в .env на каждой ноде) ALERT_WEBHOOK="https://kpgen.servaki.online/webhook/alert" # единый n8n-хаб (можно переопределить в .env) # --- Загрузка .env (TG_TOKEN, TG_CHAT_ID, SPOOL_FILE, NODE_NAME override) --- if [ -f "$ENV_FILE" ]; then # shellcheck disable=SC1090 . "$ENV_FILE" fi now="$(date +%s)" ts="$(date '+%Y-%m-%d %H:%M:%S %Z')" log() { # НЕ логируем токен. Пишем строку в лог. printf '%s [%s] %s\n' "$ts" "$NODE_NAME" "$1" >> "$LOG_FILE" 2>/dev/null } spool_count() { if [ -n "${SPOOL_FILE:-}" ] && [ -f "$SPOOL_FILE" ]; then wc -l < "$SPOOL_FILE" 2>/dev/null | tr -d ' ' else echo 0 fi } send_telegram() { # $1 = текст. Отправляем только если заданы token+chat_id. local text="$1" if [ -n "${TG_TOKEN:-}" ] && [ -n "${TG_CHAT_ID:-}" ]; then timeout 10 curl -s -o /dev/null \ "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \ --data-urlencode "chat_id=${TG_CHAT_ID}" \ --data-urlencode "text=${text}" >/dev/null 2>&1 \ && log "telegram: sent" \ || log "telegram: send FAILED" else log "telegram: skipped (no token/chat_id)" fi } send_webhook() { # $1=severity (critical|warning|info), $2=title, $3=text. Единый хаб алертов (n8n). local sev="$1" title="$2" text="$3" iso body [ -z "${ALERT_WEBHOOK:-}" ] && { log "webhook: skipped (no ALERT_WEBHOOK)"; return; } iso="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" body="$(printf '{"source":"orbita-ingress","node":"%s","severity":"%s","title":"%s","text":"%s","ts":"%s"}' \ "$NODE_NAME" "$sev" "$title" "$text" "$iso")" local code code="$(timeout 12 curl -s -o /dev/null -m 10 --retry 2 -w '%{http_code}' \ -X POST "$ALERT_WEBHOOK" -H 'Content-Type: application/json' -d "$body" 2>/dev/null)" if [ "$code" = "200" ]; then log "webhook: sent ($sev) HTTP 200"; else log "webhook: send FAILED (HTTP ${code:-000})"; fi } write_status() { # $1=state up|down, $2=human message { printf 'node=%s\n' "$NODE_NAME" printf 'state=%s\n' "$1" printf 'updated=%s\n' "$ts" printf 'updated_epoch=%s\n' "$now" printf 'broker=%s:%s\n' "$BROKER_HOST" "$BROKER_PORT" printf 'spool_lines=%s\n' "$(spool_count)" printf 'last_message=%s\n' "$2" } > "$STATUS_FILE" 2>/dev/null } # --- Читаем предыдущий стейт --- last_ok=0 current_state="up" down_since=0 if [ -f "$STATE_FILE" ]; then # shellcheck disable=SC1090 . "$STATE_FILE" 2>/dev/null || true fi # --- TCP-проба --- if timeout "$PROBE_TIMEOUT" bash -c "cat < /dev/null > /dev/tcp/${BROKER_HOST}/${BROKER_PORT}" 2>/dev/null; then probe_ok=1 else probe_ok=0 fi new_state="$current_state" if [ "$probe_ok" -eq 1 ]; then last_ok="$now" if [ "$current_state" = "down" ]; then # ВОССТАНОВЛЕНО (переход down->up) new_state="up" down_since=0 spc="$(spool_count)" msg="✅ ВОССТАНОВЛЕНО: связь ингриса ${NODE_NAME} с брокером ${BROKER_HOST}:${BROKER_PORT} восстановлена. В спуле сейчас: ${spc} сообщ. (дренаж до-дошлёт). Время: ${ts}" log "STATE up (recovered). spool=${spc}" write_status "up" "recovered" send_webhook "info" "Связь восстановлена ${NODE_NAME}" "брокер ${BROKER_HOST}:${BROKER_PORT} снова доступен; в буфере ${spc} сообщ. (дренаж до-дошлёт)" send_telegram "$msg" else # остаёмся up, без алерта write_status "up" "ok" fi else # недоступно if [ "$down_since" -eq 0 ] 2>/dev/null || [ -z "${down_since:-}" ]; then down_since="$now" fi down_for=$(( now - down_since )) if [ "$current_state" = "up" ] && [ "$down_for" -gt "$DOWN_THRESHOLD" ]; then # ОБРЫВ (переход up->down после дебаунса) new_state="down" spc="$(spool_count)" msg="🔴 ОБРЫВ: ингрис ${NODE_NAME} потерял связь с брокером ${BROKER_HOST}:${BROKER_PORT} (недоступен ${down_for}с). В спуле накоплено: ${spc} сообщ. и буферизуется. Время: ${ts}" log "STATE down (link lost ${down_for}s). spool=${spc}" write_status "down" "link_lost" send_webhook "critical" "Обрыв связи ${NODE_NAME}" "брокер ${BROKER_HOST}:${BROKER_PORT} недоступен ${down_for}с; в буфере ${spc} сообщ., буферизуется" send_telegram "$msg" elif [ "$current_state" = "down" ]; then # уже в down — не спамим write_status "down" "still_down" else # недоступно, но порог ещё не перевален — ждём write_status "up" "probing_down ${down_for}s" fi fi # --- Сохраняем стейт --- { printf 'last_ok=%s\n' "$last_ok" printf 'current_state=%s\n' "$new_state" printf 'down_since=%s\n' "$down_since" } > "$STATE_FILE" 2>/dev/null exit 0