Add system metrics snapshot

This commit is contained in:
2026-02-08 00:02:34 +03:00
parent 5f3c9184b1
commit d3572c6005
6 changed files with 106 additions and 4 deletions

View File

@@ -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",

View File

@@ -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):

View File

@@ -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,

View File

@@ -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
View 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)}"
)

View File

@@ -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