增加异常处理

This commit is contained in:
aaron 2026-02-22 11:52:14 +08:00
parent dd7eb8d3a1
commit 5a56076858
4 changed files with 679 additions and 6 deletions

View File

@ -10,6 +10,7 @@ from contextlib import asynccontextmanager
from app.config import get_settings
from app.utils.logger import logger
from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals
from app.utils.error_handler import setup_global_exception_handler, init_error_notifier
import os
@ -289,6 +290,23 @@ async def lifespan(app: FastAPI):
# 启动时执行
logger.info("应用启动")
# 初始化全局异常处理器
setup_global_exception_handler()
logger.info("全局异常处理器已安装")
# 初始化飞书错误通知
try:
from app.services.feishu_service import get_feishu_service
feishu_service = get_feishu_service()
init_error_notifier(
feishu_service=feishu_service,
enabled=True, # 启用异常通知
cooldown=300 # 5分钟冷却时间
)
logger.info("✅ 系统异常通知已启用(异常将发送到飞书)")
except Exception as e:
logger.warning(f"飞书异常通知初始化失败: {e}")
# 启动后台任务
settings = get_settings()
if getattr(settings, 'paper_trading_enabled', True):
@ -438,9 +456,19 @@ async def signals_page():
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host=settings.api_host,
port=settings.api_port,
reload=settings.debug
)
# 设置全局异常处理器(防止主线程异常退出)
setup_global_exception_handler()
try:
uvicorn.run(
"app.main:app",
host=settings.api_host,
port=settings.api_port,
reload=settings.debug
)
except Exception as e:
logger.error(f"应用启动失败: {e}")
import traceback
logger.error(traceback.format_exc())
raise

View File

@ -0,0 +1,217 @@
"""
全局异常处理器
捕获系统中所有未处理的异常并发送飞书通知
"""
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("错误通知器初始化完成")

210
docs/ERROR_NOTIFICATION.md Normal file
View File

