"""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