- Shell 97.7%
- Dockerfile 2.3%
| docker-compose.yml | ||
| Dockerfile | ||
| hosts | ||
| pingmon.env | ||
| pingmon.service | ||
| pingmon.sh | ||
| README.md | ||
pingmon
pingmon — небольшой Bash-демон для мониторинга ICMP-доступности пула серверов с уведомлениями в Telegram.
Поддерживаются два режима:
- одиночный монитор — один экземпляр опрашивает весь пул;
- реплицированный мониторинг — несколько экземпляров независимо опрашивают пул, но обычные уведомления отправляет только активная реплика.
Проект не требует БД, внешнего координатора или общего сетевого хранилища. Состояние каждого экземпляра хранится локально.
Возможности
- параллельный
pingвсех хостов; - гистерезис переходов
UP/DOWNдля подавления кратковременных сбоев; - одно сообщение на все переходы, подтверждённые за цикл;
- сообщение о восстановлении с длительностью простоя;
- повтор отправки, если Telegram или все каналы доставки недоступны;
- перебор SOCKS5-прокси и прямого подключения;
- атомарное сохранение состояния через
tmp + rename; - перечитывание пула хостов без перезапуска;
- репликация со статическим приоритетом;
- снапшот текущих
DOWNпри перехвате роли; - запуск через systemd или Docker Compose.
Как это работает
Состояние хоста
Для каждого хоста хранятся два независимых значения:
state— подтверждённое состояние доступности:UPилиDOWN;notified— последнее состояние, отправка которого была подтверждена успешным завершениемcurl.
Если state != notified, активный монитор пытается отправить уведомление. После успешной отправки notified приравнивается к state.
Такое разделение нужно потому, что изменение доступности и доставка сообщения происходят неатомарно. Если Telegram временно недоступен, событие не теряется, а повторяется в следующих циклах.
Переходы подтверждаются последовательными проверками:
UP → DOWN после FAIL_THRESHOLD неудачных проверок подряд
DOWN → UP после OK_THRESHOLD успешных проверок подряд
Одна проверка выполняет ping -c PING_COUNT. Хост считается доступным, если получен хотя бы один ответ. Началом простоя считается первая неудачная проверка текущего сбоя, а не момент подтверждения DOWN.
Если хост успел упасть и восстановиться, пока Telegram был недоступен, устаревшие DOWN и UP не отправляются: уведомления описывают актуальное состояние, а не всю пропущенную историю.
Репликация
MONITORS задаёт порядок приоритета, а SELF — имя локального экземпляра:
MONITORS="mon-a mon-b"
SELF="mon-b"
Каждая реплика:
- Самостоятельно пингует весь пул.
- Хранит собственный state-файл.
- Проверяет, есть ли доступный монитор выше по приоритету.
- Отправляет обычные события только в активной роли.
Пассивная реплика продолжает наблюдение, но при изменении state молча устанавливает notified=state. Поэтому при перехвате она уже имеет актуальную картину и не отправляет накопившуюся историю.
При переходе в активную роль реплика отправляет один снапшот:
- список всех подтверждённых
DOWN; - либо сообщение, что подтверждённых недоступных хостов нет.
Если отправка снапшота не удалась, он перестраивается и отправляется повторно в следующем цикле. Когда приоритетный монитор возвращается, нижестоящая реплика снова становится пассивной.
State-файлы реплик не нужно синхронизировать. Общий state-файл добавил бы отдельную сетевую зависимость и не устранил бы network partition.
Требования
Для systemd-установки:
- Linux;
- Bash 5 или новее;
curl;pingизiputils;ca-certificates;- systemd.
На Debian/Ubuntu зависимости устанавливаются командой:
sudo apt update
sudo apt install -y bash curl iputils-ping ca-certificates
Для Docker-установки:
- Docker Engine с Compose plugin;
- Linux-хост: в Compose используется
network_mode: host.
Дополнительно нужны:
- токен Telegram-бота;
- ID чата, канала или группы;
- ICMP-доступность наблюдаемых хостов;
- HTTPS-доступ к
api.telegram.orgнапрямую или через один из настроенных прокси.
Состав проекта
| Файл | Назначение |
|---|---|
pingmon.sh |
Основной демон |
pingmon.env |
Пример параметров и Telegram-конфигурации |
hosts |
Пример пула наблюдаемых хостов |
pingmon.service |
systemd unit |
Dockerfile |
Образ на Alpine с Bash, curl и iputils |
docker-compose.yml |
Запуск контейнера с host networking и постоянным состоянием |
README.md |
Документация |
Настройка
pingmon.env
Не используйте приведённые примеры токена, адресов и ID чата как рабочие значения. Не публикуйте заполненный pingmon.env в Git.
| Переменная | По умолчанию | Описание |
|---|---|---|
TG_TOKEN |
нет | Токен Telegram-бота; обязательна |
TG_CHAT_ID |
нет | ID чата для уведомлений; обязательна |
INTERVAL |
30 |
Период опроса в секундах |
PING_COUNT |
3 |
Число ICMP-пакетов на одну проверку |
PING_WAIT |
2 |
Ожидание ответа на пакет в секундах |
FAIL_THRESHOLD |
3 |
Неудачных проверок подряд до DOWN |
OK_THRESHOLD |
2 |
Успешных проверок подряд до UP |
TG_PROXIES |
direct |
Каналы доставки через пробел, перебираются слева направо |
MONITORS |
пусто | Имена мониторов по убыванию приоритета; пусто — одиночный режим |
SELF |
пусто | Имя локального монитора; при репликации должно входить в MONITORS |
HOSTS_FILE |
/etc/pingmon/hosts |
Путь к пулу хостов |
STATE_FILE |
/var/lib/pingmon/state |
Путь к локальному state-файлу |
Числовые параметры должны быть положительными целыми числами.
Пример без прокси:
TG_PROXIES=direct
Пример с локальным и резервным SOCKS5-прокси:
TG_PROXIES="socks5h://10.0.0.5:1080 socks5h://10.0.0.6:1080 direct"
direct означает прямое подключение с игнорированием proxy-переменных окружения. Рекомендуется socks5h://, а не socks5://: в первом случае DNS-имя api.telegram.org разрешается на стороне прокси.
Для каждого канала curl использует --connect-timeout 5 и --max-time 15.
hosts
Формат строки:
<имя> <IP-адрес-или-DNS-имя>
Пример:
# name address
mon-a 10.0.0.5
mon-b 10.0.0.6
gw 192.168.1.1
db-01 10.0.0.11
vpn-edge vpn.example.com
Правила:
- имя может содержать только
A–Z,a–z,0–9,.,_и-; - пустые строки и строки, начинающиеся с
#, игнорируются; - имена должны быть уникальными;
- в реплицированном режиме каждое имя из
MONITORSобязательно присутствует вhosts; - файл перечитывается каждый цикл, поэтому изменение пула не требует перезапуска;
- при дублировании имени или пустом новом пуле сохраняется предыдущий рабочий пул;
- если монитор отсутствует в новом пуле, цикл пропускается и записывается ошибка в журнал.
Одиночный режим
В pingmon.env:
MONITORS=
SELF=
TG_PROXIES=direct
В hosts укажите только наблюдаемые хосты. Примеры mon-a и mon-b можно удалить.
Одиночный режим не отправляет снапшот при запуске. Сообщения формируются только при подтверждённых переходах UP/DOWN.
Режим двух реплик
Пример логической схемы:
┌────────────────┐
│ Telegram │
└───────▲────────┘
│
┌─────────────┴─────────────┐
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ mon-a │ │ mon-b │
│ приоритет 1 │ │ приоритет 2 │
└──────┬──────┘ └──────┬──────┘
│ │
└────────── общий пул ──────┘
На обеих нодах hosts и порядок MONITORS должны быть одинаковыми:
MONITORS="mon-a mon-b"
На mon-a:
SELF=mon-a
TG_PROXIES="socks5h://10.0.0.5:1080 socks5h://10.0.0.6:1080 direct"
На mon-b:
SELF=mon-b
TG_PROXIES="socks5h://10.0.0.6:1080 socks5h://10.0.0.5:1080 direct"
Локальный прокси указан первым, прокси соседа — вторым. Если прокси не используются, оставьте TG_PROXIES=direct.
Нормальное поведение:
mon-aактивен,mon-bпассивен.- После подтверждённого
DOWNузлаmon-aузелmon-bперехватывает роль и отправляет снапшот. - После подтверждённого возвращения
mon-aузелmon-bуступает роль. - В реплицированном режиме реплика стартует пассивной; если она сразу становится активной, отправляется снапшот. Поэтому перезапуск текущей активной реплики может дать повторный снапшот.
Установка через systemd
systemd является предпочтительным вариантом для этой утилиты: мониторинг не зависит от Docker daemon, а Bash, curl и iputils не создают сложного окружения, ради которого обязательно нужен контейнер.
Сначала отредактируйте pingmon.env и hosts, затем из каталога проекта выполните:
sudo install -m755 pingmon.sh /usr/local/bin/pingmon.sh
sudo install -d -m755 /etc/pingmon
sudo install -m600 pingmon.env /etc/pingmon/pingmon.env
sudo install -m644 hosts /etc/pingmon/hosts
sudo install -m644 pingmon.service /etc/systemd/system/pingmon.service
sudo systemctl daemon-reload
sudo systemctl enable --now pingmon
Права на файлы различаются намеренно:
pingmon.envсодержит токен и должен быть0600;hostsне содержит секретов и должен быть доступен процессу сDynamicUser=yes, поэтому устанавливается с0644.
Проверка:
systemctl status pingmon --no-pager
journalctl -u pingmon -f
Unit-файл включает:
Restart=alwaysиRestartSec=5;DynamicUser=yes;StateDirectory=pingmon;AmbientCapabilities=CAP_NET_RAW;NoNewPrivileges=yes;ProtectSystem=strict,ProtectHome=yes,PrivateTmp=yes.
CAP_NET_RAW выдаётся явно, поскольку NoNewPrivileges=yes отключает получение file capabilities исполняемым файлом ping.
После изменения pingmon.env или pingmon.sh требуется перезапуск:
sudo systemctl restart pingmon
После изменения только hosts перезапуск не нужен.
Установка через Docker Compose
Отредактируйте pingmon.env и hosts, затем выполните:
docker compose up -d --build
docker compose ps
docker compose logs -f pingmon
Compose-конфигурация использует:
network_mode: host— проверки выполняются с точки зрения сети хоста, без Docker NAT;init: true— корректную обработку сигналов и завершение дочерних процессов;- volume
pingmon-state— сохранение состояния между пересозданиями контейнера; TZ=Europe/Samara— часовой пояс для времени в сообщениях; при необходимости измените значение.
Перезапуск после изменения env-файла:
docker compose up -d --force-recreate
После изменения только hosts пересоздание не требуется: файл смонтирован read-only и перечитывается каждый цикл.
Остановка без удаления состояния:
docker compose down
Не используйте docker compose down -v, если нужно сохранить состояние: флаг -v удаляет volume.
При репликации на каждом сервере должен быть собственный pingmon.env с правильным SELF. Именованные volumes разных серверов не синхронизируются — это ожидаемое поведение.
Эксплуатация
Журналы systemd
journalctl -u pingmon --since today
journalctl -u pingmon -f
Журналы Docker
docker compose logs --since 24h pingmon
docker compose logs -f pingmon
Основные сообщения:
| Сообщение | Значение |
|---|---|
started: ... |
Демон запущен с указанными параметрами |
<host>: DOWN |
Подтверждён переход в DOWN |
<host>: UP |
Подтверждён переход в UP |
became active |
Реплика перехватила отправку |
became passive |
Реплика уступила отправку приоритетному монитору |
telegram send failed ... |
Обычные события не отправлены; будет повтор |
telegram takeover snapshot failed ... |
Снапшот не отправлен; будет перестроен и повторён |
monitor ... is missing ... |
Ошибка конфигурации пула; цикл пропущен |
stopping |
Получен SIGTERM или SIGINT |
State-файл
По умолчанию состояние находится в /var/lib/pingmon/state. Каждая строка имеет формат:
<name> <state> <notified> <changed_epoch> <down_at_epoch>
Файл записывается атомарно. Он предназначен для диагностики, а не для редактирования во время работы демона.
Поведение при отказах
| Ситуация | Поведение |
|---|---|
| Наблюдаемый хост перестал отвечать | После гистерезиса активная реплика отправляет DOWN |
| Хост восстановился | После гистерезиса отправляется UP с длительностью простоя |
| Telegram временно недоступен | Событие остаётся pending и повторяется |
| Первый прокси недоступен | Перебираются следующие записи TG_PROXIES |
| Приоритетный монитор перестал отвечать на ICMP | Нижестоящая реплика перехватывает роль и отправляет снапшот |
| Приоритетный монитор вернулся | Нижестоящая реплика становится пассивной |
Процесс pingmon умер, но его хост отвечает на ping |
Другая реплика не перехватывает роль: она проверяет ICMP, а не heartbeat процесса |
| Реплики не видят друг друга, но обе видят Telegram | Возможны сообщения от обеих; префикс [SELF] помогает распознать partition |
Гарантии и ограничения
- Семантика доставки — at-least-once, не exactly-once. При таймауте запрос мог быть выполнен Telegram, но ответ мог потеряться. Повтор через тот же или другой прокси способен создать дубль.
- ICMP-доступность не равна работоспособности сервиса. Хост может отвечать на ping при неработающем приложении. Для проверки приложений потребуется отдельный TCP/HTTP healthcheck.
- Репликация не является распределённым консенсусом. Нет leases, quorum или защиты от split-brain.
- Отказ процесса монитора не обнаруживается, если хост продолжает отвечать на ping.
Restart=alwaysи restart policy Docker защищают от части таких отказов, но не заменяют heartbeat. - При первом запуске новые хосты считаются
UP. ПервыйDOWNвозникает только послеFAIL_THRESHOLDнеудачных проверок. - Все переходы цикла объединяются в одно сообщение. Это уменьшает риск rate limit, но текущая версия не разбивает сообщение на части: при очень большом количестве событий можно превысить лимит длины Telegram.
- Отказ единственного канала доставки не компенсируется репликацией. Если все реплики используют один и тот же прокси, его отказ заблокирует уведомления всех экземпляров.
- Отказ самого одиночного монитора остаётся невидимым для него самого. Для такого сценария нужен внешний dead man's switch или вторая реплика.
Безопасность
- Храните заполненный
pingmon.envс правами0600и не добавляйте его в систему контроля версий. - Токен передаётся
curlчерез stdin-конфиг (-K -), а не через аргументы командной строки, поэтому не отображается в argv процесса. - Если URL прокси содержит логин и пароль, они также становятся частью конфигурации окружения; защищайте env-файл соответствующим образом.
hostsне должен содержать секреты: в systemd-установке он намеренно доступен непривилегированному процессу.
Минимальная проверка перед установкой
Проверка синтаксиса:
bash -n pingmon.sh
Проверка конфигурации Compose без запуска:
docker compose config --quiet
Перед боевым запуском рекомендуется временно добавить отдельный тестовый хост и проверить оба перехода DOWN и UP, а также убедиться, что бот имеет право отправлять сообщения в выбранный чат.