Files
Nexus/server/background/self_monitor.py
T

149 lines
6.2 KiB
Python
Raw Normal View History

"""Nexus — Python Self-Monitor Background Task
Every 30 seconds: check Redis, MySQL, WebSocket connections.
If critical service fails → send Telegram system alert.
If service recovers → send Telegram recovery notification.
"""
import asyncio
import logging
import time
from sqlalchemy import text
from server.infrastructure.database.session import AsyncSessionLocal
from server.infrastructure.redis.client import get_redis
from server.infrastructure.telegram import send_telegram_system_alert
from server.api.websocket import manager as ws_manager, sync_manager as ws_sync_manager
from server.config import settings
logger = logging.getLogger("nexus.self_monitor")
MONITOR_INTERVAL = 30 # seconds
_TELEGRAM_COOLDOWN = 300 # 5 minutes — same alert won't re-push Telegram
# Track previous state for recovery detection
_prev_redis_ok = True
_prev_mysql_ok = True
# Track last Telegram push time per alert key (prevent spam)
_last_telegram_time: dict = {}
async def self_monitor_loop():
"""Background task: every 30s, self-check critical services.
Checks:
1. Redis connection — if lost, send Telegram warning; if recovered, send recovery
2. MySQL connection — if lost, send Telegram warning; if recovered, send recovery
3. WebSocket client count (via ConnectionManager)
"""
global _prev_redis_ok, _prev_mysql_ok
def _should_send(key: str) -> bool:
"""Rate-limit Telegram pushes to 1 per 5 minutes per alert key."""
now = time.monotonic()
last = _last_telegram_time.get(key, 0)
if now - last < _TELEGRAM_COOLDOWN:
return False
_last_telegram_time[key] = now
return True
logger.info("Self-monitor loop started (interval: 30s)")
while True:
await asyncio.sleep(MONITOR_INTERVAL)
# ── Redis check ──
redis_ok = True
try:
redis = get_redis()
await redis.ping()
except Exception:
redis_ok = False
logger.warning("Redis health check failed", exc_info=True)
try:
if settings.REDIS_URL and _should_send("NOTIFY_SYSTEM_REDIS"):
await send_telegram_system_alert(
summary="Redis 连接失败,无法 ping 通",
title="Redis 连接异常",
notify_key="NOTIFY_SYSTEM_REDIS",
icon="🔴",
details=[
"检测来源:Layer 2 self_monitor30 秒周期)",
"影响范围:子机实时心跳、在线状态、Redis 缓存读写可能中断",
"数据后果:前端仪表盘实时数据可能停滞,历史数据仍可从 MySQL 读取",
],
action="请检查 1Panel Redis 容器状态与 NEXUS_REDIS_URL 配置",
)
except Exception:
logger.error("Failed to send Redis alert via Telegram")
# Redis recovery notification
if redis_ok and not _prev_redis_ok:
try:
await send_telegram_system_alert(
summary="Redis 连接已恢复,ping 检测通过",
title="Redis 连接恢复",
notify_key="NOTIFY_SYSTEM_REDIS",
icon="🟢",
details=[
"检测来源:Layer 2 self_monitor30 秒周期)",
"当前状态:Redis 可正常读写,实时心跳应恢复更新",
],
)
_last_telegram_time.pop("NOTIFY_SYSTEM_REDIS", None) # clear cooldown
except Exception:
logger.error("Failed to send Redis recovery via Telegram")
logger.info("Redis connection recovered")
_prev_redis_ok = redis_ok
# ── MySQL check ──
mysql_ok = True
try:
async with AsyncSessionLocal() as session:
await session.execute(text("SELECT 1"))
except Exception:
mysql_ok = False
logger.warning("MySQL health check failed", exc_info=True)
try:
if _should_send("NOTIFY_SYSTEM_MYSQL"):
await send_telegram_system_alert(
summary="MySQL 连接失败,SELECT 1 探测未通过",
title="MySQL 数据库异常",
notify_key="NOTIFY_SYSTEM_MYSQL",
icon="🔴",
details=[
"检测来源:Layer 2 self_monitor30 秒周期)",
"影响范围:设置、子机列表、审计、推送任务等所有 DB 读写",
"严重程度:平台核心功能可能不可用",
],
action="请检查 1Panel MySQL 容器、磁盘空间与 DATABASE_URL 配置",
)
except Exception:
logger.error("Failed to send MySQL alert via Telegram", exc_info=True)
# MySQL recovery notification
if mysql_ok and not _prev_mysql_ok:
try:
await send_telegram_system_alert(
summary="MySQL 连接已恢复,SELECT 1 探测通过",
title="MySQL 数据库恢复",
notify_key="NOTIFY_SYSTEM_MYSQL",
icon="🟢",
details=[
"检测来源:Layer 2 self_monitor30 秒周期)",
"当前状态:数据库读写应已恢复正常",
],
)
_last_telegram_time.pop("NOTIFY_SYSTEM_MYSQL", None) # clear cooldown
except Exception:
logger.error("Failed to send MySQL recovery via Telegram")
logger.info("MySQL connection recovered")
_prev_mysql_ok = mysql_ok
# ── Log status ──
logger.debug(
f"Self-monitor: redis={'ok' if redis_ok else 'error'}, "
f"mysql={'ok' if mysql_ok else 'error'}, "
f"ws_alert_clients={ws_manager.client_count}, "
f"ws_sync_clients={ws_sync_manager.client_count}"
)