Версия 1.7 | 12 сентября 2026
Практическое руководство для сервера 193.32.179.138: как устроены алерты Netdata, как управлять доставкой уведомлений в Telegram и как менять пороговые значения (например, предупреждение о заполнении диска при 50% вместо штатных 90%). Отдельный раздел — периодические отчёты и статус по запросу через бота. Установка Netdata описана в сравнении инструментов мониторинга.
Netdata каждые N секунд вычисляет выражения всех алертов. Когда значение переходит границу, алерт меняет состояние, и в этот момент (и только в этот) отправляется уведомление:
метрики ──► health-движок (каждую минуту вычисляет warn/crit)
│
├── состояние CLEAR ──► всё хорошо, молчим
├── переход в WARNING ──► alarm-notify.sh ──► Telegram ⚠️
├── переход в CRITICAL ─► alarm-notify.sh ──► Telegram 🔥
└── возврат в CLEAR ────► alarm-notify.sh ──► Telegram ✅
delay дополнительно защищает от «дребезга» значения вокруг границы;health_alarm_notify.conf;health.d/*.conf.| Что | Значение |
|---|---|
| Бот Telegram | @avk_vps_monitoring_bot |
| Получатель | chat_id 265987634 (Andrey) |
| Конфиг уведомлений | /opt/infrastructure/netdata/config/health_alarm_notify.conf |
| Переопределения алертов | /opt/infrastructure/netdata/config/health.d/ |
| Штатные (эталонные) алерты | /usr/lib/netdata/conf.d/health.d/ (внутри контейнера, read-only) |
| Кастомизация | диск: warning при > 50% (штатно 90%); email-канал отключён |
| Бот статуса (по запросу) | systemd-сервис vps-status-bot (скрипт: /opt/scripts/vps-status-bot.py) |
/opt/infrastructure/netdata/config/ смонтирован в контейнер как /etc/netdata/. Редактировать можно на хосте (удобнее) или через docker exec netdata edit-config <файл> — это одно и то же.
Файл — обычный shell-скрипт, Netdata перечитывает его при каждой отправке, поэтому изменения применяются без перезапуска. В конце нашего файла добавлены строки:
# --- Telegram notifications (avk27.ru monitoring, added 2026-09-12) ---
SEND_TELEGRAM="YES" # включить канал Telegram
TELEGRAM_BOT_TOKEN="<токен бота>" # от @BotFather
DEFAULT_RECIPIENT_TELEGRAM="265987634" # кому слать (все роли)
SEND_EMAIL="NO" # email-канал отключен
| Переменная | Назначение |
|---|---|
| SEND_TELEGRAM | YES/NO — включает или полностью выключает Telegram |
| TELEGRAM_BOT_TOKEN | токен бота; хранится в этом файле (права 600, владелец netdata) |
| DEFAULT_RECIPIENT_TELEGRAM | список chat_id через пробел; действует для всех ролей (sysadmin и др.) |
| role_recipients_telegram[sysadmin] | адресация по ролям — можно разделить, кому идут критичные, а кому нет |
curl -s "https://api.telegram.org/bot<токен>/getUpdates" | grep -o '"chat":{"id":[0-9]*'
DEFAULT_RECIPIENT_TELEGRAM="265987634 <второй_chat_id>"
# Шлёт 3 тестовых уведомления (warning, critical, clear)
docker exec -u netdata netdata /usr/libexec/netdata/plugins.d/alarm-notify.sh test
В определении алерта (шаблоне) строка to: задаёт получателей. Значение silent глушит алерт — он виден в интерфейсе, но ничего не отправляет:
to: silent # вместо: to: sysadmin
/revoke — старый токен умрёт;TELEGRAM_BOT_TOKEN в health_alarm_notify.conf;health.d/ с тем же именем, что у штатного, полностью замещает штатный. Нельзя положить в disks.conf только один шаблон — остальные алерты этого файла (inodes, скорость заполнения и т.д.) пропадут. Правильный порядок: скопировать штатный файл целиком → точечно отредактировать нужные строки → перечитать конфигурацию.
template: disk_space_usage # имя — по нему алерт ищется в UI и API
on: disk.space # источник: контекст графика
chart labels: mount_point=!/dev ... # фильтр: для каких точек монтирования
calc: $used * 100 / ($avail + $used) # формула: $this = % занятого места
units: %
every: 1m # период проверки
warn: $this > (($status >= $WARNING ) ? (45) : (50)) # порог WARNING
crit: ($this > (($status == $CRITICAL) ? (90) : (98))) && $avail < 5
delay: up 1m down 15m multiplier 1.5 max 1h # защита от дребезга
summary: Disk ${label:mount_point} space usage # заголовок в уведомлении
info: Total space utilization ... # пояснение в уведомлении
to: sysadmin # кому слать
Выражение $this > (($status >= $WARNING ) ? (45) : (50)) читается так:
Зазор в 5% не даёт алерту «моргать» туда-сюда, когда значение скачет вокруг границы. Хотите один порог без гистерезиса — напишите просто warn: $this > 50.
# 1. Забрать штатный файл целиком (на хост, в каталог переопределений)
ssh root@193.32.179.138
docker exec netdata cat /usr/lib/netdata/conf.d/health.d/disks.conf \
> /opt/infrastructure/netdata/config/health.d/disks.conf
# 2. Отредактировать строку warn нужного шаблона
# (внутри контейнера: docker exec -it netdata edit-config disks.conf)
# warn: $this > (($status >= $WARNING ) ? (45) : (50)) # было 80/90
# 3. Права и применение
chown 201:201 /opt/infrastructure/netdata/config/health.d/disks.conf
docker exec netdata netdatacli reload-health
# 4. Проверка: алерт должен увидеть новое значение
docker exec netdata curl -s "http://localhost:19999/api/v1/alarms?all" | grep -o \
'"disk_space_usage"[^}]*"status":"[A-Z]*"' | head -1
Так на этом сервере порог предупреждения о диске уже снижен с 90% до 50% (снятие — 45%); критический порог оставлен штатным: > 98% при остатке < 5 GB.
rm /opt/infrastructure/netdata/config/health.d/disks.conf
docker exec netdata netdatacli reload-health
# По имени или значению — по всем штатным файлам:
docker exec netdata grep -rl "disk_space_usage" /usr/lib/netdata/conf.d/health.d/
# Список всех активных алертов и их состояний:
docker exec netdata curl -s "http://localhost:19999/api/v1/alarms?all" | python3 -m json.tool | less
# В интерфейсе: netdata.avk27.ru → вкладка Alerts (бейдж с числами = активные)
Файл с новым именем не совпадает ни с одним штатным — он не замещает, а дополняет их. Пример: слать предупреждение, если контейнер 3x-ui грузит CPU:
Создать файл /opt/infrastructure/netdata/config/health.d/custom.conf:
template: 3xui_cpu_usage
on: cgroup_3x-ui.cpu
class: Utilization
type: System
component: CPU
lookup: average -10m unaligned of user, system
every: 1m
units: %
warn: $this > (($status >= $WARNING) ? (70) : (80))
crit: $this > (($status == $CRITICAL) ? (85) : (90))
delay: down 15m multiplier 1.5 max 1h
summary: CPU контейнера 3x-ui
info: Средняя загрузка CPU VPN-контейнера за последние 10 минут
to: sysadmin
chown 201:201 /opt/infrastructure/netdata/config/health.d/custom.conf
docker exec netdata netdatacli reload-health
Имя графика для on: можно подсмотреть в интерфейсе (заголовок графика → Details) или в списке графиков: docker exec netdata curl -s http://localhost:19999/api/v1/charts.
Актуально для установленной версии Netdata v2.11.0 (Ubuntu 24.04, 2 vCPU). Файлы — в /usr/lib/netdata/conf.d/health.d/.
| Алерт | Файл | Предупреждение | Критично |
|---|---|---|---|
| Заполненность диска (disk_space_usage) | disks.conf | > 90% → у нас переопределено: > 50% | > 98% и свободно < 5 GB |
| Прогноз заполнения (out_of_disk_space_time) | disks.conf | диск заполнится < чем через 8 ч | < 2 ч |
| Inodes диска (disk_inode_usage) | disks.conf | > 90% | > 98% |
| Загрузка CPU за 10 мин (10min_cpu_usage) | cpu.conf | > 85% | > 95% |
| Ожидание I/O CPU (10min_cpu_iowait) | cpu.conf | > 40% | — |
| RAM занято | ram.conf | > 90% | > 98% |
| RAM доступно | ram.conf | < 10% | — |
| Load average (15 мин) | load.conf | > 200% от числа ядер | — |
Штатно Netdata отправляет уведомления только при смене состояния алертов — встроенного «дайджеста по расписанию» нет. Но API Netdata отдаёт любые метрики в JSON, поэтому обе задачи решаются поверх уже существующего бота @avk_vps_monitoring_bot:
| Вариант | Как работает | Когда выбирать |
|---|---|---|
| Ежедневный отчёт (cron) | Скрипт раз в сутки собирает метрики из API Netdata и отправляет одним сообщением; можно запускать и вручную | Хочу утром видеть сводку — рекомендую начать с этого |
| Статус по команде боту | Постоянно работающий процесс слушает сообщения (long polling), по команде /status присылает ту же сводку |
Хочу узнавать состояние в любой момент с телефона |
disk_space./ — used/avail в GB; system.ram — free/used/cached/buffers в MiB; system.load — load1/5/15 в натуральной величине; system.cpu — idle в %; system.uptime — секунды; активные алерты — /api/v1/alarms.
Создайте файл /opt/scripts/vps-report.sh:
#!/bin/bash
# Отчёт о состоянии VPS в Telegram. Источник метрик — Netdata API.
set -euo pipefail
BOT_TOKEN="<токен_бота>" # от @BotFather
CHAT_ID="265987634" # ваш chat_id
# Забираем метрики и алерты из Netdata
METRICS=$(docker exec netdata curl -s "http://localhost:19999/api/v1/allmetrics?format=json")
ALERTS=$(docker exec netdata curl -s "http://localhost:19999/api/v1/alarms")
# Собираем текст отчёта (python3 входит в Ubuntu 24.04)
TEXT=$(METRICS="$METRICS" ALERTS="$ALERTS" python3 << 'PY'
import json, os, datetime
m = json.loads(os.environ['METRICS'])
a = json.loads(os.environ['ALERTS'])
def v(chart, dim):
return m[chart]['dimensions'][dim]['value']
disk_used = v('disk_space./', 'used') # GB
disk_avail = v('disk_space./', 'avail') # GB
disk_pct = disk_used * 100 / (disk_used + disk_avail)
ram = m['system.ram']['dimensions'] # MiB
ram_total = sum(d['value'] for k, d in ram.items()
if k in ('free', 'used', 'cached', 'buffers'))
ram_pct = ram['used']['value'] * 100 / ram_total
cpu_busy = 100 - v('system.cpu', 'idle') # %
load1 = v('system.load', 'load1') # натуральное значение
uptime_d = int(v('system.uptime', 'uptime') / 86400)
active = [x for x in a.get('alarms', {}).values()
if x.get('status') in ('WARNING', 'CRITICAL')]
alerts = ', '.join(f"{x['name']} — {x['status']}" for x in active[:3]) or 'нет активных'
print("🖥 VPS 193.32.179.138 — " + datetime.datetime.now().strftime('%d.%m.%Y %H:%M'))
print(f"💾 Диск: {disk_pct:.0f}% занято, свободно {disk_avail:.1f} GB")
print(f"🧠 RAM: {ram_pct:.0f}% занято")
print(f"⚡ CPU: {cpu_busy:.0f}% · load 1m: {load1:.2f}")
print(f"⏱ Аптайм: {uptime_d} дн")
print(f"🛡 Алерты: {len(active)} ({alerts})")
PY
)
# Отправка
curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d chat_id="${CHAT_ID}" --data-urlencode text="${TEXT}" >/dev/null
echo "${TEXT}"
Установка и расписание:
chmod 700 /opt/scripts/vps-report.sh
# Проверка — сообщение придёт в Telegram
/opt/scripts/vps-report.sh
# Расписание: каждый день в 09:00 МСК (сервер живёт по московскому времени)
(crontab -l 2>/dev/null; echo "0 9 * * * /opt/scripts/vps-report.sh >> /var/log/vps-report.log 2>&1") | crontab -
Сообщение выглядит так (реальные значения с сервера на момент написания):
🖥 VPS 193.32.179.138 — 12.09.2026 19:12
💾 Диск: 77% занято, свободно 8.7 GB
🧠 RAM: 63% занято
⚡ CPU: 47% · load 1m: 4.16
⏱ Аптайм: 42 дн
🛡 Алерты: 1 (disk_space_usage — WARNING)
Особенность Telegram: бот не может написать первым — только ответить. Значит, нужен постоянно работающий процесс, который слушает входящие сообщения (long polling) и отвечает на команды. Размещаем его на хосте как systemd-сервис: он дергает API Netdata через docker exec и отправляет сводку тем же ботом.
Интерактив — два штатных механизма Telegram: меню команд (кнопка «/» рядом с полем ввода, заполняется методом setMyCommands) и инлайн-кнопки под сообщением (reply_markup с inline_keyboard; нажатия приходят обновлением callback_query, на каждое нужно ответить answerCallbackQuery, иначе у пользователя кнопка будет «крутиться»). В скрипте ниже: в меню — команды /status, /alerts, /ping, а под каждым отчётом — кнопки «🔄 Обновить» и «🛡 Алерты».
if msg.get("chat", {}).get("id") != ADMIN_CHAT: continue. Конфликтов с вариантом 1 нет: cron-скрипт только отправляет (sendMessage), а getUpdates читает один лишь этот бот.
Создайте файл /opt/scripts/vps-status-bot.py:
#!/usr/bin/env python3
"""Статус VPS по команде /status в Telegram (long polling)."""
import json, os, subprocess, sys, time, urllib.request
TOKEN = os.environ["BOT_TOKEN"] # из /etc/vps-report.env
ADMIN_CHAT = 265987634 # отвечаем только этому chat_id
OFFSET = 0
def tg(method, **params):
req = urllib.request.Request(
f"https://api.telegram.org/bot{TOKEN}/{method}",
data=json.dumps(params).encode(),
headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req, timeout=35).read())
def nd(path):
out = subprocess.run(
["docker", "exec", "netdata", "curl", "-s",
f"http://localhost:19999/api/v1/{path}"],
capture_output=True, text=True).stdout
return json.loads(out)
def report():
m, a = nd("allmetrics?format=json"), nd("alarms")
def v(chart, dim):
return m[chart]['dimensions'][dim]['value']
disk_used = v('disk_space./', 'used') # GB
disk_avail = v('disk_space./', 'avail') # GB
disk_pct = disk_used * 100 / (disk_used + disk_avail)
ram = m['system.ram']['dimensions'] # MiB
ram_total = sum(d['value'] for k, d in ram.items()
if k in ('free', 'used', 'cached', 'buffers'))
ram_pct = ram['used']['value'] * 100 / ram_total
cpu_busy = 100 - v('system.cpu', 'idle') # %
load1 = v('system.load', 'load1') # натуральное значение
uptime_d = int(v('system.uptime', 'uptime') / 86400)
active = [x for x in a.get('alarms', {}).values()
if x.get('status') in ('WARNING', 'CRITICAL')]
alerts = ', '.join(f"{x['name']} — {x['status']}" for x in active[:3]) or 'нет активных'
return "\n".join([
"🖥 VPS 193.32.179.138",
f"💾 Диск: {disk_pct:.0f}% занято, свободно {disk_avail:.1f} GB",
f"🧠 RAM: {ram_pct:.0f}% занято",
f"⚡ CPU: {cpu_busy:.0f}% · load 1m: {load1:.2f}",
f"⏱ Аптайм: {uptime_d} дн",
backup_line(),
f"🛡 Алерты: {len(active)} ({alerts})",
])
def backup_line():
try:
p = "/opt/backups/.last-status" # пишет backup.sh при каждом запуске
age_h = (time.time() - os.path.getmtime(p)) / 3600
if age_h > 30:
return f"🗂 Бэкап: ⚠️ нет свежей копии ({age_h:.0f} ч назад)"
st = open(p).read().strip().split("|")
return f"🗂 Бэкап: {st[0]}, {st[2]}, {st[1]} МСК"
except Exception:
return "🗂 Бэкап: ⚠️ статус неизвестен"
def alerts_text():
a = nd("alarms")
act = [x for x in a.get("alarms", {}).values()
if x.get("status") in ("WARNING", "CRITICAL")]
if not act:
return "🛡 Активных алертов нет"
return "🛡 Активные алерты:\n" + "\n".join(
("⚠️ " if x["status"] == "WARNING" else "🔥 ") + x["name"] + " — " + x["status"]
for x in act)
# Кнопки под сообщением (inline keyboard)
BUTTONS = {"inline_keyboard": [[
{"text": "🔄 Обновить", "callback_data": "status"},
{"text": "🛡 Алерты", "callback_data": "alerts"}]]}
# Разовая проверка: отправить отчёт и выйти (для теста деплоя)
if "--test" in sys.argv:
tg("sendMessage", chat_id=ADMIN_CHAT, text=report(), reply_markup=BUTTONS)
print("test report sent")
sys.exit(0)
# Меню команд — кнопка «/» рядом с полем ввода
try:
tg("setMyCommands", commands=[
{"command": "status", "description": "Состояние сервера"},
{"command": "alerts", "description": "Активные алерты"},
{"command": "ping", "description": "Проверить, что бот жив"}])
except Exception as e:
print("menu setup failed:", e, flush=True)
while True:
try:
for u in tg("getUpdates", offset=OFFSET, timeout=25)["result"]:
OFFSET = u["update_id"] + 1
# Нажатие кнопки под сообщением (callback_query)
if "callback_query" in u:
cb = u["callback_query"]
if cb.get("from", {}).get("id") != ADMIN_CHAT:
continue # чужим не отвечаем (и на кнопки тоже)
if cb.get("data") == "status":
tg("sendMessage", chat_id=ADMIN_CHAT, text=report(),
reply_markup=BUTTONS)
elif cb.get("data") == "alerts":
tg("sendMessage", chat_id=ADMIN_CHAT, text=alerts_text(),
reply_markup=BUTTONS)
tg("answerCallbackQuery", callback_query_id=cb["id"])
continue
# Обычное сообщение или команда из меню
msg = u.get("message") or {}
if msg.get("chat", {}).get("id") != ADMIN_CHAT:
continue # чужим не отвечаем
text = msg.get("text", "").strip()
if text.startswith(("/start", "/status")):
tg("sendMessage", chat_id=ADMIN_CHAT, text=report(),
reply_markup=BUTTONS)
elif text.startswith("/alerts"):
tg("sendMessage", chat_id=ADMIN_CHAT, text=alerts_text(),
reply_markup=BUTTONS)
elif text == "/ping":
tg("sendMessage", chat_id=ADMIN_CHAT, text="pong")
except Exception as e:
print("error:", e, flush=True)
time.sleep(5)
Токен — в отдельном файле /etc/vps-report.env (права 600), юнит — /etc/systemd/system/vps-status-bot.service:
# /etc/vps-report.env
BOT_TOKEN=<токен_бота>
# /etc/systemd/system/vps-status-bot.service
[Unit]
Description=VPS status Telegram bot (/status)
After=network-online.target docker.service
Wants=network-online.target
[Service]
EnvironmentFile=/etc/vps-report.env
ExecStart=/usr/bin/python3 /opt/scripts/vps-status-bot.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
chmod 600 /etc/vps-report.env
chmod 700 /opt/scripts/vps-status-bot.py
systemctl daemon-reload
systemctl enable --now vps-status-bot
# Логи бота
journalctl -u vps-status-bot -f
vps-status-bot, токен в /etc/vps-report.env). Быстрая проверка после правок скрипта: set -a; . /etc/vps-report.env; set +a; python3 /opt/scripts/vps-status-bot.py --test — пришлёт один отчёт и выйдет, затем systemctl restart vps-status-bot. 12.09.2026 бот расширен отчётами /disk, /ram, /cpu (разрезы по каталогам, процессам и контейнерам) и командой /clear (очистка истории чата) — реализация и примеры вывода в отдельном документе «Отчёты бота: Диск, RAM и CPU в разрезе».
Что выбрать. Начните с варианта 1: он настраивается за 15 минут, не добавляет новых сервисов и не требует обслуживания — пока живы сервер и Netdata, отчёт придёт. Ручная отправка в любой момент — одна команда по SSH.
Вариант 2 добавляйте, если поймаете себя на желании спросить «как там сервер» чаще одного раза в день: /status из Telegram удобнее всего с телефона. Цена — ещё один процесс на хосте, который надо иногда смотреть (journalctl) и обновлять при смене токена. Скрипты вариантов совместимы: бот и cron используют одного бота и один источник метрик.
# Перечитать конфигурацию алертов (после правки файлов)
docker exec netdata netdatacli reload-health
# Тестовые уведомления в Telegram
docker exec -u netdata netdata /usr/libexec/netdata/plugins.d/alarm-notify.sh test
# Все алерты и их текущие состояния
docker exec netdata curl -s "http://localhost:19999/api/v1/alarms?all"
# Редактирование конфигов (внутри контейнера)
docker exec -it netdata edit-config health_alarm_notify.conf
docker exec -it netdata edit-config disks.conf
# Пути: хост /opt/infrastructure/netdata/config/* = контейнер /etc/netdata/*
# Штатные эталоны: /usr/lib/netdata/conf.d/health.d/ (только для чтения)
🎉 Готово. Документ обновляйте при изменении порогов или получателей.