""" 全局异常捕获中间件 两层防线确保异常上报到 Log Center: 1. got_request_exception 信号 —— 捕获被 Django convert_exception_to_response 吞掉的异常 (如 CommonMiddleware 的 APPEND_SLASH RuntimeError) 2. ExceptionReportMiddleware 的 try/except —— 兜底捕获穿透所有内层包裹的异常 """ import os import sys import traceback import threading import requests from django.http import JsonResponse from django.core.signals import got_request_exception LOG_CENTER_URL = os.environ.get('LOG_CENTER_URL', 'https://qiyuan-log-center-api.airlabs.art') LOG_CENTER_ENABLED = os.environ.get('LOG_CENTER_ENABLED', 'true').lower() == 'true' def _send_to_log_center(payload): """异步发送日志到 Log Center""" def send_async(): try: requests.post( f"{LOG_CENTER_URL}/api/v1/logs/report", json=payload, timeout=3, ) except Exception: pass thread = threading.Thread(target=send_async) thread.daemon = True thread.start() def _report_exception(exc, request): """构造 payload 并上报异常""" if not LOG_CENTER_ENABLED: return try: tb = traceback.extract_tb(exc.__traceback__) if exc.__traceback__ else [] last_frame = tb[-1] if tb else None payload = { "project_id": "rtc_backend", "environment": os.environ.get('ENVIRONMENT', 'development'), "level": "ERROR", "repo_url": "https://gitea.airlabs.art/zyc/rtc_backend.git", "error": { "type": type(exc).__name__, "message": str(exc), "file_path": last_frame.filename if last_frame else "unknown", "line_number": last_frame.lineno if last_frame else 0, "stack_trace": traceback.format_exception(exc) if exc.__traceback__ else [str(exc)], }, "context": { "url": request.path, "method": request.method, "view": "middleware", }, } _send_to_log_center(payload) except Exception: pass def _on_request_exception(sender, request, **kwargs): """ Django 信号回调:convert_exception_to_response 内部触发。 此时 sys.exc_info() 仍持有完整的异常上下文。 """ exc_info = sys.exc_info() exc = exc_info[1] if exc: _report_exception(exc, request) # 模块加载时注册信号,全局生效 got_request_exception.connect(_on_request_exception) class TrailingSlashMiddleware: """ 为缺少尾部斜杠的 API 请求补全 '/',直接修改 request.path_info, 不做 HTTP 重定向,因此 POST/PUT/PATCH 请求体完好保留。 配合 APPEND_SLASH = False 使用,替代 CommonMiddleware 的重定向逻辑。 必须放在 CommonMiddleware 之前。 """ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if not request.path_info.endswith('/'): request.path_info = request.path_info + '/' return self.get_response(request) class ExceptionReportMiddleware: """ 全局异常捕获中间件,必须放在 MIDDLEWARE 列表的第一个位置。 作为第二道防线:如果异常穿过了所有内层中间件的 convert_exception_to_response 包裹,这里的 try/except 仍会兜底。 """ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): try: response = self.get_response(request) return response except Exception as exc: _report_exception(exc, request) return JsonResponse( {"code": 1, "message": str(exc), "data": None}, status=500, )