zyc 060d3a726f
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m16s
fix bug report
2026-03-13 17:17:01 +08:00

149 lines
5.8 KiB
Python

"""Log Center integration — runtime error reporting + exception middleware."""
import os
import json
import logging
import traceback
import threading
from urllib.request import Request, urlopen
from urllib.error import URLError
LOG_CENTER_URL = os.getenv('LOG_CENTER_URL', 'https://qiyuan-log-center-api.airlabs.art')
LOG_CENTER_ENABLED = os.getenv('LOG_CENTER_ENABLED', 'false').lower() in ('true', '1', 'yes')
PROJECT_ID = 'video_backend'
ENVIRONMENT = os.getenv('ENVIRONMENT', 'development')
logger = logging.getLogger('log_center')
def report_error(exc, context=None):
"""Report a runtime error to Log Center (async, non-blocking)."""
if not LOG_CENTER_ENABLED:
return
tb = traceback.extract_tb(exc.__traceback__)
last_frame = tb[-1] if tb else None
payload = {
'project_id': PROJECT_ID,
'environment': ENVIRONMENT,
'level': 'ERROR',
'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': ''.join(traceback.format_exception(exc)),
},
'context': context or {},
}
# Fire-and-forget in background thread
threading.Thread(target=_send, args=(payload,), daemon=True).start()
def _send(payload):
"""Send payload to Log Center API."""
try:
data = json.dumps(payload).encode('utf-8')
req = Request(
f'{LOG_CENTER_URL}/api/v1/logs/report',
data=data,
headers={'Content-Type': 'application/json'},
method='POST',
)
urlopen(req, timeout=3)
except (URLError, Exception):
pass # Silent failure — never affect main business
# ──────────────────────────────────────────────
# Django Middleware: 捕获 DRF 之外的所有异常
# (模块导入失败、中间件错误、URL 解析错误等)
# ──────────────────────────────────────────────
class LogCenterMiddleware:
"""Django middleware to catch ALL unhandled exceptions and report to Log Center.
DRF 的 custom_exception_handler 只能捕获视图内的异常。
模块导入失败、中间件异常、URL 解析错误等发生在 DRF 之前,
不会经过 DRF 的异常处理,需要这个中间件来兜底上报。
异常捕获分工:
- DRF 视图异常 → custom_exception_handler 上报 (DRF 内部 try/except 拦截,不会传播到中间件)
- 非 DRF 视图异常 → process_exception 上报
- 中间件/导入异常 → __call__ try/except 上报
两层互不重复。
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
response = self.get_response(request)
except Exception as exc:
# 捕获中间件链中的异常(如其他中间件报错、模块导入失败等)
logger.error('Unhandled middleware exception: %s', exc, exc_info=True)
report_error(exc, {
'source': 'django_middleware',
'request_path': getattr(request, 'path', 'unknown'),
'request_method': getattr(request, 'method', 'unknown'),
'user': str(getattr(request, 'user', 'anonymous')),
})
raise
return response
def process_exception(self, request, exception):
"""捕获非 DRF 视图的异常(普通 Django 视图、URL 解析错误等)。"""
logger.error('Unhandled view exception: %s', exception, exc_info=True)
report_error(exception, {
'source': 'django_process_exception',
'request_path': request.path,
'request_method': request.method,
'user': str(getattr(request, 'user', 'anonymous')),
})
return None # 不处理响应,交给 Django 默认 500 handler
# ──────────────────────────────────────────────
# DRF Exception Handler: 捕获视图内异常 + 返回 JSON
# ──────────────────────────────────────────────
def custom_exception_handler(exc, context):
"""DRF custom exception handler with Log Center reporting."""
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status
from rest_framework.exceptions import (
MethodNotAllowed, NotAuthenticated, AuthenticationFailed,
NotFound, PermissionDenied,
)
# Only report unexpected errors, skip normal HTTP client errors
if not isinstance(exc, (MethodNotAllowed, NotAuthenticated,
AuthenticationFailed, NotFound,
PermissionDenied)):
request = context.get('request')
report_error(exc, {
'source': 'drf_exception_handler',
'view': str(context.get('view', '')),
'request_path': getattr(request, 'path', None),
'request_method': getattr(request, 'method', None),
'user': str(getattr(request, 'user', 'anonymous')),
})
# Default DRF handling
response = exception_handler(exc, context)
# If DRF didn't handle it (unhandled exception), return JSON 500
# instead of letting Django render an HTML error page
if response is None:
logger.error('Unhandled DRF exception: %s', exc, exc_info=True)
response = Response(
{'error': f'{type(exc).__name__}: {exc}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return response