- Add hardcoded repo_url for repair agent to locate git repository - Change default environment from 'production' to 'development' Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
"""
|
||
全局异常捕获中间件
|
||
|
||
两层防线确保异常上报到 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,
|
||
)
|