feat: memory alerts with top-memory process, top offender in alerts, scheduled status reports

This commit is contained in:
snowy-yang committed 2026-09-12 20:06:31 +08:00
1 parent e61142399a
commit f26f34cb87
8 files changed
+169 -37

No files matched your search

+125 -36
View File
@@ -1,4 +1,4 @@
"""后台高负载告警任务。"""
"""后台高负载告警任务:CPU/内存超阈值告警 + 定时状态推送。"""
import asyncio
import logging
import time
@@ -9,33 +9,43 @@ import templates
log = logging.getLogger("alerter")
def _top1_str(p: dict) -> str:
return f"{p['name']} (PID {p['pid']}) CPU {p['cpu']}% 内存 {p['mem_mb']}MB"
class Alerter:
def __init__(self, ob, cfg: dict):
self.ob = ob
self.cfg = cfg
self.interval = cfg.get("interval", 10)
self.threshold = cfg.get("cpu_threshold", 85.0)
self.consecutive_need = cfg.get("consecutive", 2)
self.cooldown = cfg.get("cooldown", 600)
self.top_n = cfg.get("top_n", 5)
self.groups = cfg.get("groups", []) or []
self.friends = cfg.get("friends", []) or []
self.groups: list = []
self.friends: list = []
self.apply_config(cfg, initial=True)
self._consecutive = 0
self._alerting = False # 当前是否处于高负载状态
self._last_alert_ts = 0.0
self._cpu_hits = 0
self._mem_hits = 0
self._cpu_alerting = False
self._mem_alerting = False
self._cpu_last_ts = 0.0
self._mem_last_ts = 0.0
self._task = None
def apply_config(self, cfg: dict):
def apply_config(self, cfg: dict, initial: bool = False):
"""热重载告警参数(保留告警状态)。"""
self.interval = cfg.get("interval", self.interval)
new_threshold = cfg.get("cpu_threshold", self.threshold)
if new_threshold != self.threshold:
log.info("告警阈值变更: %.0f%% -> %.0f%%", self.threshold, new_threshold)
self.threshold = new_threshold
self.consecutive_need = cfg.get("consecutive", self.consecutive_need)
self.cooldown = cfg.get("cooldown", self.cooldown)
self.top_n = cfg.get("top_n", self.top_n)
if initial:
self.interval = cfg.get("interval", 10)
self.threshold = cfg.get("cpu_threshold", 85.0)
self.consecutive_need = cfg.get("consecutive", 2)
self.cooldown = cfg.get("cooldown", 600)
self.top_n = cfg.get("top_n", 5)
else:
self.interval = cfg.get("interval", self.interval)
new_threshold = cfg.get("cpu_threshold", self.threshold)
if new_threshold != self.threshold:
log.info("CPU告警阈值变更: %.0f%% -> %.0f%%", self.threshold, new_threshold)
self.threshold = new_threshold
self.consecutive_need = cfg.get("consecutive", self.consecutive_need)
self.cooldown = cfg.get("cooldown", self.cooldown)
self.top_n = cfg.get("top_n", self.top_n)
self.memory_threshold = cfg.get("memory_threshold", self.__dict__.get("memory_threshold", 90.0))
self.groups = cfg.get("groups", self.groups) or []
self.friends = cfg.get("friends", self.friends) or []
@@ -47,38 +57,57 @@ class Alerter:
self._task.cancel()
async def _loop(self):
log.info("告警监控已启动: 阈值 %.0f%%, 连续 %d 次, 间隔 %ds, 冷却 %ds",
self.threshold, self.consecutive_need, self.interval, self.cooldown)
log.info("告警监控已启动: CPU阈值 %.0f%%, 内存阈值 %.0f%%, 连续 %d 次, 间隔 %ds, 冷却 %ds",
self.threshold, self.memory_threshold, self.consecutive_need,
self.interval, self.cooldown)
while True:
try:
await asyncio.sleep(self.interval)
if not self.ob.connected or (not self.groups and not self.friends):
continue
cpu = monitor.get_cpu()
await self._check(cpu["percent"])
mem = monitor.get_memory()
await self._check(cpu["percent"], mem["percent"])
except asyncio.CancelledError:
break
except Exception:
log.exception("告警循环出错")
async def _check(self, cpu_percent: float):
async def _check(self, cpu_percent: float, mem_percent: float):
now = time.monotonic()
# CPU 告警
if cpu_percent >= self.threshold:
self._consecutive += 1
if (self._consecutive >= self.consecutive_need
and (not self._alerting or now - self._last_alert_ts >= self.cooldown)):
self._alerting = True
self._last_alert_ts = now
await self._send_alert(cpu_percent)
self._cpu_hits += 1
if (self._cpu_hits >= self.consecutive_need
and (not self._cpu_alerting or now - self._cpu_last_ts >= self.cooldown)):
self._cpu_alerting = True
self._cpu_last_ts = now
await self._send_cpu_alert(cpu_percent)
else:
self._consecutive = 0
if self._alerting:
self._alerting = False
self._cpu_hits = 0
if self._cpu_alerting:
self._cpu_alerting = False
msg = templates.render("recovered", cpu=f"{cpu_percent:.1f}")
log.info(msg)
await self.ob.broadcast(msg, self.groups, self.friends)
async def _send_alert(self, cpu_percent: float):
# 内存告警
if mem_percent >= self.memory_threshold:
self._mem_hits += 1
if (self._mem_hits >= self.consecutive_need
and (not self._mem_alerting or now - self._mem_last_ts >= self.cooldown)):
self._mem_alerting = True
self._mem_last_ts = now
await self._send_mem_alert(mem_percent)
else:
self._mem_hits = 0
if self._mem_alerting:
self._mem_alerting = False
msg = templates.render("recovered_mem", mem=f"{mem_percent:.1f}")
log.info(msg)
await self.ob.broadcast(msg, self.groups, self.friends)
async def _send_cpu_alert(self, cpu_percent: float):
top = monitor.get_top_processes(self.top_n)
cpu = monitor.get_cpu()
load = cpu["load"]
@@ -86,6 +115,7 @@ class Alerter:
f"{i}. {p['name']} (PID {p['pid']}) CPU {p['cpu']}% 内存 {p['mem_mb']}MB"
for i, p in enumerate(top, 1)
)
top1 = top[0] if top else {"name": "无数据", "pid": "-", "cpu": 0, "mem_mb": 0}
msg = templates.render(
"alert",
threshold=f"{self.threshold:.0f}",
@@ -94,6 +124,65 @@ class Alerter:
load=f"{load[0]:.2f} / {load[1]:.2f} / {load[2]:.2f}",
top_n=self.top_n,
top_list=top_list or "无数据",
top1=_top1_str(top1),
)
log.warning("触发高负载告警: %.1f%%", cpu_percent)
log.warning("触发CPU高负载告警: %.1f%%", cpu_percent)
await self.ob.broadcast(msg, self.groups, self.friends)
async def _send_mem_alert(self, mem_percent: float):
m = monitor.get_memory()
top1 = monitor.get_top_memory(1)
top1 = top1[0] if top1 else {"name": "无数据", "pid": "-", "cpu": 0, "mem_mb": 0}
msg = templates.render(
"alert_mem",
threshold=f"{self.memory_threshold:.0f}",
mem=f"{mem_percent:.1f}",
mem_detail=f"{m['used_gb']}/{m['total_gb']}GB (Swap {m['swap_percent']}%)",
top1=_top1_str(top1),
)
log.warning("触发内存高占用告警: %.1f%%", mem_percent)
await self.ob.broadcast(msg, self.groups, self.friends)
class Reporter:
"""定时向配置目标推送状态报告。"""
def __init__(self, ob, cfg: dict):
self.ob = ob
self.enabled = cfg.get("enabled", False)
self.interval_minutes = cfg.get("interval_minutes", 60)
self.groups = cfg.get("groups", []) or []
self.friends = cfg.get("friends", []) or []
self._task = None
def start(self):
if not self.enabled:
log.info("定时报告未启用")
return
self._task = asyncio.get_running_loop().create_task(self._loop())
async def stop(self):
if self._task:
self._task.cancel()
async def _loop(self):
log.info("定时报告已启动: 每 %d 分钟推送一次", self.interval_minutes)
while True:
await asyncio.sleep(self.interval_minutes * 60)
try:
if not self.ob.connected or (not self.groups and not self.friends):
continue
status = bot_status()
msg = templates.render("report", status=status)
await self.ob.broadcast(msg, self.groups, self.friends)
log.info("已推送定时报告")
except asyncio.CancelledError:
break
except Exception:
log.exception("定时报告推送出错")
def bot_status() -> str:
"""状态文本(避免 bot <-> alerter 循环导入,延迟导入 bot)。"""
import bot
return bot.fmt_status(monitor.get_status())