Files
Nexus/server/background/self_monitor.py
2026-07-08 22:31:31 +08:00

149 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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}"
)