Files
egorin/deploy/ingress-watchdog.sh
T
GermanandClaude Opus 4.8 b1c52ab748 Железный Егорин — форк Орбиты: тёплый стиль (amber), логин с odyssey WebGL-молнией справа, энергомониторинг+панель
Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент.
Доступ агентам: репо публичный, push german-токеном.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 23:57:16 +03:00

154 lines
6.3 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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