diff --git a/tools/liderra-monitoring/.gitattributes b/tools/liderra-monitoring/.gitattributes new file mode 100644 index 00000000..40dc7990 --- /dev/null +++ b/tools/liderra-monitoring/.gitattributes @@ -0,0 +1,6 @@ +# Эти файлы заливаются на Linux-сервер через scp. +# CRLF здесь = повтор инцидента 22.05.2026 (битый .env). +* text eol=lf +*.sh text eol=lf +*.service text eol=lf +*.template text eol=lf diff --git a/tools/liderra-monitoring/liderra-healthcheck.sh b/tools/liderra-monitoring/liderra-healthcheck.sh new file mode 100644 index 00000000..09549788 --- /dev/null +++ b/tools/liderra-monitoring/liderra-healthcheck.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# liderra-healthcheck.sh — проверка здоровья портала + email-алёрт на kdv1@bk.ru. +# Cron: каждые 2 минуты. +# Логика: 2 подряд провала (~4 мин даунтайма) → DOWN-алёрт; первый успех после DOWN → UP-алёрт. +set -u + +ALERT_TO="kdv1@bk.ru" +URL="https://127.0.0.1/" +HOST_HDR="liderra.ru" +STATE_DIR="/var/lib/liderra" +STATE_FILE="$STATE_DIR/healthcheck.state" +LOG="/var/log/liderra-healthcheck.log" + +DOWN_THRESHOLD=2 # подряд провалов до DOWN-алёрта +TIMEOUT=10 + +mkdir -p "$STATE_DIR" 2>/dev/null || true +touch "$STATE_FILE" "$LOG" 2>/dev/null || true + +# Текущее состояние: DOWN_COUNT и LAST_STATE (up|down) +DOWN_COUNT=$(awk -F= '/^down_count=/{print $2}' "$STATE_FILE" 2>/dev/null) +DOWN_COUNT="${DOWN_COUNT:-0}" +LAST_STATE=$(awk -F= '/^last_state=/{print $2}' "$STATE_FILE" 2>/dev/null) +LAST_STATE="${LAST_STATE:-up}" + +HTTP_CODE=$(curl -sS -k --max-time "$TIMEOUT" -H "Host: $HOST_HDR" -o /dev/null -w "%{http_code}" "$URL" 2>/dev/null || echo "000") +NOW=$(date '+%Y-%m-%d %H:%M:%S') + +write_state() { + cat > "$STATE_FILE" <> "$LOG" 2>&1 +} + +# Считаем 5xx, 000 (timeout/refused) и любые сетевые ошибки как «провал». +# 200/301/302/401/403 — портал отвечает (даже 401 — это «жив»). +if [[ "$HTTP_CODE" =~ ^[23] ]] || [[ "$HTTP_CODE" =~ ^(301|302|401|403)$ ]]; then + STATE="up" +else + STATE="down" +fi + +if [[ "$STATE" == "down" ]]; then + DOWN_COUNT=$((DOWN_COUNT + 1)) + echo "[$NOW] DOWN code=$HTTP_CODE count=$DOWN_COUNT" >> "$LOG" + if [[ "$DOWN_COUNT" -ge "$DOWN_THRESHOLD" ]] && [[ "$LAST_STATE" == "up" ]]; then + TAIL_ERR=$(grep -E 'production\.ERROR' /var/www/liderra/app/storage/logs/laravel.log 2>/dev/null | tail -3 | cut -c1-400) + NGX=$(tail -5 /var/log/nginx/error.log 2>/dev/null) + send_mail "[Лидерра ПАДЕНИЕ] liderra.ru недоступен — HTTP $HTTP_CODE" "Портал liderra.ru недоступен. + +Время: $NOW +HTTP-код: $HTTP_CODE +Провалов подряд: $DOWN_COUNT (порог $DOWN_THRESHOLD) + +Последние ошибки Laravel: +$TAIL_ERR + +Последние строки nginx error.log: +$NGX + +Сервер: ssh ubuntu@111.88.246.137 +" + write_state "$DOWN_COUNT" "down" + else + write_state "$DOWN_COUNT" "$LAST_STATE" + fi +else + if [[ "$LAST_STATE" == "down" ]]; then + echo "[$NOW] RECOVERED code=$HTTP_CODE" >> "$LOG" + send_mail "[Лидерра восстановлен] liderra.ru снова работает — HTTP $HTTP_CODE" "Портал liderra.ru снова отвечает. + +Время восстановления: $NOW +HTTP-код: $HTTP_CODE +" + fi + write_state 0 "up" +fi diff --git a/tools/liderra-monitoring/liderra-precheck.sh b/tools/liderra-monitoring/liderra-precheck.sh new file mode 100644 index 00000000..9a129c22 --- /dev/null +++ b/tools/liderra-monitoring/liderra-precheck.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# liderra-precheck.sh — pre-flight гейт перед/после деплоя. +# Проверяет .env, ключи, БД, Redis, шифрование. exit 1 при любом провале. +# Запускать руками после scp файлов / git pull, до systemctl restart. +set -e + +APP_DIR="/var/www/liderra/app" +ENV="$APP_DIR/.env" +FAIL=0 + +red() { printf '\033[31m✗ %s\033[0m\n' "$1"; FAIL=1; } +green() { printf '\033[32m✓ %s\033[0m\n' "$1"; } +yellow(){ printf '\033[33m! %s\033[0m\n' "$1"; } + +echo "=== liderra pre-flight check ===" + +# 1. .env existence + perms +if [[ ! -f "$ENV" ]]; then red "$ENV отсутствует"; exit 1; fi +if [[ "$(stat -c '%U' "$ENV")" != "www-data" ]]; then yellow ".env owner $(stat -c '%U:%G %a' "$ENV") — должен быть www-data:www-data 640"; fi +green ".env существует" + +# 2. CRLF check +CRLF=$(grep -c $'\r' "$ENV" || true) +if [[ "$CRLF" -gt 0 ]]; then red ".env содержит $CRLF строк с CRLF — Laravel сломает значения. Запусти: sudo sed -i 's/\\r\$//' $ENV"; else green ".env без CRLF"; fi + +# 3. APP_KEY +KEY=$(grep '^APP_KEY=' "$ENV" | head -1 | cut -d= -f2-) +if [[ -z "$KEY" ]]; then red "APP_KEY пустой"; +elif [[ "$KEY" != base64:* ]]; then red "APP_KEY без префикса base64: → '$KEY'"; +elif [[ "${#KEY}" -ne 51 ]]; then red "APP_KEY длина ${#KEY}, должна быть 51 (base64: + 44 символа base64)"; +else green "APP_KEY корректный (${#KEY} символов)"; fi + +# 4. Дубль APP_KEY +DUP=$(grep -c '^APP_KEY=' "$ENV" || true) +if [[ "$DUP" -gt 1 ]]; then red "В .env $DUP строк APP_KEY= — должна быть одна"; fi + +# 5. Critical drivers +for VAR in APP_ENV DB_CONNECTION SESSION_DRIVER CACHE_STORE QUEUE_CONNECTION REDIS_HOST; do + V=$(grep "^${VAR}=" "$ENV" | head -1 | cut -d= -f2-) + if [[ -z "$V" ]]; then yellow "$VAR не задан (берётся дефолт)"; else green "$VAR=$V"; fi +done + +# 6. PostgreSQL connection +cd "$APP_DIR" +if sudo -u www-data php -r "require 'vendor/autoload.php'; \$app=require 'bootstrap/app.php'; \$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); try { DB::select('SELECT 1'); echo 'PG_OK'; } catch (Throwable \$e) { echo 'PG_FAIL: '.\$e->getMessage(); }" 2>/dev/null | grep -q "PG_OK"; then + green "PostgreSQL доступен" +else + red "PostgreSQL недоступен — проверь DB_HOST/DB_PASSWORD" +fi + +# 7. Redis ping +if sudo -u www-data php -r "require 'vendor/autoload.php'; \$app=require 'bootstrap/app.php'; \$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); try { \$r=Illuminate\Support\Facades\Redis::ping(); echo strpos(strtolower((string)\$r ?: 'pong'),'pong')!==false || \$r===true ? 'REDIS_OK' : 'REDIS_FAIL'; } catch (Throwable \$e) { echo 'REDIS_FAIL: '.\$e->getMessage(); }" 2>/dev/null | grep -q "REDIS_OK"; then + green "Redis доступен" +else + red "Redis недоступен — проверь REDIS_HOST" +fi + +# 8. Encryption round-trip (главная защита от инцидента 22.05.2026) +if [[ "$(sudo -u www-data php artisan tinker --execute "echo decrypt(encrypt('ping'));" 2>/dev/null | tail -1)" == "ping" ]]; then + green "Шифрование работает (decrypt(encrypt) round-trip)" +else + red "Шифрование сломано — APP_KEY невалидный, портал упадёт на 500" +fi + +# 9. config-cache не stale +if [[ -f "$APP_DIR/bootstrap/cache/config.php" ]]; then + CACHE_AGE=$(( $(date +%s) - $(stat -c %Y "$APP_DIR/bootstrap/cache/config.php") )) + ENV_AGE=$(( $(date +%s) - $(stat -c %Y "$ENV") )) + if [[ "$ENV_AGE" -lt "$CACHE_AGE" ]]; then + red "config.php кэш СТАРШЕ чем .env — запусти: sudo -u www-data php artisan config:cache" + else + green "config-кэш свежее .env" + fi +else + yellow "config-кэш отсутствует (норма для dev, но в prod лучше иметь)" +fi + +# 10. Pending migrations +if cd "$APP_DIR" && sudo -u www-data php artisan migrate:status 2>/dev/null | grep -q "Pending"; then + yellow "Есть pending миграции — запусти: sudo -u www-data php artisan migrate --force" +else + green "Миграции применены" +fi + +# 11. HTTP smoke +HTTP=$(curl -sS -k --max-time 5 -H "Host: liderra.ru" -o /dev/null -w "%{http_code}" https://127.0.0.1/ 2>/dev/null || echo "000") +case "$HTTP" in + 200|301|302|401|403) green "HTTP smoke: $HTTP" ;; + *) red "HTTP smoke: $HTTP — портал не отвечает" ;; +esac + +echo +if [[ "$FAIL" -eq 0 ]]; then + green "ВСЁ ОК — можно перезапускать сервисы (sudo systemctl reload php8.3-fpm; sudo systemctl restart liderra-queue)" + exit 0 +else + red "ЕСТЬ ПРОВАЛЫ — НЕ ЗАПУСКАЙ systemctl restart, сначала почини выше" + exit 1 +fi diff --git a/tools/liderra-monitoring/liderra-queue-alert.service b/tools/liderra-monitoring/liderra-queue-alert.service new file mode 100644 index 00000000..dde62a49 --- /dev/null +++ b/tools/liderra-monitoring/liderra-queue-alert.service @@ -0,0 +1,6 @@ +[Unit] +Description=Liderra queue alert (sends email on liderra-queue failure) + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/liderra-systemd-alert.sh liderra-queue diff --git a/tools/liderra-monitoring/liderra-queue.service b/tools/liderra-monitoring/liderra-queue.service new file mode 100644 index 00000000..49014566 --- /dev/null +++ b/tools/liderra-monitoring/liderra-queue.service @@ -0,0 +1,19 @@ +[Unit] +Description=Liderra queue worker +After=redis-server.service postgresql.service network.target +# Перейти в failed после 5 крашей за 5 минут — иначе systemd крутит бесконечно +StartLimitIntervalSec=300 +StartLimitBurst=5 +# При окончательном fail — запустить алёрт-юнит +OnFailure=liderra-queue-alert.service + +[Service] +User=www-data +Group=www-data +Restart=on-failure +RestartSec=10 +WorkingDirectory=/var/www/liderra/app +ExecStart=/usr/bin/php /var/www/liderra/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 + +[Install] +WantedBy=multi-user.target diff --git a/tools/liderra-monitoring/liderra-systemd-alert.sh b/tools/liderra-monitoring/liderra-systemd-alert.sh new file mode 100644 index 00000000..404fb9a2 --- /dev/null +++ b/tools/liderra-monitoring/liderra-systemd-alert.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# liderra-systemd-alert.sh — отправка email-алёрта при упавшем systemd-юните. +# Используется как OnFailure= для liderra-queue.service. +set -u + +UNIT="${1:-unknown}" +ALERT_TO="kdv1@bk.ru" +NOW=$(date '+%Y-%m-%d %H:%M:%S') + +STATUS=$(systemctl status "$UNIT" --no-pager -l 2>&1 | head -30) +JOURNAL=$(journalctl -u "$UNIT" -n 30 --no-pager 2>&1) + +{ + printf 'Subject: [Лидерра-мониторинг] systemd-юнит упал: %s\n' "$UNIT" + printf 'From: verify@liderra.ru\n' + printf 'To: %s\n' "$ALERT_TO" + printf 'MIME-Version: 1.0\n' + printf 'Content-Type: text/plain; charset=utf-8\n\n' + printf 'Юнит %s окончательно упал (5 крашей за 5 минут — превышен лимит).\n\n' "$UNIT" + printf 'Время: %s\n' "$NOW" + printf 'Сервер: ssh ubuntu@111.88.246.137\n\n' + printf '=== systemctl status ===\n%s\n\n' "$STATUS" + printf '=== journalctl (последние 30 строк) ===\n%s\n' "$JOURNAL" +} | msmtp -a yandex "$ALERT_TO" 2>>/var/log/msmtp.log diff --git a/tools/liderra-monitoring/msmtprc.template b/tools/liderra-monitoring/msmtprc.template new file mode 100644 index 00000000..886547dc --- /dev/null +++ b/tools/liderra-monitoring/msmtprc.template @@ -0,0 +1,15 @@ +defaults +auth on +tls on +tls_starttls off +tls_trust_file /etc/ssl/certs/ca-certificates.crt +logfile /var/log/msmtp.log + +account yandex +host smtp.yandex.ru +port 465 +from verify@liderra.ru +user verify@liderra.ru +password __MAIL_PASSWORD__ + +account default : yandex