@ -0,0 +1,210 @@
# 系统异常通知功能使用说明
## 功能概述
系统异常通知功能会自动捕获所有未处理的异常,并发送通知到飞书,让你及时了解系统错误。
## 主要特性
**自动捕获异常** - 捕获系统中所有未处理的异常
**飞书通知** - 自动发送异常详情到飞书
**完整堆栈信息** - 包含异常类型、错误信息和完整堆栈跟踪
**冷却机制** - 避免重复通知默认5分钟冷却时间
**可配置** - 可以启用/禁用通知,调整冷却时间
## 工作原理
### 1. 异常捕获流程
```
系统异常 → 全局异常处理器 → 记录日志 → 检查冷却时间 → 发送飞书通知
```
### 2. 通知内容
飞书通知包含以下信息:
- 🚨 异常类型(如 `ValueError`, `RuntimeError`
- 📝 异常信息(错误描述)
- ⏰ 发生时间
- 📚 完整堆栈跟踪(代码格式化显示)
### 3. 冷却机制
为了避免同一错误重复通知,系统实现了冷却机制:
- 默认冷却时间300秒5分钟
- 在冷却期内的异常只记录日志,不发送飞书通知
- 冷却时间过后再次出现异常才会发送通知
## 配置说明
### 代码配置
异常通知已在 `main.py` 中自动初始化:
```python
# 初始化飞书错误通知
from app.services.feishu_service import get_feishu_service
feishu_service = get_feishu_service()
init_error_notifier(
feishu_service=feishu_service,
enabled=True, # 启用异常通知
cooldown=300 # 5分钟冷却时间
)
```
### 动态调整配置
你可以在运行时动态调整异常通知配置:
```python
from app.utils.error_handler import get_exception_handler
handler = get_exception_handler()
# 启用/禁用异常通知
handler.set_enabled(True) # 启用
handler.set_enabled(False) # 禁用
# 调整冷却时间(秒)
handler.set_cooldown(300) # 5分钟
handler.set_cooldown(60) # 1分钟
handler.set_cooldown(0) # 禁用冷却(每次都通知)
```
## 使用示例
### 示例1: 手动触发异常通知
```python
from app.utils.error_handler import get_exception_handler
handler = get_exception_handler()
# 手动报告异常
try:
# 你的代码
result = 1 / 0
except Exception as e:
import sys
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
```
### 示例2: 在函数中装饰器使用
```python
from functools import wraps
from app.utils.error_handler import get_exception_handler
def notify_on_error(func):
"""异常时发送通知的装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
handler = get_exception_handler()
import sys
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
raise
return wrapper
@notify_on_error
def risky_function():
# 可能出错的代码
pass
```
## 测试
运行测试脚本验证功能:
```bash
source backend/venv/bin/activate
python scripts/test_error_notification.py
```
测试脚本会:
1. 触发各种类型的异常
2. 发送飞书通知
3. 测试冷却机制
4. 验证通知格式
## 飞书通知示例
```
🚨 **系统异常报警**
**异常类型**: ZeroDivisionError
**异常信息**: division by zero
**发生时间**: 2026-02-22 11:50:59
**堆栈跟踪**:
```
Traceback (most recent call last):
File "/app/main.py", line 100, in process_data
result = calculate_ratio(x, y)
File "/app/utils.py", line 50, in calculate_ratio
return x / y
ZeroDivisionError: division by zero
```
⚠️ 请及时处理系统异常
```
## 注意事项
1. **敏感信息**: 异常堆栈可能包含敏感信息如API密钥、密码等请注意飞书安全性
2. **网络依赖**: 发送飞书通知需要网络连接,如果网络异常会只记录日志
3. **性能影响**: 异常处理本身对性能影响很小,但频繁异常可能表示系统有问题
4. **日志级别**: 异常信息会记录到 ERROR 级别日志,可通过日志系统查询
## 故障排查
### 问题1: 没有收到飞书通知
检查项:
- ✅ 飞书 webhook URL 是否正确配置(`.env` 文件中的 `FEISHU_WEBHOOK_URL`
- ✅ `FEISHU_ENABLED=true` 是否设置
- ✅ 网络连接是否正常
- ✅ 是否在冷却期内
- ✅ 查看日志中是否有 "飞书消息发送成功" 或相关错误信息
### 问题2: 通知太频繁
解决方案:
- 增加冷却时间:`handler.set_cooldown(600)` # 10分钟
- 或者临时禁用通知:`handler.set_enabled(False)`
### 问题3: 想临时禁用通知
```python
from app.utils.error_handler import get_exception_handler
handler = get_exception_handler()
handler.set_enabled(False)
```
## 技术实现
异常处理器使用 Python 的 `sys.excepthook` 机制,可以捕获所有未处理的异常:
```python
import sys
def exception_hook(exc_type, exc_value, exc_traceback):
# 处理异常
handler.handle_exception(exc_type, exc_value, exc_traceback)
sys.excepthook = exception_hook
```
这种方式可以捕获:
- 主线程中的所有未处理异常
- 异步任务中的异常(如果未正确捕获)
- 脚本运行时的异常
但不能捕获:
- 已被 try-except 捕获的异常(这是设计行为)
- 子线程中的异常(需要在子线程中单独处理)
- 系统级别的严重错误(如 Segmentation Fault

View File

@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
测试系统异常通知功能
测试场景
1. 测试普通异常通知
2. 测试带堆栈信息的异常通知
3. 测试冷却时间避免重复通知
"""
import sys
from pathlib import Path
# 添加项目路径
backend_dir = Path(__file__).parent.parent / "backend"
sys.path.insert(0, str(backend_dir))
def test_normal_exception():
"""测试普通异常通知"""
print("\n" + "=" * 60)
print("🧪 测试1: 普通异常通知")
print("=" * 60)
from app.utils.error_handler import get_exception_handler
handler = get_exception_handler()
# 模拟一个异常
try:
result = 1 / 0 # ZeroDivisionError
except Exception as e:
import traceback
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
print("✅ 已触发异常处理(请检查飞书是否收到通知)")
def test_custom_exception():
"""测试自定义异常通知"""
print("\n" + "=" * 60)
print("🧪 测试2: 自定义异常通知")
print("=" * 60)
from app.utils.error_handler import get_exception_handler
handler = get_exception_handler()
# 模拟一个自定义异常
try:
raise ValueError("这是一个测试异常,用于验证飞书通知功能")
except Exception as e:
import traceback
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
print("✅ 已触发自定义异常处理(请检查飞书是否收到通知)")
def test_nested_exception():
"""测试嵌套调用异常"""
print("\n" + "=" * 60)
print("🧪 测试3: 嵌套调用异常(带完整堆栈)")
print("=" * 60)
from app.utils.error_handler import get_exception_handler
handler = get_exception_handler()
def level_3():
"""第三层调用"""
raise RuntimeError("深层错误:数据库连接失败")
def level_2():
"""第二层调用"""
level_3()
def level_1():
"""第一层调用"""
level_2()
# 模拟嵌套调用异常
try:
level_1()
except Exception as e:
import traceback
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
print("✅ 已触发嵌套异常处理(请检查飞书是否收到完整堆栈信息)")
def test_cooldown():
"""测试冷却时间"""
print("\n" + "=" * 60)
print("🧪 测试4: 冷却时间(连续触发异常)")
print("=" * 60)
from app.utils.error_handler import get_exception_handler
import time
handler = get_exception_handler()
handler.set_cooldown(10) # 设置10秒冷却时间
print(f"⏰ 冷却时间设置为 10 秒")
# 第一次触发
try:
raise RuntimeError("第一次异常")
except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
print("✅ 第一次异常已处理")
# 立即第二次触发(应该在冷却期内)
print("\n⏳ 立即触发第二次异常(应该在冷却期内)...")
try:
raise RuntimeError("第二次异常")
except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
print("✅ 第二次异常已处理(应该被冷却,不发送飞书通知)")
print("\n⏳ 等待 11 秒后再次触发...")
time.sleep(11)
# 第三次触发(冷却期已过)
try:
raise RuntimeError("第三次异常")
except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
print("✅ 第三次异常已处理(冷却期已过,应该发送飞书通知)")
def test_with_feishu_integration():
"""测试与飞书集成的完整流程"""
print("\n" + "=" * 60)
print("🧪 测试5: 完整集成测试(带飞书服务)")
print("=" * 60)
from app.utils.error_handler import setup_global_exception_handler, get_exception_handler
from app.services.feishu_service import get_feishu_service
# 设置全局异常处理器
setup_global_exception_handler()
print("✅ 全局异常处理器已安装")
# 初始化飞书通知
feishu_service = get_feishu_service()
handler = get_exception_handler()
handler.set_feishu_service(feishu_service)
handler.set_enabled(True)
print("✅ 飞书通知已启用")
# 触发一个测试异常
print("\n🔔 触发测试异常...")
try:
raise Exception("【测试】这是一个集成测试异常,用于验证飞书通知功能是否正常工作")
except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()
handler.handle_exception(exc_type, exc_value, exc_traceback)
print("✅ 测试异常已处理,请检查飞书是否收到通知")
def main():
"""主测试函数"""
print("\n" + "🚀" * 30)
print("系统异常通知功能测试")
print("🚀" * 30)
print("\n⚠️ 注意:以下测试会触发真实的异常,并尝试发送飞书通知")
print("⚠️ 请确保飞书 webhook 已正确配置\n")
try:
# 测试1: 普通异常
test_normal_exception()
# 等待用户确认
input("\n按 Enter 键继续下一个测试...")
# 测试2: 自定义异常
test_custom_exception()
# 等待用户确认
input("\n按 Enter 键继续下一个测试...")
# 测试3: 嵌套异常
test_nested_exception()
# 等待用户确认
input("\n按 Enter 键继续下一个测试...")
# 测试4: 冷却时间
test_cooldown()
# 等待用户确认
input("\n按 Enter 键继续最后一个测试...")
# 测试5: 完整集成
test_with_feishu_integration()
print("\n" + "=" * 60)
print("✅ 所有测试完成!")
print("=" * 60)
print("\n📋 请检查飞书聊天窗口,确认是否收到异常通知")
print("💡 提示:如果未收到通知,请检查:")
print(" 1. 飞书 webhook URL 是否正确")
print(" 2. 网络连接是否正常")
print(" 3. .env 文件中的 FEISHU_ENABLED 是否为 true\n")
except KeyboardInterrupt:
print("\n\n⚠️ 测试被用户中断")
except Exception as e:
print(f"\n\n❌ 测试失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()