← Вернуться к оглавлению

🔕 Netdata: алерты и уведомления в Telegram

Версия 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 ✅
        

📍 Текущая конфигурация на сервере

Что Значение
Бот 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 <файл> — это одно и то же.

📨 Управление уведомлениями в Telegram

Основные переменные health_alarm_notify.conf

Файл — обычный 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] адресация по ролям — можно разделить, кому идут критичные, а кому нет

Добавить второго получателя

  1. Новый человек открывает бота и нажимает Start (бот не может написать первым);
  2. Узнаём его chat_id:
    curl -s "https://api.telegram.org/bot<токен>/getUpdates" | grep -o '"chat":{"id":[0-9]*'
                    
  3. Дописываем id через пробел:
    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
        

Смена токена (компрометация)

  1. В @BotFather выполнить /revoke — старый токен умрёт;
  2. Заменить TELEGRAM_BOT_TOKEN в health_alarm_notify.conf;
  3. Прогнать тестовую отправку (команда выше).

🎚️ Управление пороговыми значениями

⚠️ Главное правило: файл в health.d/ с тем же именем, что у штатного, полностью замещает штатный. Нельзя положить в disks.conf только один шаблон — остальные алерты этого файла (inodes, скорость заполнения и т.д.) пропадут. Правильный порядок: скопировать штатный файл целиком → точечно отредактировать нужные строки → перечитать конфигурацию.

Анатомия алерта (нашем примере disk_space_usage)

    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.

Пошагово: поменять порог (пример — диск 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 присылает ту же сводку Хочу узнавать состояние в любой момент с телефона
💡 Проверенные имена метрик API (для этого сервера, Netdata v2.11): 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.

Вариант 1 — отчёт по расписанию (рекомендуется)

Создайте файл /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)
        

Вариант 2 — статус по запросу боту

Особенность Telegram: бот не может написать первым — только ответить. Значит, нужен постоянно работающий процесс, который слушает входящие сообщения (long polling) и отвечает на команды. Размещаем его на хосте как systemd-сервис: он дергает API Netdata через docker exec и отправляет сводку тем же ботом.

Интерактив — два штатных механизма Telegram: меню команд (кнопка «/» рядом с полем ввода, заполняется методом setMyCommands) и инлайн-кнопки под сообщением (reply_markup с inline_keyboard; нажатия приходят обновлением callback_query, на каждое нужно ответить answerCallbackQuery, иначе у пользователя кнопка будет «крутиться»). В скрипте ниже: в меню — команды /status, /alerts, /ping, а под каждым отчётом — кнопки «🔄 Обновить» и «🛡 Алерты».

⚠️ Безопасность: бот обязан отвечать только вашему chat_id — иначе любой, кто найдёт бота, сможет запрашивать метрики сервера. В скрипте ниже это проверяет строка 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
        
💡 Развёрнуто: на этом сервере бот работает с 12.09.2026 (сервис 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/ (только для чтения)
        

🎉 Готово. Документ обновляйте при изменении порогов или получателей.