from __future__ import annotations import logging import traceback from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.services.operation_log_service import build_exception_detail, log_operation_event DOMAIN = "upload_resource" logger = logging.getLogger("videogen.upload_resource") def _exception_payload(exc: BaseException | None, detail: dict[str, Any] | None = None) -> dict[str, Any]: payload = dict(detail or {}) if exc is not None: payload.setdefault("error_type", exc.__class__.__name__) payload.setdefault("error_message", str(exc)) payload.setdefault("traceback", "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))) return payload def log_upload_resource_event( *, event_type: str, module: str | None = None, user_id: str | None = None, resource_id: str | None = None, source_model: str | None = None, source_id: str | None = None, message: str | None = None, detail: dict[str, Any] | None = None, error: str | None = None, exc: BaseException | None = None, event_status: str = "success", ) -> None: """Write an upload-resource operation log. 日志失败不能影响主业务流程;失败时降级到标准 logger。 detail 只接收普通 dict,禁止传 ORM 对象,避免 commit/rollback 后懒加载异常。 """ payload = dict(detail or {}) if resource_id: payload["resource_id"] = resource_id if source_model: payload["source_model"] = source_model if source_id: payload["source_id"] = source_id if exc is not None: payload = build_exception_detail(exc, payload) error = error or str(exc) event_status = "failed" try: log_operation_event( domain=DOMAIN, event_type=event_type, module=module or DOMAIN, event_status=event_status, source="service", user_id=user_id, message=message, detail=payload, error=error, ) except Exception: # noqa: BLE001 - 日志降级,不能影响主流程 logger.exception( "upload_resource operation log failed: event_type=%s user_id=%s resource_id=%s detail=%s error=%s", event_type, user_id, resource_id, payload, error, ) def log_upload_resource_exception( *, event_type: str, message: str | None = None, user_id: str | None = None, resource_id: str | None = None, resource_ids: list[str] | None = None, module: str | None = None, source_model: str | None = None, source_id: str | None = None, detail: dict[str, Any] | None = None, exc: BaseException | None = None, ) -> None: """统一记录 UploadResource 相关异常。""" payload = _exception_payload(exc, detail) if resource_ids is not None: payload["resource_ids"] = list(resource_ids) log_upload_resource_event( event_type=event_type, module=module, user_id=user_id, resource_id=resource_id, source_model=source_model, source_id=source_id, message=message, detail=payload, error=str(exc) if exc else None, event_status="failed", ) async def safe_rollback_with_log( db: AsyncSession, *, event_type: str, message: str | None = None, user_id: str | None = None, module: str | None = None, detail: dict[str, Any] | None = None, original_exc: BaseException | None = None, ) -> None: """Rollback with rollback-failure logging. 只记录 rollback 自身异常;不吞掉主异常,调用方继续 raise 原始异常。 """ try: await db.rollback() except Exception as rollback_exc: # noqa: BLE001 payload = dict(detail or {}) if original_exc is not None: payload["original_error_type"] = original_exc.__class__.__name__ payload["original_error_message"] = str(original_exc) log_upload_resource_exception( event_type=event_type, message=message or "数据库回滚失败", user_id=user_id, module=module, detail=payload, exc=rollback_exc, )