Add system metrics snapshot
This commit is contained in:
@@ -17,7 +17,7 @@ async def help_cmd(msg: Message):
|
|||||||
"🐳 Docker — управление контейнерами\n"
|
"🐳 Docker — управление контейнерами\n"
|
||||||
"📦 Backup — restic бэкапы\n"
|
"📦 Backup — restic бэкапы\n"
|
||||||
"🧉 Artifacts — критичные образы (Clonezilla, NAND)\n"
|
"🧉 Artifacts — критичные образы (Clonezilla, NAND)\n"
|
||||||
"⚙️ System — диски, безопасность, URL, reboot\n\n"
|
"⚙️ System — диски, безопасность, URL, metrics, reboot\n\n"
|
||||||
"Inline-кнопки используются для выбора контейнеров.",
|
"Inline-кнопки используются для выбора контейнеров.",
|
||||||
reply_markup=menu_kb,
|
reply_markup=menu_kb,
|
||||||
parse_mode="Markdown",
|
parse_mode="Markdown",
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from services.http_checks import get_url_checks, check_url
|
|||||||
from services.queue import enqueue
|
from services.queue import enqueue
|
||||||
from services.updates import list_updates, apply_updates
|
from services.updates import list_updates, apply_updates
|
||||||
from services.runner import run_cmd
|
from services.runner import run_cmd
|
||||||
from state import UPDATES_CACHE, REBOOT_PENDING
|
from state import UPDATES_CACHE, REBOOT_PENDING, METRICS_STORE
|
||||||
|
from services.metrics import summarize
|
||||||
|
|
||||||
|
|
||||||
@dp.message(F.text == "💽 Disks")
|
@dp.message(F.text == "💽 Disks")
|
||||||
@@ -55,6 +56,16 @@ async def urls(msg: Message):
|
|||||||
asyncio.create_task(worker())
|
asyncio.create_task(worker())
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message(F.text == "📈 Metrics")
|
||||||
|
async def metrics(msg: Message):
|
||||||
|
if not is_admin_msg(msg):
|
||||||
|
return
|
||||||
|
if METRICS_STORE is None:
|
||||||
|
await msg.answer("⚠️ Metrics not initialized", reply_markup=system_kb)
|
||||||
|
return
|
||||||
|
await msg.answer(summarize(METRICS_STORE, minutes=15), reply_markup=system_kb)
|
||||||
|
|
||||||
|
|
||||||
@dp.message(F.text == "📦 Updates")
|
@dp.message(F.text == "📦 Updates")
|
||||||
async def updates_list(msg: Message):
|
async def updates_list(msg: Message):
|
||||||
if not is_admin_msg(msg):
|
if not is_admin_msg(msg):
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ artifacts_kb = ReplyKeyboardMarkup(
|
|||||||
system_kb = ReplyKeyboardMarkup(
|
system_kb = ReplyKeyboardMarkup(
|
||||||
keyboard=[
|
keyboard=[
|
||||||
[KeyboardButton(text="💽 Disks"), KeyboardButton(text="🔐 Security")],
|
[KeyboardButton(text="💽 Disks"), KeyboardButton(text="🔐 Security")],
|
||||||
[KeyboardButton(text="🌐 URLs"), KeyboardButton(text="📦 Updates")],
|
[KeyboardButton(text="🌐 URLs"), KeyboardButton(text="📈 Metrics")],
|
||||||
[KeyboardButton(text="⬆️ Upgrade"), KeyboardButton(text="🔄 Reboot")],
|
[KeyboardButton(text="📦 Updates"), KeyboardButton(text="⬆️ Upgrade")],
|
||||||
|
[KeyboardButton(text="🔄 Reboot")],
|
||||||
[KeyboardButton(text="⬅️ Назад")],
|
[KeyboardButton(text="⬅️ Назад")],
|
||||||
],
|
],
|
||||||
resize_keyboard=True,
|
resize_keyboard=True,
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -5,6 +5,7 @@ from app import bot, dp, cfg, ADMIN_ID
|
|||||||
from keyboards import menu_kb
|
from keyboards import menu_kb
|
||||||
from services.docker import discover_containers, docker_watchdog
|
from services.docker import discover_containers, docker_watchdog
|
||||||
from services.alerts import monitor_resources, monitor_smart
|
from services.alerts import monitor_resources, monitor_smart
|
||||||
|
from services.metrics import MetricsStore, start_sampler
|
||||||
from services.queue import worker as queue_worker
|
from services.queue import worker as queue_worker
|
||||||
from services.notify import notify
|
from services.notify import notify
|
||||||
import state
|
import state
|
||||||
@@ -36,6 +37,8 @@ async def main():
|
|||||||
asyncio.create_task(monitor_resources(cfg, notify, bot, ADMIN_ID))
|
asyncio.create_task(monitor_resources(cfg, notify, bot, ADMIN_ID))
|
||||||
if cfg.get("alerts", {}).get("smart_enabled", True):
|
if cfg.get("alerts", {}).get("smart_enabled", True):
|
||||||
asyncio.create_task(monitor_smart(cfg, notify, bot, ADMIN_ID))
|
asyncio.create_task(monitor_smart(cfg, notify, bot, ADMIN_ID))
|
||||||
|
state.METRICS_STORE = MetricsStore()
|
||||||
|
asyncio.create_task(start_sampler(state.METRICS_STORE, interval=5))
|
||||||
asyncio.create_task(queue_worker())
|
asyncio.create_task(queue_worker())
|
||||||
await notify_start()
|
await notify_start()
|
||||||
await dp.start_polling(bot)
|
await dp.start_polling(bot)
|
||||||
|
|||||||
86
services/metrics.py
Normal file
86
services/metrics.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsStore:
|
||||||
|
def __init__(self, maxlen: int = 720):
|
||||||
|
self.samples = deque(maxlen=maxlen)
|
||||||
|
self.interval = 5
|
||||||
|
|
||||||
|
def add(self, sample: dict):
|
||||||
|
self.samples.append(sample)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_sampler(store: MetricsStore, interval: int = 5):
|
||||||
|
store.interval = interval
|
||||||
|
psutil.cpu_percent(interval=None)
|
||||||
|
last_net = psutil.net_io_counters()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = time.time()
|
||||||
|
cpu = psutil.cpu_percent(interval=None)
|
||||||
|
load1 = psutil.getloadavg()[0]
|
||||||
|
|
||||||
|
net = psutil.net_io_counters()
|
||||||
|
rx_bytes = net.bytes_recv - last_net.bytes_recv
|
||||||
|
tx_bytes = net.bytes_sent - last_net.bytes_sent
|
||||||
|
last_net = net
|
||||||
|
|
||||||
|
rx_rate = rx_bytes / interval
|
||||||
|
tx_rate = tx_bytes / interval
|
||||||
|
|
||||||
|
store.add({
|
||||||
|
"ts": now,
|
||||||
|
"cpu": cpu,
|
||||||
|
"load1": load1,
|
||||||
|
"rx_bytes": rx_bytes,
|
||||||
|
"tx_bytes": tx_bytes,
|
||||||
|
"rx_rate": rx_rate,
|
||||||
|
"tx_rate": tx_rate,
|
||||||
|
})
|
||||||
|
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
|
||||||
|
|
||||||
|
def summarize(store: MetricsStore, minutes: int = 15) -> str:
|
||||||
|
cutoff = time.time() - minutes * 60
|
||||||
|
data = [s for s in list(store.samples) if s["ts"] >= cutoff]
|
||||||
|
if not data:
|
||||||
|
return "📈 Metrics\n\n⚠️ No data yet"
|
||||||
|
|
||||||
|
cpu_vals = [s["cpu"] for s in data]
|
||||||
|
load_vals = [s["load1"] for s in data]
|
||||||
|
rx_rates = [s["rx_rate"] for s in data]
|
||||||
|
tx_rates = [s["tx_rate"] for s in data]
|
||||||
|
|
||||||
|
total_rx = sum(s["rx_bytes"] for s in data)
|
||||||
|
total_tx = sum(s["tx_bytes"] for s in data)
|
||||||
|
|
||||||
|
def avg(vals):
|
||||||
|
return sum(vals) / len(vals) if vals else 0.0
|
||||||
|
|
||||||
|
def fmt_rate(bps):
|
||||||
|
if bps > 1024**2:
|
||||||
|
return f"{bps / (1024**2):.2f} MiB/s"
|
||||||
|
if bps > 1024:
|
||||||
|
return f"{bps / 1024:.2f} KiB/s"
|
||||||
|
return f"{bps:.0f} B/s"
|
||||||
|
|
||||||
|
def fmt_bytes(b):
|
||||||
|
if b > 1024**3:
|
||||||
|
return f"{b / (1024**3):.2f} GiB"
|
||||||
|
if b > 1024**2:
|
||||||
|
return f"{b / (1024**2):.2f} MiB"
|
||||||
|
if b > 1024:
|
||||||
|
return f"{b / 1024:.2f} KiB"
|
||||||
|
return f"{b} B"
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"📈 Metrics (last {minutes}m)\n\n"
|
||||||
|
f"🧠 CPU avg: {avg(cpu_vals):.1f}% | max: {max(cpu_vals):.1f}%\n"
|
||||||
|
f"⚙️ Load avg: {avg(load_vals):.2f} | max: {max(load_vals):.2f}\n"
|
||||||
|
f"⬇️ RX avg: {fmt_rate(avg(rx_rates))} | max: {fmt_rate(max(rx_rates))} | total: {fmt_bytes(total_rx)}\n"
|
||||||
|
f"⬆️ TX avg: {fmt_rate(avg(tx_rates))} | max: {fmt_rate(max(tx_rates))} | total: {fmt_bytes(total_tx)}"
|
||||||
|
)
|
||||||
1
state.py
1
state.py
@@ -5,3 +5,4 @@ LOG_FILTER_PENDING: Dict[int, dict] = {}
|
|||||||
UPDATES_CACHE: Dict[int, dict] = {}
|
UPDATES_CACHE: Dict[int, dict] = {}
|
||||||
ARCANE_CACHE: Dict[int, dict] = {}
|
ARCANE_CACHE: Dict[int, dict] = {}
|
||||||
REBOOT_PENDING: Dict[int, dict] = {}
|
REBOOT_PENDING: Dict[int, dict] = {}
|
||||||
|
METRICS_STORE = None
|
||||||
|
|||||||
Reference in New Issue
Block a user