tg_monitor_bot/tg-bot-mon.py

247 lines
10 KiB
Python
Raw Permalink 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.

import asyncio
import logging
from datetime import datetime
from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
from config import BOT_TOKEN, ADMIN_ID, HOSTS_LIST
from monitor import collect_metrics
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("bot.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# Кеш для хранения последних собранных данных
CACHE = {}
def format_metrics(data):
"""Формирует красивое сообщение из метрик"""
if "error" in data:
return f"❌ Ошибка подключения к {data['host']}:\n{data['error']}"
m = data['metrics']
host = data['host']
ip = data['ip']
host_type = data.get('host_type', 'unknown')
# Эмодзи для типа хоста
type_emoji = {
'physical': '🖥️',
'vm': '🖥️',
'lxc': '📦',
'container': '🐳'
}.get(host_type, '🖥️')
type_label = {
'physical': 'Железо',
'vm': 'ВМ',
'lxc': 'LXC',
'container': 'Контейнер'
}.get(host_type, 'Неизвестно')
text = f"{type_emoji} *{host} ({ip})* - `{type_label}`\n"
text += f"🕒 *Uptime:* {m.get('uptime', 'N/A')}\n"
text += f"📊 *Load Avg:* {m.get('load_1', '0')}, {m.get('load_5', '0')}, {m.get('load_15', '0')}\n\n"
# Алерты (если есть)
alerts = m.get('alerts', [])
if alerts:
text += "🚨 *АЛЕРТЫ!*\n"
for alert in alerts:
text += f" {alert}\n"
text += "\n"
# CPU
cpu_bar = "" * int(m['cpu'] // 5) + "" * (20 - int(m['cpu'] // 5))
cpu_color = "🔴" if m['cpu'] > 80 else "🟡" if m['cpu'] > 60 else "🟢"
text += f"{cpu_color} *CPU:* {m['cpu']:.1f}%\n`{cpu_bar}`\n"
# RAM
mem_bar = "" * int(m['mem_percent'] // 5) + "" * (20 - int(m['mem_percent'] // 5))
mem_color = "🔴" if m['mem_percent'] > 85 else "🟡" if m['mem_percent'] > 70 else "🟢"
text += f"{mem_color} *RAM:* {m['mem_used_mb']} MB / {m.get('mem_total_mb', '?')} MB ({m['mem_percent']}%)\n`{mem_bar}`\n"
# Диск (свободное место)
disk_color = "🔴" if m.get('disk_usage', 0) > 80 else "🟡" if m.get('disk_usage', 0) > 70 else "🟢"
text += f"{disk_color} *Disk (/):* {m.get('disk_usage', 0):.1f}% used\n"
# Сеть
text += f"🌐 *Network (per hour):* 📥 {m['net_rx_mb']} MB | 📤 {m['net_tx_mb']} MB\n"
# Диски iostat
text += "\n💾 *Disks (top 3 by %util):*\n"
for disk in m.get('disks', []):
text += f" └─ {disk['name']}: util {disk['util']:.1f}% (r: {disk['rps']:.1f}/s, w: {disk['wps']:.1f}/s)\n"
# Температуры (только для железа)
if host_type == "physical" and m.get('temps'):
text += "\n🌡️ *Temperatures:*\n"
for i, temp in enumerate(m['temps']):
temp_color = "🔴" if temp > 75 else "🟡" if temp > 60 else "🟢"
text += f" {temp_color} Sensor {i+1}: {temp:.1f}°C\n"
elif host_type != "physical":
text += "\n🌡️ *Temperatures:* Не требуется (ВМ/LXC)\n"
# Топ процессов
text += "🏆 *Top 5 CPU:*\n```\n"
top_cpu = m.get('top_cpu', 'N/A')
# Экранируем опасные символы для Markdown
top_cpu = top_cpu.replace('_', '\\_').replace('*', '\\*').replace('`', '\\`')
text += top_cpu + "\n```"
text += "🏆 *Top 5 RAM:*\n```\n"
top_mem = m.get('top_mem', 'N/A')
top_mem = top_mem.replace('_', '\\_').replace('*', '\\*').replace('`', '\\`')
text += top_mem + "\n```"
# text += "\n🏆 *Top 5 CPU:*\n```\n"
# text += m.get('top_cpu', 'N/A') + "\n```"
# text += "🏆 *Top 5 RAM:*\n```\n"
# text += m.get('top_mem', 'N/A') + "\n```"
# Логи и безопасность
text += "\n⚠️ *Recent Errors (journalctl -p 3):*\n```\n"
text += m.get('logs_errors', 'N/A')[:300] + "\n```"
# OOM
text += "\n💀 *Out of Memory Events:*\n```\n"
text += m.get('oom', 'Нет данных') + "\n```"
# Fail2ban
text += "\n🛡️ *Fail2ban Status:*\n```\n"
text += m.get('fail2ban_status', 'N/A') + "\n```"
text += "\n🚫 *Fail2ban Logs (last bans):*\n```\n"
text += m.get('fail2ban_logs', 'N/A') + "\n```"
text += f"\n⏱️ *Обновлено:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
return text
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /status"""
if update.effective_user.id != ADMIN_ID:
await update.message.reply_text("⛔ Доступ запрещен. Ты не мой хозяин.")
return
# Создаем клавиатуру с хостами
keyboard = []
for host in HOSTS_LIST:
keyboard.append([InlineKeyboardButton(host['name'], callback_data=f"host_{host['ip']}")])
keyboard.append([InlineKeyboardButton("🔄 Все хосты", callback_data="host_all")])
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"🔍 Выбери хост для мониторинга или нажми 'Все хосты':",
reply_markup=reply_markup
)
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработка нажатий на кнопки"""
query = update.callback_query
await query.answer()
if update.effective_user.id != ADMIN_ID:
await query.edit_message_text("⛔ Недостаточно прав.")
return
data = query.data
if data == "host_all":
await query.edit_message_text("⏳ Собираю данные со всех хостов...")
full_report = ""
for host in HOSTS_LIST:
result = collect_metrics(host)
report = format_metrics(result)
full_report += report + "\n\n" + "="*30 + "\n\n"
# Обрезаем, если слишком длинное
if len(full_report) > 4000:
full_report = full_report[:4000] + "\n\n... (обрезано)"
await query.edit_message_text(full_report, parse_mode="Markdown")
elif data.startswith("host_"):
ip = data.replace("host_", "")
host = next((h for h in HOSTS_LIST if h['ip'] == ip), None)
if not host:
await query.edit_message_text("Хост не найден")
return
await query.edit_message_text(f"⏳ Собираю данные с {host['name']}...")
result = collect_metrics(host)
report = format_metrics(result)
await query.edit_message_text(report, parse_mode="Markdown")
async def scheduled_job(context: ContextTypes.DEFAULT_TYPE):
"""Функция, вызываемая раз в час"""
bot: Bot = context.bot
logger.info("🔄 Запущен плановый сбор метрик (каждый час)")
for host in HOSTS_LIST:
try:
logger.info(f"📊 Собираю данные с {host['name']}")
result = collect_metrics(host)
report = format_metrics(result)
await bot.send_message(chat_id=ADMIN_ID, text=report, parse_mode="Markdown")
logger.info(f"✅ Отправлен отчет для {host['name']}")
except Exception as e:
error_msg = f"❌ Ошибка при сборе/отправке для {host['name']}: {e}"
logger.error(error_msg)
await bot.send_message(chat_id=ADMIN_ID, text=error_msg)
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Команда /start - проверка, что бот жив"""
if update.effective_user.id != ADMIN_ID:
await update.message.reply_text("Привет! Я бот для мониторинга серверов, но ты не мой хозяин.")
return
await update.message.reply_text(
"🤖 *Бот мониторинга запущен!*\n\n"
"📌 Используй команду `/status`, чтобы получить актуальные метрики с серверов.\n"
"⏰ Каждый час я буду присылать автоматический отчет.\n\n"
f"📋 Настроено хостов: {len(HOSTS_LIST)}",
parse_mode="Markdown"
)
def main():
"""Главная функция запуска бота"""
logger.info("🚀 Запуск бота...")
logger.info(f"📋 Загружено хостов: {len(HOSTS_LIST)}")
# Проверяем хосты при старте
if not HOSTS_LIST:
logger.error("❌ Нет хостов в конфигурации! Проверь .env файл!")
return
for host in HOSTS_LIST:
logger.info(f" - {host['name']} ({host['ip']}:{host['port']})")
app = Application.builder().token(BOT_TOKEN).build()
# Регистрируем команды
app.add_handler(CommandHandler("start", start_command))
app.add_handler(CommandHandler("status", status_command))
app.add_handler(CallbackQueryHandler(button_callback))
# Настройка шедулера (раз в час)
job_queue = app.job_queue
if job_queue:
# Запускаем через 10 секунд после старта, потом каждый час
job_queue.run_repeating(scheduled_job, interval=3600, first=10)
logger.info("⏰ Шедулер настроен: отчеты будут приходить каждый час")
else:
logger.warning("⚠️ JobQueue не доступна! Автоматические отчеты не будут работать.")
logger.warning("⚠️ Установи: pip install 'python-telegram-bot[job-queue]'")
logger.info("🤖 Бот запущен и готов к работе! Напиши /start в Telegram")
try:
app.run_polling()
except KeyboardInterrupt:
logger.info("🛑 Бот остановлен пользователем")
except Exception as e:
logger.error(f"❌ Ошибка при работе бота: {e}")
if __name__ == "__main__":
main()