180 lines
6.8 KiB
Python
180 lines
6.8 KiB
Python
# ///
|
|
# monitor_service.py
|
|
# 描述:节点状态监控服务,定时检测API和微信状态
|
|
# 作者:AI Generated
|
|
# 创建日期:2026-04-05
|
|
# ///
|
|
|
|
import asyncio
|
|
from datetime import datetime
|
|
from typing import Dict, Any, Optional
|
|
import httpx
|
|
|
|
from config import get_settings, WebhookConfig
|
|
from services.node_manager import node_manager
|
|
from services.log_service import log_service
|
|
from database_service import get_db_service
|
|
|
|
|
|
class MonitorService:
|
|
def __init__(self):
|
|
self._running = False
|
|
self._task: Optional[asyncio.Task] = None
|
|
self._last_status: Dict[str, Dict[str, str]] = {}
|
|
|
|
async def start(self):
|
|
if self._running:
|
|
return
|
|
self._running = True
|
|
self._task = asyncio.create_task(self._monitor_loop())
|
|
|
|
async def stop(self):
|
|
self._running = False
|
|
if self._task:
|
|
self._task.cancel()
|
|
try:
|
|
await self._task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
async def _monitor_loop(self):
|
|
settings = get_settings()
|
|
log_service.info("监控服务启动", "Monitor")
|
|
while self._running:
|
|
try:
|
|
await self._check_all_nodes()
|
|
except Exception as e:
|
|
log_service.error(f"监控循环错误: {e}", "Monitor")
|
|
await asyncio.sleep(settings.monitor.check_interval)
|
|
|
|
async def _check_all_nodes(self):
|
|
settings = get_settings()
|
|
if not settings.monitor.enabled:
|
|
return
|
|
|
|
for node in await node_manager.get_all_nodes():
|
|
await self._check_node(node.node_id)
|
|
|
|
async def _check_node(self, node_id: str):
|
|
node = await node_manager.get_node(node_id)
|
|
if not node:
|
|
return
|
|
|
|
api_status_changed = False
|
|
wechat_status_changed = False
|
|
old_api_status = self._last_status.get(node_id, {}).get("api_status")
|
|
old_wechat_status = self._last_status.get(node_id, {}).get("wechat_status")
|
|
|
|
api_status = await node_manager.check_node_status(node_id)
|
|
if old_api_status is not None and old_api_status != api_status:
|
|
api_status_changed = True
|
|
self._last_status.setdefault(node_id, {})["api_status"] = api_status
|
|
|
|
wechat_status = node.wechat_status
|
|
if old_wechat_status is not None and old_wechat_status != wechat_status:
|
|
wechat_status_changed = True
|
|
self._last_status.setdefault(node_id, {})["wechat_status"] = wechat_status
|
|
|
|
if api_status_changed:
|
|
event = "node_online" if self._last_status[node_id]["api_status"] == "active" else "node_offline"
|
|
msg = f"节点 {node.name} ({node_id}) API状态变为 {self._last_status[node_id]['api_status']}"
|
|
log_service.info(msg, "Monitor")
|
|
if event == "node_offline":
|
|
log_service.error(msg, "Monitor")
|
|
await self._send_webhook(event, {
|
|
"node_id": node_id,
|
|
"node_name": node.name,
|
|
"api_status": self._last_status[node_id]["api_status"],
|
|
"message": msg
|
|
})
|
|
|
|
if wechat_status_changed:
|
|
event = "wechat_online" if wechat_status == "online" else "wechat_offline"
|
|
msg = f"节点 {node.name} ({node_id}) 微信状态变为 {wechat_status}"
|
|
log_service.info(msg, "Monitor")
|
|
if event == "wechat_offline":
|
|
log_service.warning(msg, "Monitor")
|
|
await self._send_webhook(event, {
|
|
"node_id": node_id,
|
|
"node_name": node.name,
|
|
"wechat_status": wechat_status,
|
|
"message": msg
|
|
})
|
|
|
|
async def _send_webhook(self, event: str, data: Dict[str, Any]):
|
|
settings = get_settings()
|
|
webhook_config: WebhookConfig = settings.webhook
|
|
|
|
db_service = get_db_service()
|
|
webhooks = []
|
|
if db_service:
|
|
webhooks = db_service.get_enabled_webhooks()
|
|
|
|
if not webhooks:
|
|
if not webhook_config.enabled or not webhook_config.urls:
|
|
return
|
|
webhooks = [{"url": url, "format": webhook_config.format} for url in webhook_config.urls]
|
|
|
|
event_names = {
|
|
"api_error": "API错误",
|
|
"wechat_offline": "微信掉线",
|
|
"wechat_online": "微信上线",
|
|
"node_offline": "节点离线",
|
|
"node_online": "节点在线"
|
|
}
|
|
|
|
event_name = event_names.get(event, event)
|
|
node_name = data.get('node_name', 'N/A')
|
|
status_info = data.get('wechat_status') or data.get('api_status') or 'N/A'
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
for webhook in webhooks:
|
|
url = webhook.url if hasattr(webhook, 'url') else webhook.get('url')
|
|
format_type = webhook.format if hasattr(webhook, 'format') else webhook.get('format', 'bark')
|
|
event_types = webhook.event_types if hasattr(webhook, 'event_types') else []
|
|
|
|
if event_types and event not in event_types:
|
|
continue
|
|
|
|
if format_type == "bark":
|
|
title = f"WXAuto告警 - {event_name}"
|
|
content = f"{node_name}\n状态: {status_info}\n时间: {timestamp}"
|
|
payload = {
|
|
"title": title,
|
|
"body": content,
|
|
"icon": "https://is3-ssl.mzstatic.com/image/thumb/Purple122/v4/8e/1a/b5/8e1ab5a2-09c7-2c06-ae03-2056b0348d2c/AppIcon-Production-20x20@2x.png/0x0ss.png"
|
|
}
|
|
else:
|
|
content = f"""【WXAuto Center 告警】
|
|
事件类型: {event_name}
|
|
节点名称: {node_name}
|
|
状态信息: {status_info}
|
|
时间: {timestamp}"""
|
|
payload = {
|
|
"msgtype": "text",
|
|
"text": {
|
|
"content": content
|
|
}
|
|
}
|
|
|
|
for attempt in range(webhook_config.retry_times):
|
|
try:
|
|
async with httpx.AsyncClient(timeout=webhook_config.timeout) as client:
|
|
response = await client.post(url, json=payload)
|
|
if response.status_code == 200:
|
|
log_service.success(f"Webhook通知发送成功: {event} -> {url}", "Monitor")
|
|
break
|
|
except Exception as e:
|
|
log_service.warning(f"Webhook发送失败 (尝试 {attempt + 1}): {e}", "Monitor")
|
|
else:
|
|
log_service.error(f"Webhook通知发送失败 (已重试{webhook_config.retry_times}次): {event} -> {url}", "Monitor")
|
|
|
|
def get_last_status(self, node_id: str) -> Dict[str, str]:
|
|
return self._last_status.get(node_id, {})
|
|
|
|
def get_all_status(self) -> Dict[str, Dict[str, str]]:
|
|
return self._last_status.copy()
|
|
|
|
|
|
monitor_service = MonitorService()
|