Add system metrics snapshot
This commit is contained in:
@@ -17,7 +17,7 @@ async def help_cmd(msg: Message):
|
||||
"🐳 Docker — управление контейнерами\n"
|
||||
"📦 Backup — restic бэкапы\n"
|
||||
"🧉 Artifacts — критичные образы (Clonezilla, NAND)\n"
|
||||
"⚙️ System — диски, безопасность, URL, reboot\n\n"
|
||||
"⚙️ System — диски, безопасность, URL, metrics, reboot\n\n"
|
||||
"Inline-кнопки используются для выбора контейнеров.",
|
||||
reply_markup=menu_kb,
|
||||
parse_mode="Markdown",
|
||||
|
||||
@@ -9,7 +9,8 @@ from services.http_checks import get_url_checks, check_url
|
||||
from services.queue import enqueue
|
||||
from services.updates import list_updates, apply_updates
|
||||
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")
|
||||
@@ -55,6 +56,16 @@ async def urls(msg: Message):
|
||||
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")
|
||||
async def updates_list(msg: Message):
|
||||
if not is_admin_msg(msg):
|
||||
|
||||
@@ -55,8 +55,9 @@ artifacts_kb = ReplyKeyboardMarkup(
|
||||
system_kb = ReplyKeyboardMarkup(
|
||||
keyboard=[
|
||||
[KeyboardButton(text="💽 Disks"), KeyboardButton(text="🔐 Security")],
|
||||
[KeyboardButton(text="🌐 URLs"), KeyboardButton(text="📦 Updates")],
|
||||
[KeyboardButton(text="⬆️ Upgrade"), KeyboardButton(text="🔄 Reboot")],
|
||||
[KeyboardButton(text="🌐 URLs"), KeyboardButton(text="📈 Metrics")],
|
||||
[KeyboardButton(text="📦 Updates"), KeyboardButton(text="⬆️ Upgrade")],
|
||||
[KeyboardButton(text="🔄 Reboot")],
|
||||
[KeyboardButton(text="⬅️ Назад")],
|
||||
],
|
||||
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 services.docker import discover_containers, docker_watchdog
|
||||
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.notify import notify
|
||||
import state
|
||||
@@ -36,6 +37,8 @@ async def main():
|
||||
asyncio.create_task(monitor_resources(cfg, notify, bot, ADMIN_ID))
|
||||
if cfg.get("alerts", {}).get("smart_enabled", True):
|
||||
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())
|
||||
await notify_start()
|
||||
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)}"
|
||||
)
|
||||
Reference in New Issue
Block a user