stock-ai-agent/backend/app/utils/error_handler.py
2026-02-27 09:54:17 +08:00

280 lines
8.9 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""
全局异常处理器
捕获系统中所有未处理的异常,并发送飞书通知
"""
import sys
import traceback
from datetime import datetime
from typing import Optional
from threading import Lock
from app.utils.logger import logger
class GlobalExceptionHandler:
"""全局异常处理器"""
_instance = None
_lock = Lock()
_initialized = False
def __new__(cls):
"""单例模式"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
"""初始化异常处理器"""
if GlobalExceptionHandler._initialized:
return
GlobalExceptionHandler._initialized = True
self.feishu_service = None
self.enabled = True
self.last_error_time = None
self.error_cooldown = 300 # 错误通知冷却时间(秒),避免重复通知
logger.info("全局异常处理器初始化完成")
def set_feishu_service(self, feishu_service):
"""设置飞书服务"""
self.feishu_service = feishu_service
logger.info("异常处理器已连接飞书服务")
def set_enabled(self, enabled: bool):
"""启用或禁用异常通知"""
self.enabled = enabled
logger.info(f"异常通知已{'启用' if enabled else '禁用'}")
def set_cooldown(self, seconds: int):
"""设置错误通知冷却时间"""
self.error_cooldown = seconds
logger.info(f"错误通知冷却时间已设置为 {seconds}")
def handle_exception(self, exc_type, exc_value, exc_traceback):
"""
处理异常
Args:
exc_type: 异常类型
exc_value: 异常值
exc_traceback: 异常堆栈
"""
# 检查是否是键盘中断(用户主动退出)
if exc_type == KeyboardInterrupt:
logger.info("用户主动中断程序")
return
# 检查是否启用
if not self.enabled:
logger.warning("异常通知已禁用,仅记录日志")
self._log_exception(exc_type, exc_value, exc_traceback)
return
# 检查冷却时间
if self.last_error_time:
time_since_last = (datetime.now() - self.last_error_time).total_seconds()
if time_since_last < self.error_cooldown:
logger.warning(f"错误通知冷却中(剩余 {int(self.error_cooldown - time_since_last)} 秒)")
self._log_exception(exc_type, exc_value, exc_traceback)
return
# 记录异常
self._log_exception(exc_type, exc_value, exc_traceback)
# 发送飞书通知
self._send_error_notification(exc_type, exc_value, exc_traceback)
# 更新最后错误时间
self.last_error_time = datetime.now()
def _log_exception(self, exc_type, exc_value, exc_traceback):
"""记录异常到日志"""
logger.error("=" * 60)
logger.error("❌ 未捕获的异常")
logger.error("=" * 60)
logger.error(f"异常类型: {exc_type.__name__}")
logger.error(f"异常信息: {str(exc_value)}")
logger.error(f"发生时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 打印完整的堆栈跟踪
tb_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
logger.error("堆栈跟踪:\n" + "".join(tb_lines))
logger.error("=" * 60)
def _send_error_notification(self, exc_type, exc_value, exc_traceback):
"""发送错误通知到飞书"""
if not self.feishu_service:
logger.warning("飞书服务未设置,无法发送错误通知")
return
try:
# 获取异常信息
exc_name = exc_type.__name__
exc_msg = str(exc_value)
exc_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 获取堆栈跟踪(限制长度)
tb_list = traceback.format_exception(exc_type, exc_value, exc_traceback)
# 构建堆栈信息(限制长度,避免超出飞书限制)
stack_trace = "".join(tb_list)
# 限制堆栈信息长度(飞书有长度限制)
max_length = 3000
if len(stack_trace) > max_length:
stack_trace = stack_trace[:max_length] + "\n... (堆栈信息过长,已截断)"
# 格式化堆栈信息,使用代码块
formatted_stack = "```\n" + stack_trace + "\n```"
# 构建飞书消息
message = f"""🚨 **系统异常报警**
**异常类型**: {exc_name}
**异常信息**: {exc_msg}
**发生时间**: {exc_time}
**堆栈跟踪**:
{formatted_stack}
⚠️ 请及时处理系统异常"""
# 发送飞书通知
import asyncio
try:
# 获取当前事件循环
loop = asyncio.get_event_loop()
if loop.is_running():
# 如果事件循环正在运行,使用 run_coroutine_threadsafe
asyncio.run_coroutine_threadsafe(
self.feishu_service.send_text(message),
loop
)
else:
# 如果事件循环未运行,直接运行
asyncio.run(self.feishu_service.send_text(message))
logger.info("✅ 已发送异常通知到飞书")
except RuntimeError:
# 没有事件循环,创建新的
asyncio.run(self.feishu_service.send_text(message))
logger.info("✅ 已发送异常通知到飞书")
except Exception as e:
logger.error(f"发送异常通知失败: {e}")
# 创建全局异常处理器实例
_exception_handler: Optional[GlobalExceptionHandler] = None
def get_exception_handler() -> GlobalExceptionHandler:
"""获取全局异常处理器实例"""
global _exception_handler
if _exception_handler is None:
_exception_handler = GlobalExceptionHandler()
return _exception_handler
def setup_global_exception_handler():
"""
设置全局异常处理器
捕获所有未处理的异常,并发送飞书通知
"""
handler = get_exception_handler()
def handle_exception(exc_type, exc_value, exc_traceback):
"""异常处理回调函数"""
handler.handle_exception(exc_type, exc_value, exc_traceback)
# 设置全局异常钩子
sys.excepthook = handle_exception
logger.info("✅ 全局异常处理器已安装")
def init_error_notifier(feishu_service, enabled: bool = True, cooldown: int = 300):
"""
初始化错误通知器
Args:
feishu_service: 飞书服务实例
enabled: 是否启用异常通知
cooldown: 错误通知冷却时间(秒)
"""
handler = get_exception_handler()
handler.set_feishu_service(feishu_service)
handler.set_enabled(enabled)
handler.set_cooldown(cooldown)
logger.info("错误通知器初始化完成")
def notify_error(title: str, message: str, level: str = "error"):
"""
手动触发错误通知(用于已捕获但需要通知的错误)
Args:
title: 错误标题
message: 错误消息
level: 错误级别 (error, warning, info)
"""
handler = get_exception_handler()
if not handler.feishu_service or not handler.enabled:
logger.debug(f"错误通知未启用或飞书服务未设置: {title}")
return
# 检查冷却时间
if handler.last_error_time:
time_since_last = (datetime.now() - handler.last_error_time).total_seconds()
if time_since_last < handler.error_cooldown:
logger.debug(f"错误通知冷却中,跳过: {title}")
return
try:
# 根据级别选择图标
icons = {
"error": "🚨",
"warning": "⚠️",
"info": ""
}
icon = icons.get(level, "📌")
# 构建消息
formatted_message = f"""{icon} **{title}**
{message}
**时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"""
# 发送飞书通知
import asyncio
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.run_coroutine_threadsafe(
handler.feishu_service.send_text(formatted_message),
loop
)
else:
asyncio.run(handler.feishu_service.send_text(formatted_message))
logger.info(f"✅ 已发送错误通知: {title}")
handler.last_error_time = datetime.now()
except RuntimeError:
asyncio.run(handler.feishu_service.send_text(formatted_message))
logger.info(f"✅ 已发送错误通知: {title}")
handler.last_error_time = datetime.now()
except Exception as e:
logger.error(f"发送手动错误通知失败: {e}")