deepdata: core product code (backend, frontend, runtime, deepflow module)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# app/core/__init__.py
|
||||
|
||||
"""
|
||||
Core module
|
||||
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,120 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import model_validator
|
||||
from typing import Optional
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用程序配置"""
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
extra = 'ignore'
|
||||
|
||||
# Service
|
||||
app_name: str = "DeepData Python Backend"
|
||||
app_version: str = "1.0.0"
|
||||
app_description: str = "Adapter for integrating Data Management System with Label Studio"
|
||||
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 18000
|
||||
|
||||
# CORS
|
||||
# allowed_origins: List[str] = ["*"]
|
||||
# allowed_methods: List[str] = ["*"]
|
||||
# allowed_headers: List[str] = ["*"]
|
||||
|
||||
# Log
|
||||
log_level: str = "INFO"
|
||||
debug: bool = True
|
||||
log_file_dir: str = "/var/log/deepdata/backend-python"
|
||||
log_pvc_monitor_enabled: bool = True
|
||||
log_pvc_monitor_path: str = "/var/log/deepdata"
|
||||
log_pvc_monitor_threshold: float = 0.9
|
||||
log_pvc_monitor_interval_seconds: int = 300
|
||||
log_pvc_monitor_delete_batch_size: int = 10
|
||||
log_pvc_monitor_file_suffixes: str = "log,out,err"
|
||||
log_rotation_max_size: str = "100MB"
|
||||
log_rotation_backup_count: int = 30
|
||||
rag_storage_dir: str = "/data/rag_storage"
|
||||
|
||||
# Database
|
||||
pgsql_host: str = "deepdata-database"
|
||||
pgsql_port: int = 5432
|
||||
pgsql_user: str = "postgres"
|
||||
pgsql_password: str = ""
|
||||
pgsql_database: str = "deepdata"
|
||||
|
||||
# Database
|
||||
mysql_host: str = "deepdata-database"
|
||||
mysql_port: int = 3306
|
||||
mysql_user: str = "root"
|
||||
mysql_password: str = ""
|
||||
mysql_database: str = "deepdata"
|
||||
|
||||
database_url: str = "" # Will be overridden by build_database_url() if not provided
|
||||
|
||||
@model_validator(mode='after')
|
||||
def build_database_url(self):
|
||||
"""如果没有提供 database_url,则根据 MySQL 配置构建"""
|
||||
if not self.database_url:
|
||||
if self.pgsql_host:
|
||||
if self.pgsql_password and self.pgsql_user:
|
||||
self.database_url = f"postgresql+asyncpg://{self.pgsql_user}:{self.pgsql_password}@{self.pgsql_host}:{self.pgsql_port}/{self.pgsql_database}"
|
||||
else:
|
||||
self.database_url = f"postgresql+asyncpg://{self.pgsql_host}:{self.pgsql_port}/{self.pgsql_database}"
|
||||
elif self.mysql_password and self.mysql_user:
|
||||
self.database_url = f"mysql+aiomysql://{self.mysql_user}:{self.mysql_password}@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
|
||||
else:
|
||||
self.database_url = f"mysql+aiomysql://{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
|
||||
return self
|
||||
|
||||
|
||||
# Label Studio
|
||||
label_studio_base_url: str = "http://label-studio:8000"
|
||||
label_studio_username: Optional[str] = None
|
||||
label_studio_password: Optional[str] = None
|
||||
label_studio_user_token: Optional[str] = None # Legacy Token
|
||||
|
||||
label_studio_local_document_root: str = "/label-studio/local" # Label Studio local file storage path
|
||||
label_studio_file_path_prefix: str = "/data/local-files/?d=" # Label Studio local file serving URL prefix
|
||||
|
||||
ls_task_page_size: int = 1000
|
||||
|
||||
# DeepData
|
||||
dm_file_path_prefix: str = "/dataset" # DM存储文件夹前缀
|
||||
|
||||
deepdata_jwt_enable: bool = False
|
||||
|
||||
# Milvus 配置
|
||||
milvus_uri: str = "http://milvus:19530"
|
||||
milvus_token: str = ""
|
||||
|
||||
# 文件存储配置(共享文件系统)
|
||||
file_storage_path: str = "/data/files"
|
||||
|
||||
# ==================== 配比任务并行复制配置 ====================
|
||||
# 动态并发计算参数(全闪存储高性能场景默认值)
|
||||
|
||||
# 并发下限(最少并发数)
|
||||
ratio_copy_min_concurrent: int = 8
|
||||
|
||||
# 并发上限(最多并发数,防止资源耗尽)
|
||||
ratio_copy_max_concurrent: int = 128
|
||||
|
||||
# CPU核心系数(每个核心贡献的并发数,全闪存储建议4.0)
|
||||
ratio_copy_cpu_factor: float = 4.0
|
||||
|
||||
# 每并发任务预估内存占用(MB)
|
||||
ratio_copy_memory_per_task_mb: int = 32
|
||||
|
||||
# 内存安全保留比例(保留给其他进程)
|
||||
ratio_copy_memory_reserve_ratio: float = 0.2
|
||||
|
||||
# 是否启用动态计算(False则使用固定值)
|
||||
ratio_copy_dynamic_concurrent: bool = True
|
||||
|
||||
# 固定并发数(当 dynamic_concurrent=False 时使用)
|
||||
ratio_copy_fixed_concurrent: int = 10
|
||||
|
||||
# 全局设置实例
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
核心异常处理模块
|
||||
|
||||
为应用程序提供统一、优雅的异常处理系统,确保所有 API 响应都符合标准化格式。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 注册异常处理器(在 main.py 中)
|
||||
|
||||
```python
|
||||
from app.core.exception import register_exception_handlers, ExceptionHandlingMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# 注册全局异常捕获中间件(最外层)
|
||||
app.add_middleware(ExceptionHandlingMiddleware)
|
||||
|
||||
# 注册异常处理器
|
||||
register_exception_handlers(app)
|
||||
```
|
||||
|
||||
### 2. 在代码中抛出业务异常
|
||||
|
||||
```python
|
||||
from app.core.exception import ErrorCodes, BusinessError
|
||||
|
||||
# 资源不存在
|
||||
async def get_user(user_id: str):
|
||||
user = await db.get_user(user_id)
|
||||
if not user:
|
||||
raise BusinessError(ErrorCodes.NOT_FOUND)
|
||||
return user
|
||||
|
||||
# 参数验证失败
|
||||
async def create_user(name: str):
|
||||
if not name:
|
||||
raise BusinessError(ErrorCodes.BAD_REQUEST)
|
||||
# ...
|
||||
```
|
||||
|
||||
### 3. 返回成功响应
|
||||
|
||||
```python
|
||||
from app.core.exception import SuccessResponse
|
||||
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user(user_id: str):
|
||||
user = await service.get_user(user_id)
|
||||
return SuccessResponse(data=user)
|
||||
# 返回格式: {"code": "0", "message": "success", "data": {...}}
|
||||
```
|
||||
|
||||
### 4. 使用事务管理
|
||||
|
||||
```python
|
||||
from app.core.exception import transaction, BusinessError, ErrorCodes
|
||||
|
||||
@router.post("/users")
|
||||
async def create_user(request: CreateUserRequest, db: AsyncSession = Depends(get_db)):
|
||||
async with transaction(db):
|
||||
# 所有数据库操作
|
||||
user = User(name=request.name)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
# 如果抛出异常,事务会自动回滚
|
||||
if check_duplicate(user.name):
|
||||
raise BusinessError(ErrorCodes.OPERATION_FAILED)
|
||||
|
||||
# 事务已自动提交
|
||||
return SuccessResponse(data=user)
|
||||
```
|
||||
|
||||
## 异常类型说明
|
||||
|
||||
### BusinessError(业务异常)
|
||||
用于预期的业务错误,如资源不存在、权限不足、参数错误等。
|
||||
|
||||
特点:
|
||||
- 不会记录完整的堆栈跟踪(因为这是预期内的错误)
|
||||
- 返回对应的 HTTP 状态码(400、404 等)
|
||||
- 客户端会收到标准化的错误响应
|
||||
|
||||
使用场景:
|
||||
- 资源不存在
|
||||
- 参数验证失败
|
||||
- 权限不足
|
||||
- 业务规则违反
|
||||
|
||||
### SystemError(系统异常)
|
||||
用于意外的系统错误,如数据库错误、网络错误、配置错误等。
|
||||
|
||||
特点:
|
||||
- 记录完整的堆栈跟踪
|
||||
- 返回 HTTP 500
|
||||
- 不暴露敏感的系统信息给客户端
|
||||
|
||||
使用场景:
|
||||
- 数据库连接失败
|
||||
- 网络超时
|
||||
- 配置错误
|
||||
- 编程错误
|
||||
|
||||
## 错误码定义
|
||||
|
||||
所有错误码在 `ErrorCodes` 类中集中定义,遵循规范:`{module}.{sequence}`
|
||||
|
||||
### 通用错误码
|
||||
- `SUCCESS` (0): 操作成功
|
||||
- `BAD_REQUEST` (common.0001): 请求参数错误
|
||||
- `NOT_FOUND` (common.0002): 资源不存在
|
||||
- `FORBIDDEN` (common.0003): 权限不足
|
||||
- `UNAUTHORIZED` (common.0004): 未授权访问
|
||||
- `VALIDATION_ERROR` (common.0005): 数据验证失败
|
||||
|
||||
### 系统级错误码
|
||||
- `INTERNAL_ERROR` (system.0001): 服务器内部错误
|
||||
- `DATABASE_ERROR` (system.0002): 数据库错误
|
||||
- `NETWORK_ERROR` (system.0003): 网络错误
|
||||
|
||||
### 模块错误码
|
||||
- `annotation.*`: 标注模块相关错误
|
||||
- `collection.*`: 归集模块相关错误
|
||||
- `evaluation.*`: 评估模块相关错误
|
||||
- `generation.*`: 生成模块相关错误
|
||||
- `rag.*`: RAG 模块相关错误
|
||||
- `ratio.*`: 配比模块相关错误
|
||||
|
||||
## Result 类型(可选的函数式错误处理)
|
||||
|
||||
如果你不喜欢使用异常,可以使用 Result 类型进行函数式错误处理:
|
||||
|
||||
```python
|
||||
from app.core.exception import Result, Ok, Err, ErrorCodes
|
||||
|
||||
def get_user(user_id: str) -> Result[User]:
|
||||
user = db.find_user(user_id)
|
||||
if user:
|
||||
return Ok(user)
|
||||
return Err(ErrorCodes.NOT_FOUND)
|
||||
|
||||
# 使用结果
|
||||
result = get_user("123")
|
||||
if result.is_ok():
|
||||
user = result.unwrap()
|
||||
print(f"User: {user.name}")
|
||||
else:
|
||||
error = result.unwrap_err()
|
||||
print(f"Error: {error.message}")
|
||||
|
||||
# 链式操作
|
||||
result = get_user("123")
|
||||
.map(lambda user: user.name)
|
||||
.and_then(validate_name)
|
||||
```
|
||||
|
||||
## 响应格式
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"code": "0",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 123,
|
||||
"name": "张三"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": "common.0002",
|
||||
"message": "资源不存在",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **始终使用业务异常**:对于可预见的业务错误,使用 `BusinessError` 而不是 HTTPException
|
||||
2. **集中定义错误码**:所有错误码在 `ErrorCodes` 中定义,不要硬编码
|
||||
3. **提供有用的数据**:在抛出异常时,可以通过 `data` 参数传递额外的错误信息
|
||||
4. **使用事务管理**:涉及多个数据库操作时,使用 `transaction` 上下文管理器
|
||||
5. **不要捕获 SystemError**:让系统错误由全局处理器统一处理
|
||||
|
||||
## 迁移指南
|
||||
|
||||
如果你有旧的异常处理代码,迁移步骤:
|
||||
|
||||
1. 删除旧的异常类定义
|
||||
2. 将 `raise OldException(...)` 替换为 `raise BusinessError(ErrorCodes.XXX)`
|
||||
3. 移除 try-except 中的异常转换逻辑,让全局处理器处理
|
||||
4. 更新导入语句:`from app.core.exception import ErrorCodes, BusinessError`
|
||||
|
||||
## 测试
|
||||
|
||||
可以使用测试端点验证异常处理:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/test-success
|
||||
curl http://localhost:8000/test-business-error
|
||||
curl http://localhost:8000/test-system-error
|
||||
```
|
||||
"""
|
||||
|
||||
from .base import BaseError, ErrorCode, SystemError, BusinessError
|
||||
from .codes import ErrorCodes
|
||||
from .handlers import (
|
||||
register_exception_handlers,
|
||||
ErrorResponse,
|
||||
SuccessResponse
|
||||
)
|
||||
from .middleware import ExceptionHandlingMiddleware
|
||||
from .result import Result, Ok, Err
|
||||
from .transaction import transaction, ensure_transaction_rollback
|
||||
|
||||
__all__ = [
|
||||
# 基础异常类
|
||||
'BaseError',
|
||||
'ErrorCode',
|
||||
'SystemError',
|
||||
'BusinessError',
|
||||
# 错误码
|
||||
'ErrorCodes',
|
||||
# 处理器
|
||||
'register_exception_handlers',
|
||||
'ErrorResponse',
|
||||
'SuccessResponse',
|
||||
# 中间件
|
||||
'ExceptionHandlingMiddleware',
|
||||
# Result 类型
|
||||
'Result',
|
||||
'Ok',
|
||||
'Err',
|
||||
# 事务管理
|
||||
'transaction',
|
||||
'ensure_transaction_rollback',
|
||||
]
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
基础异常类和错误码定义
|
||||
"""
|
||||
from typing import Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ErrorCode:
|
||||
"""
|
||||
不可变的错误码定义
|
||||
|
||||
属性:
|
||||
code: 错误码字符串(如 "annotation.0001")
|
||||
message: 人类可读的错误消息
|
||||
http_status: HTTP状态码(业务错误默认为400)
|
||||
"""
|
||||
code: str
|
||||
message: str
|
||||
http_status: int = 400
|
||||
|
||||
def __post_init__(self):
|
||||
"""验证错误码格式"""
|
||||
if not isinstance(self.code, str):
|
||||
raise ValueError(f"错误码必须是字符串,实际类型: {type(self.code)}")
|
||||
if not self.code:
|
||||
raise ValueError("错误码不能为空")
|
||||
|
||||
|
||||
class BaseError(Exception):
|
||||
"""
|
||||
所有应用异常的基类
|
||||
|
||||
所有自定义异常都应该继承此类。
|
||||
提供自动的错误码和消息处理。
|
||||
|
||||
使用示例:
|
||||
raise BusinessError(ErrorCodes.TASK_NOT_FOUND)
|
||||
|
||||
客户端将收到:
|
||||
{
|
||||
"code": "annotation.0001",
|
||||
"message": "任务不存在",
|
||||
"data": null
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
error_code: ErrorCode,
|
||||
data: Any = None,
|
||||
*args: Any
|
||||
):
|
||||
"""
|
||||
使用错误码和可选数据初始化异常
|
||||
|
||||
Args:
|
||||
error_code: ErrorCode 实例
|
||||
data: 附加错误数据(会包含在响应中)
|
||||
*args: 额外参数(用于兼容性)
|
||||
"""
|
||||
self.error_code = error_code
|
||||
self.data = data
|
||||
super().__init__(error_code.message, *args)
|
||||
|
||||
@property
|
||||
def code(self) -> str:
|
||||
"""获取错误码字符串"""
|
||||
return self.error_code.code
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
"""获取错误消息"""
|
||||
return self.error_code.message
|
||||
|
||||
@property
|
||||
def http_status(self) -> int:
|
||||
"""获取HTTP状态码"""
|
||||
return self.error_code.http_status
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""将异常转换为响应字典"""
|
||||
return {
|
||||
"code": self.code,
|
||||
"message": self.message,
|
||||
"data": self.data
|
||||
}
|
||||
|
||||
|
||||
class SystemError(BaseError):
|
||||
"""
|
||||
系统级异常(意外错误)
|
||||
|
||||
用于:
|
||||
- 数据库错误
|
||||
- 网络错误
|
||||
- 配置错误
|
||||
- 编程错误(bug)
|
||||
|
||||
系统错误会记录完整的堆栈跟踪
|
||||
"""
|
||||
|
||||
def __init__(self, error_code: ErrorCode, data: Any = None):
|
||||
super().__init__(error_code, data)
|
||||
|
||||
|
||||
class BusinessError(BaseError):
|
||||
"""
|
||||
业务逻辑异常(预期内的错误)
|
||||
|
||||
用于:
|
||||
- 验证失败
|
||||
- 资源不存在
|
||||
- 权限不足
|
||||
- 业务规则违反
|
||||
|
||||
业务错误不记录堆栈跟踪(因为它们是预期的)
|
||||
"""
|
||||
|
||||
def __init__(self, error_code: ErrorCode, data: Any = None):
|
||||
super().__init__(error_code, data)
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
集中式错误码定义
|
||||
|
||||
所有错误码都在这里定义,遵循规范:{module}.{sequence}
|
||||
|
||||
模块代码:
|
||||
- common: 通用错误
|
||||
- system: 系统级错误
|
||||
- annotation: 标注模块
|
||||
- collection: 归集模块
|
||||
- evaluation: 评估模块
|
||||
- generation: 生成模块
|
||||
- rag: RAG模块
|
||||
- ratio: 配比模块
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from .base import ErrorCode
|
||||
|
||||
|
||||
class ErrorCodes:
|
||||
def __init__(self):
|
||||
self.message = None
|
||||
self.code = None
|
||||
|
||||
"""
|
||||
集中式错误码定义
|
||||
|
||||
所有错误码在此一次性定义,使用时直接通过类属性访问。
|
||||
|
||||
使用示例:
|
||||
from app.core.exception import ErrorCodes, BusinessError
|
||||
|
||||
raise BusinessError(ErrorCodes.TASK_NOT_FOUND)
|
||||
"""
|
||||
|
||||
# ========== 通用错误码 ==========
|
||||
SUCCESS: Final = ErrorCode("0", "Success", 200)
|
||||
BAD_REQUEST: Final = ErrorCode("common.0001", "Bad request", 400)
|
||||
NOT_FOUND: Final = ErrorCode("common.0002", "Resource not found", 404)
|
||||
FORBIDDEN: Final = ErrorCode("common.0003", "Forbidden", 403)
|
||||
UNAUTHORIZED: Final = ErrorCode("common.0004", "Unauthorized", 401)
|
||||
VALIDATION_ERROR: Final = ErrorCode("common.0005", "Validation error", 422)
|
||||
OPERATION_FAILED: Final = ErrorCode("common.0006", "Operation failed", 500)
|
||||
|
||||
# ========== 系统级错误码 ==========
|
||||
INTERNAL_ERROR: Final = ErrorCode("system.0001", "Internal server error", 500)
|
||||
DATABASE_ERROR: Final = ErrorCode("system.0002", "Database error", 500)
|
||||
NETWORK_ERROR: Final = ErrorCode("system.0003", "Network error", 500)
|
||||
CONFIG_ERROR: Final = ErrorCode("system.0004", "Configuration error", 500)
|
||||
SERVICE_UNAVAILABLE: Final = ErrorCode("system.0005", "Service unavailable", 503)
|
||||
|
||||
# ========== 标注模块 ==========
|
||||
ANNOTATION_TASK_NOT_FOUND: Final = ErrorCode(
|
||||
"annotation.0001", "Annotation task not found", 404
|
||||
)
|
||||
ANNOTATION_PROJECT_NOT_FOUND: Final = ErrorCode(
|
||||
"annotation.0002", "Annotation project not found", 404
|
||||
)
|
||||
ANNOTATION_TEMPLATE_NOT_FOUND: Final = ErrorCode(
|
||||
"annotation.0003", "Annotation template not found", 404
|
||||
)
|
||||
ANNOTATION_FILE_NOT_FOUND: Final = ErrorCode(
|
||||
"annotation.0004", "File not found", 404
|
||||
)
|
||||
ANNOTATION_TAG_UPDATE_FAILED: Final = ErrorCode(
|
||||
"annotation.0005", "Failed to update tags", 500
|
||||
)
|
||||
|
||||
# ========== 归集模块 ==========
|
||||
COLLECTION_TASK_NOT_FOUND: Final = ErrorCode(
|
||||
"collection.0001", "Collection task not found", 404
|
||||
)
|
||||
COLLECTION_TEMPLATE_NOT_FOUND: Final = ErrorCode(
|
||||
"collection.0002", "Collection template not found", 404
|
||||
)
|
||||
COLLECTION_EXECUTION_NOT_FOUND: Final = ErrorCode(
|
||||
"collection.0003", "Execution record not found", 404
|
||||
)
|
||||
COLLECTION_LOG_NOT_FOUND: Final = ErrorCode(
|
||||
"collection.0004", "Log file not found", 404
|
||||
)
|
||||
|
||||
# ========== 评估模块 ==========
|
||||
EVALUATION_TASK_NOT_FOUND: Final = ErrorCode(
|
||||
"evaluation.0001", "Evaluation task not found", 404
|
||||
)
|
||||
EVALUATION_TASK_TYPE_ERROR: Final = ErrorCode(
|
||||
"evaluation.0002", "Invalid task type", 400
|
||||
)
|
||||
EVALUATION_MODEL_NOT_FOUND: Final = ErrorCode(
|
||||
"evaluation.0003", "Evaluation model not found", 404
|
||||
)
|
||||
|
||||
# ========== 生成模块 ==========
|
||||
GENERATION_TASK_NOT_FOUND: Final = ErrorCode(
|
||||
"generation.0001", "Generation task not found", 404
|
||||
)
|
||||
GENERATION_FILE_NOT_FOUND: Final = ErrorCode(
|
||||
"generation.0002", "Generation file not found", 404
|
||||
)
|
||||
GENERATION_CHUNK_NOT_FOUND: Final = ErrorCode(
|
||||
"generation.0003", "Data chunk not found", 404
|
||||
)
|
||||
GENERATION_DATA_NOT_FOUND: Final = ErrorCode(
|
||||
"generation.0004", "Generation data not found", 404
|
||||
)
|
||||
|
||||
# ========== RAG 模块 ==========
|
||||
RAG_CONFIG_ERROR: Final = ErrorCode("rag.0001", "RAG configuration error", 400)
|
||||
RAG_KNOWLEDGE_BASE_NOT_FOUND: Final = ErrorCode(
|
||||
"rag.0002", "Knowledge base not found", 404
|
||||
)
|
||||
RAG_KNOWLEDGE_BASE_ALREADY_EXISTS: Final = ErrorCode(
|
||||
"rag.0003", "Knowledge base already exists", 400
|
||||
)
|
||||
RAG_KNOWLEDGE_BASE_NAME_INVALID: Final = ErrorCode(
|
||||
"rag.0004", "Knowledge base name is invalid", 400
|
||||
)
|
||||
RAG_FILE_NOT_FOUND: Final = ErrorCode("rag.0005", "RAG file not found", 404)
|
||||
RAG_FILE_PROCESS_FAILED: Final = ErrorCode(
|
||||
"rag.0006", "File processing failed", 500
|
||||
)
|
||||
RAG_FILE_PARSE_FAILED: Final = ErrorCode("rag.0007", "File parsing failed", 500)
|
||||
RAG_CHUNK_NOT_FOUND: Final = ErrorCode("rag.0008", "Chunk not found", 404)
|
||||
RAG_MODEL_NOT_FOUND: Final = ErrorCode("rag.0009", "RAG model not found", 404)
|
||||
RAG_QUERY_FAILED: Final = ErrorCode("rag.0010", "RAG query failed", 500)
|
||||
RAG_MILVUS_ERROR: Final = ErrorCode("rag.0011", "Milvus operation failed", 500)
|
||||
RAG_COLLECTION_NOT_FOUND: Final = ErrorCode(
|
||||
"rag.0012", "Milvus collection not found", 404
|
||||
)
|
||||
RAG_EMBEDDING_FAILED: Final = ErrorCode(
|
||||
"rag.0013", "Embedding generation failed", 500
|
||||
)
|
||||
RAG_UNSUPPORTED_TYPE: Final = ErrorCode(
|
||||
"rag.0014", "Unsupported RAG type", 400
|
||||
)
|
||||
RAG_INVALID_REQUEST: Final = ErrorCode(
|
||||
"rag.0015", "Invalid request", 400
|
||||
)
|
||||
|
||||
# ========== 配比模块 ==========
|
||||
RATIO_TASK_NOT_FOUND: Final = ErrorCode("ratio.0001", "Ratio task not found", 404)
|
||||
RATIO_NAME_REQUIRED: Final = ErrorCode("ratio.0002", "Task name is required", 400)
|
||||
RATIO_ALREADY_EXISTS: Final = ErrorCode("ratio.0003", "Task already exists", 400)
|
||||
RATIO_DELETE_FAILED: Final = ErrorCode("ratio.0004", "Failed to delete task", 500)
|
||||
|
||||
# ========== 清洗模块 ==========
|
||||
CLEANING_TASK_NOT_FOUND: Final = ErrorCode(
|
||||
"cleaning.0001", "Cleaning task not found", 404
|
||||
)
|
||||
CLEANING_NAME_DUPLICATED: Final = ErrorCode(
|
||||
"cleaning.0002", "Cleaning task name is duplicated", 400
|
||||
)
|
||||
CLEANING_TEMPLATE_NOT_FOUND: Final = ErrorCode(
|
||||
"cleaning.0003", "Cleaning template not found", 404
|
||||
)
|
||||
CLEANING_TEMPLATE_NAME_DUPLICATED: Final = ErrorCode(
|
||||
"cleaning.0004", "Cleaning template name is duplicated", 400
|
||||
)
|
||||
CLEANING_INVALID_OPERATOR_INPUT: Final = ErrorCode(
|
||||
"cleaning.0005", "Invalid operator input/output types", 400
|
||||
)
|
||||
CLEANING_INVALID_EXECUTOR_TYPE: Final = ErrorCode(
|
||||
"cleaning.0006", "Invalid executor type", 400
|
||||
)
|
||||
CLEANING_DATASET_NOT_FOUND: Final = ErrorCode(
|
||||
"cleaning.0007", "Dataset not found", 404
|
||||
)
|
||||
CLEANING_FILE_SYSTEM_ERROR: Final = ErrorCode(
|
||||
"cleaning.0008", "File system error", 500
|
||||
)
|
||||
CLEANING_SETTINGS_PARSE_ERROR: Final = ErrorCode(
|
||||
"cleaning.0009", "Settings parse error", 400
|
||||
)
|
||||
CLEANING_TASK_ID_REQUIRED: Final = ErrorCode(
|
||||
"cleaning.0010", "Task ID is required", 400
|
||||
)
|
||||
CLEANING_CANNOT_DELETE_PRESET_TEMPLATE: Final = ErrorCode(
|
||||
"cleaning.0011", "Cannot delete preset template", 400
|
||||
)
|
||||
CLEANING_TASK_LOG_NOT_FOUND: Final = ErrorCode(
|
||||
"cleaning.0012", "Cleaning task log file not found", 404
|
||||
)
|
||||
CLEANING_TASK_STATUS_INVALID: Final = ErrorCode(
|
||||
"cleaning.0013", "Cleaning task status is invalid for this operation", 400
|
||||
)
|
||||
|
||||
# ========== 算子市场模块 ==========
|
||||
OPERATOR_NOT_FOUND: Final = ErrorCode("operator.0001", "Operator not found", 404)
|
||||
OPERATOR_IN_INSTANCE: Final = ErrorCode("operator.0002", "Operator is in use", 400)
|
||||
OPERATOR_CANNOT_DELETE_PREDEFINED: Final = ErrorCode(
|
||||
"operator.0003", "Cannot delete predefined operator", 400
|
||||
)
|
||||
OPERATOR_UNSUPPORTED_FILE_TYPE: Final = ErrorCode(
|
||||
"operator.0004", "Unsupported file type", 400
|
||||
)
|
||||
OPERATOR_PARSE_FAILED: Final = ErrorCode(
|
||||
"operator.0005", "Failed to parse operator package", 400
|
||||
)
|
||||
OPERATOR_FIELD_NOT_FOUND: Final = ErrorCode(
|
||||
"operator.0006", "Required field is missing", 400
|
||||
)
|
||||
|
||||
# ========== 系统模块 ==========
|
||||
SYSTEM_MODEL_NOT_FOUND: Final = ErrorCode(
|
||||
"system.0006", "Model configuration not found", 404
|
||||
)
|
||||
SYSTEM_MODEL_HEALTH_CHECK_FAILED: Final = ErrorCode(
|
||||
"system.0007", "Model health check failed", 500
|
||||
)
|
||||
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
异常处理器和响应构建器
|
||||
|
||||
提供异常的自动转换,将异常转换为标准化的 JSON 响应。
|
||||
"""
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import Request, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
from .base import BaseError, BusinessError, SystemError
|
||||
from .codes import ErrorCodes
|
||||
from ..logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def SuccessResponse(data: Any = None, message: str = "success", code: str = "0") -> dict:
|
||||
"""
|
||||
构建成功响应
|
||||
|
||||
Args:
|
||||
data: 响应数据
|
||||
message: 成功消息
|
||||
code: 自定义成功码(默认为 "0")
|
||||
|
||||
Returns:
|
||||
响应字典,符合 StandardResponse 格式
|
||||
|
||||
使用示例:
|
||||
return SuccessResponse(data={"id": 123})
|
||||
# 或
|
||||
return SuccessResponse({"id": 123})
|
||||
"""
|
||||
return {
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
def ErrorResponse(
|
||||
error_code: ErrorCodes,
|
||||
data: Any = None,
|
||||
custom_message: Optional[str] = None
|
||||
) -> dict:
|
||||
"""
|
||||
构建错误响应
|
||||
|
||||
Args:
|
||||
error_code: 错误码(来自 ErrorCodes)
|
||||
data: 附加错误数据
|
||||
custom_message: 覆盖默认错误消息
|
||||
|
||||
Returns:
|
||||
响应字典,符合 StandardResponse 格式
|
||||
|
||||
使用示例:
|
||||
return ErrorResponse(ErrorCodes.TASK_NOT_FOUND)
|
||||
"""
|
||||
return {
|
||||
"code": error_code.code,
|
||||
"message": custom_message or error_code.message,
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
async def business_error_handler(request: Request, exc: BusinessError) -> JSONResponse:
|
||||
"""
|
||||
处理业务逻辑异常
|
||||
|
||||
业务异常是预期的错误,不需要记录堆栈跟踪。
|
||||
返回对应的 HTTP 状态码(400、404 等),错误信息在响应体的 code 字段中。
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.http_status,
|
||||
content=exc.to_dict()
|
||||
)
|
||||
|
||||
|
||||
async def system_error_handler(request: Request, exc: SystemError) -> JSONResponse:
|
||||
"""
|
||||
处理系统异常
|
||||
|
||||
系统异常是意外的错误,需要记录完整的堆栈跟踪。
|
||||
返回 HTTP 500 和经过净化的错误消息。
|
||||
"""
|
||||
logger.error(
|
||||
f"System error occurred at {request.method} {request.url.path}: {exc.message}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.http_status,
|
||||
content={
|
||||
"code": exc.code,
|
||||
"message": exc.message,
|
||||
"data": None # 绝不暴露系统错误详情
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validation_error_handler(
|
||||
request: Request,
|
||||
exc: RequestValidationError
|
||||
) -> JSONResponse:
|
||||
"""处理请求验证错误"""
|
||||
errors = exc.errors()
|
||||
simplified_errors = [
|
||||
err.get("msg", "Validation error")
|
||||
for err in errors
|
||||
]
|
||||
|
||||
return JSONResponse(
|
||||
status_code=422,
|
||||
content={
|
||||
"code": ErrorCodes.VALIDATION_ERROR.code,
|
||||
"message": ErrorCodes.VALIDATION_ERROR.message,
|
||||
"data": {
|
||||
"detail": "请求验证失败",
|
||||
"errors": simplified_errors
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def http_exception_handler(
|
||||
request: Request,
|
||||
exc: StarletteHTTPException
|
||||
) -> JSONResponse:
|
||||
"""处理 HTTP 异常(404、500 等)"""
|
||||
# 根据 HTTP 状态码映射到错误码
|
||||
error_code = ErrorCodes.INTERNAL_ERROR
|
||||
if exc.status_code == 404:
|
||||
error_code = ErrorCodes.NOT_FOUND
|
||||
elif exc.status_code == 401:
|
||||
error_code = ErrorCodes.UNAUTHORIZED
|
||||
elif exc.status_code == 403:
|
||||
error_code = ErrorCodes.FORBIDDEN
|
||||
elif exc.status_code == 422:
|
||||
error_code = ErrorCodes.VALIDATION_ERROR
|
||||
elif exc.status_code == 400:
|
||||
error_code = ErrorCodes.BAD_REQUEST
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"code": error_code.code,
|
||||
"message": "error",
|
||||
"data": {
|
||||
"detail": str(exc.detail)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""
|
||||
通用异常处理器(最后的兜底处理)
|
||||
|
||||
这是最后一道防线 - 记录完整的堆栈跟踪
|
||||
并返回经过净化的错误给客户端。
|
||||
"""
|
||||
logger.error(
|
||||
f"Unhandled exception occurred at {request.method} {request.url.path}: {str(exc)}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"code": ErrorCodes.INTERNAL_ERROR.code,
|
||||
"message": ErrorCodes.INTERNAL_ERROR.message,
|
||||
"data": {
|
||||
"detail": "服务器内部错误"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def register_exception_handlers(app) -> None:
|
||||
"""
|
||||
注册所有异常处理器到 FastAPI 应用
|
||||
|
||||
Args:
|
||||
app: FastAPI 应用实例
|
||||
|
||||
使用示例:
|
||||
from app.core.exception import register_exception_handlers
|
||||
|
||||
app = FastAPI()
|
||||
register_exception_handlers(app)
|
||||
"""
|
||||
from .base import BusinessError, SystemError
|
||||
|
||||
# 按照特异性顺序注册异常处理器(最具体的在前)
|
||||
app.add_exception_handler(BusinessError, business_error_handler)
|
||||
app.add_exception_handler(SystemError, system_error_handler)
|
||||
app.add_exception_handler(RequestValidationError, validation_error_handler)
|
||||
app.add_exception_handler(StarletteHTTPException, http_exception_handler)
|
||||
app.add_exception_handler(Exception, generic_exception_handler)
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
全局异常处理中间件,支持自动响应包装
|
||||
|
||||
此中间件提供自动地返回值和异常转换,确保所有响应都符合标准化的 JSON 格式。
|
||||
关键特性:
|
||||
- 自动包装 dict/list 响应为 StandardResponse 格式
|
||||
- 异常处理,并记录适当的日志
|
||||
- 堆栈跟踪隔离(永远不会暴露给客户端)
|
||||
"""
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import Response as StarletteResponse
|
||||
|
||||
from .base import BaseError, BusinessError, SystemError
|
||||
from .codes import ErrorCodes
|
||||
from ..logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ExceptionHandlingMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
全局异常捕获中间件
|
||||
|
||||
这是最外层的中间件,捕获所有未处理的异常,
|
||||
并将它们转换为标准化的响应格式。
|
||||
|
||||
堆栈跟踪信息只记录到日志文件,永远不会发送给客户端。
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
"""
|
||||
处理请求并处理任何异常
|
||||
|
||||
异常会被捕获、记录(带堆栈跟踪),并转换为标准化响应。
|
||||
业务异常会被重新抛出,由专门的处理器处理。
|
||||
"""
|
||||
try:
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
except BusinessError:
|
||||
# 让业务异常处理器处理
|
||||
raise
|
||||
|
||||
except SystemError as exc:
|
||||
# 记录系统错误及其完整堆栈跟踪
|
||||
logger.error(
|
||||
f"System error occurred at {request.method} {request.url.path}",
|
||||
exc_info=True
|
||||
)
|
||||
return self._error_response(
|
||||
code=exc.code,
|
||||
message=exc.message,
|
||||
http_status=exc.http_status
|
||||
)
|
||||
|
||||
except BaseError as exc:
|
||||
# 处理其他自定义错误
|
||||
logger.warning(f"BaseError occurred at {request.url.path}: {exc.message}")
|
||||
return self._error_response(
|
||||
code=exc.code,
|
||||
message=exc.message,
|
||||
http_status=exc.http_status
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
# 捕获所有未处理的异常
|
||||
logger.error(
|
||||
f"Unhandled exception occurred at {request.method} {request.url.path}",
|
||||
exc_info=True
|
||||
)
|
||||
return self._error_response(
|
||||
code=ErrorCodes.INTERNAL_ERROR.code,
|
||||
message=ErrorCodes.INTERNAL_ERROR.message,
|
||||
http_status=500
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _error_response(
|
||||
code: str,
|
||||
message: str,
|
||||
http_status: int
|
||||
) -> StarletteResponse:
|
||||
"""构建错误响应"""
|
||||
|
||||
return JSONResponse(
|
||||
status_code=http_status,
|
||||
content={
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": None
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Result 类型用于优雅的错误处理
|
||||
|
||||
提供受 Rust 启发的 Result<T, E> 类型,用于处理可能失败的操作而无需使用异常。
|
||||
|
||||
使用示例:
|
||||
# 成功情况
|
||||
def get_user(user_id: str) -> Result[User]:
|
||||
user = db.find_user(user_id)
|
||||
if user:
|
||||
return Ok(user)
|
||||
return Err(ErrorCodes.USER_NOT_FOUND)
|
||||
|
||||
# 使用结果
|
||||
result = get_user("123")
|
||||
if result.is_ok():
|
||||
user = result.unwrap()
|
||||
print(f"User: {user.name}")
|
||||
else:
|
||||
error = result.unwrap_err()
|
||||
print(f"Error: {error.message}")
|
||||
"""
|
||||
from typing import Generic, TypeVar, Optional, Any
|
||||
|
||||
from .base import ErrorCode
|
||||
from .codes import ErrorCodes
|
||||
|
||||
T = TypeVar('T') # 成功类型
|
||||
E = TypeVar('E', bound=ErrorCode) # 错误类型
|
||||
|
||||
|
||||
class Result(Generic[T, E]):
|
||||
"""
|
||||
表示成功(Ok)或失败(Err)的 Result 类型
|
||||
|
||||
此类型允许在不需要异常的情况下进行显式错误处理
|
||||
"""
|
||||
|
||||
def __init__(self, value: Optional[T], error: Optional[E], is_ok: bool):
|
||||
self._value = value
|
||||
self._error = error
|
||||
self._is_ok = is_ok
|
||||
|
||||
@staticmethod
|
||||
def ok(value: T) -> 'Result[T, E]':
|
||||
"""创建一个包含值的成功结果"""
|
||||
return Result(value, None, True)
|
||||
|
||||
@staticmethod
|
||||
def err(error: E) -> 'Result[T, E]':
|
||||
"""创建一个包含错误码的失败结果"""
|
||||
return Result(None, error, False)
|
||||
|
||||
@property
|
||||
def is_ok(self) -> bool:
|
||||
"""检查结果是否成功"""
|
||||
return self._is_ok
|
||||
|
||||
@property
|
||||
def is_err(self) -> bool:
|
||||
"""检查结果是否失败"""
|
||||
return not self._is_ok
|
||||
|
||||
def unwrap(self) -> T:
|
||||
"""
|
||||
获取成功值
|
||||
|
||||
Returns:
|
||||
成功值
|
||||
|
||||
Raises:
|
||||
ValueError: 如果结果是错误
|
||||
"""
|
||||
if self._is_ok:
|
||||
return self._value
|
||||
raise ValueError(
|
||||
f"Cannot unwrap error result: {self._error.message}"
|
||||
)
|
||||
|
||||
def unwrap_err(self) -> E:
|
||||
"""
|
||||
获取错误码
|
||||
|
||||
Returns:
|
||||
错误码
|
||||
|
||||
Raises:
|
||||
ValueError: 如果结果是成功的
|
||||
"""
|
||||
if not self._is_ok:
|
||||
return self._error
|
||||
raise ValueError("Cannot unwrap error from successful result")
|
||||
|
||||
def unwrap_or(self, default: T) -> T:
|
||||
"""
|
||||
获取成功值,如果出错则返回默认值
|
||||
|
||||
Args:
|
||||
default: 出错时返回的默认值
|
||||
|
||||
Returns:
|
||||
成功值或默认值
|
||||
"""
|
||||
return self._value if self._is_ok else default
|
||||
|
||||
def map(self, func) -> 'Result[Any, E]':
|
||||
"""
|
||||
如果存在成功值,则应用函数
|
||||
|
||||
Args:
|
||||
func: 要应用的函数
|
||||
|
||||
Returns:
|
||||
包含映射值的新结果或相同的错误
|
||||
"""
|
||||
if self._is_ok:
|
||||
try:
|
||||
return Result.ok(func(self._value))
|
||||
except Exception:
|
||||
# 如果映射失败,转换为错误
|
||||
return Result.err(ErrorCodes.INTERNAL_ERROR)
|
||||
return self
|
||||
|
||||
def and_then(self, func) -> 'Result[Any, E]':
|
||||
"""
|
||||
链式调用返回 Result 的操作
|
||||
|
||||
Args:
|
||||
func: 接收成功值并返回新 Result 的函数
|
||||
|
||||
Returns:
|
||||
来自函数的新结果或相同的错误
|
||||
"""
|
||||
if self._is_ok:
|
||||
return func(self._value)
|
||||
return self
|
||||
|
||||
def or_else(self, func) -> 'Result[T, Any]':
|
||||
"""
|
||||
在出错时提供备用结果
|
||||
|
||||
Args:
|
||||
func: 接收错误并返回新 Result 的函数
|
||||
|
||||
Returns:
|
||||
相同的结果或来自函数的新结果
|
||||
"""
|
||||
if not self._is_ok:
|
||||
return func(self._error)
|
||||
return self
|
||||
|
||||
|
||||
def Ok(value: T) -> Result[T, ErrorCode]:
|
||||
"""
|
||||
创建一个成功的结果
|
||||
|
||||
使用示例:
|
||||
return Ok(user_data)
|
||||
"""
|
||||
return Result.ok(value)
|
||||
|
||||
|
||||
def Err(error_code: ErrorCode) -> Result[Any, ErrorCode]:
|
||||
"""
|
||||
创建一个失败的结果
|
||||
|
||||
使用示例:
|
||||
return Err(ErrorCodes.USER_NOT_FOUND)
|
||||
"""
|
||||
return Result.err(error_code)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
数据库事务管理工具
|
||||
|
||||
提供自动事务管理的上下文管理器和依赖注入函数,
|
||||
确保在异常情况下正确回滚事务。
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def transaction(db: AsyncSession) -> AsyncGenerator[None, None]:
|
||||
"""
|
||||
数据库事务上下文管理器
|
||||
|
||||
自动处理事务的提交和回滚:
|
||||
- 如果代码块正常执行完成,则自动提交
|
||||
- 如果代码块抛出异常,则自动回滚
|
||||
|
||||
使用示例:
|
||||
@router.post("/tasks")
|
||||
async def create_task(
|
||||
request: CreateTaskRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
async with transaction(db):
|
||||
# 数据库操作
|
||||
db.add(task)
|
||||
await db.flush()
|
||||
|
||||
# 事务已自动提交
|
||||
return SuccessResponse(data=task)
|
||||
|
||||
Args:
|
||||
db: SQLAlchemy 异步会话
|
||||
|
||||
Yields:
|
||||
None
|
||||
|
||||
Raises:
|
||||
Exception: 重新抛出代码块中的任何异常
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
# 如果没有异常,提交事务
|
||||
await db.commit()
|
||||
logger.debug("Transaction committed successfully")
|
||||
except Exception as e:
|
||||
# 发生异常,回滚事务
|
||||
try:
|
||||
await db.rollback()
|
||||
logger.warning(f"Transaction rolled back due to exception: {e}")
|
||||
except Exception as rollback_error:
|
||||
logger.error(f"Failed to rollback transaction: {rollback_error}")
|
||||
# 重新抛出异常,让上层异常处理器处理
|
||||
raise
|
||||
|
||||
|
||||
async def ensure_transaction_rollback(db: AsyncSession) -> None:
|
||||
"""
|
||||
确保事务回滚(用于错误处理)
|
||||
|
||||
Args:
|
||||
db: SQLAlchemy 异步会话
|
||||
"""
|
||||
try:
|
||||
await db.rollback()
|
||||
logger.debug("Transaction rolled back")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to rollback transaction: {e}")
|
||||
@@ -0,0 +1,121 @@
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
_SIZE_PATTERN = re.compile(r"^\s*(\d+)\s*([KMG]?B?)?\s*$", re.IGNORECASE)
|
||||
|
||||
|
||||
class CenteredLevelNameFormatter(logging.Formatter):
|
||||
"""Center the level name in the log output"""
|
||||
|
||||
def format(self, record):
|
||||
# 将 levelname 居中对齐到8个字符
|
||||
record.levelname = record.levelname.center(8)
|
||||
return super().format(record)
|
||||
|
||||
|
||||
def _parse_size_to_bytes(value: str) -> int:
|
||||
match = _SIZE_PATTERN.match(value)
|
||||
if not match:
|
||||
return 100 * 1024 * 1024
|
||||
|
||||
amount = int(match.group(1))
|
||||
unit = (match.group(2) or "B").upper()
|
||||
if unit in {"G", "GB"}:
|
||||
return amount * 1024 * 1024 * 1024
|
||||
if unit in {"M", "MB"}:
|
||||
return amount * 1024 * 1024
|
||||
if unit in {"K", "KB"}:
|
||||
return amount * 1024
|
||||
return amount
|
||||
|
||||
|
||||
def _rotated_log_namer(default_name: str) -> str:
|
||||
path = Path(default_name)
|
||||
index_suffix = path.suffix
|
||||
if not index_suffix.lstrip(".").isdigit():
|
||||
return default_name
|
||||
|
||||
base_path = Path(str(path)[: -len(index_suffix)])
|
||||
if not base_path.suffix:
|
||||
return default_name
|
||||
|
||||
return str(base_path.with_name(f"{base_path.stem}{index_suffix}{base_path.suffix}"))
|
||||
|
||||
|
||||
def _remove_managed_handlers(logger: logging.Logger) -> None:
|
||||
for handler in list(logger.handlers):
|
||||
if getattr(handler, "_deepdata_managed", False):
|
||||
logger.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
|
||||
def _mark_managed(handler: logging.Handler) -> logging.Handler:
|
||||
setattr(handler, "_deepdata_managed", True)
|
||||
return handler
|
||||
|
||||
|
||||
def setup_logging():
|
||||
|
||||
log_format = "%(asctime)s [%(levelname)s] - %(name)s - %(message)s"
|
||||
date_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
console_handler = _mark_managed(logging.StreamHandler(sys.stdout))
|
||||
console_handler.setLevel(getattr(logging, settings.log_level.upper()))
|
||||
|
||||
log_dir = Path(settings.log_file_dir)
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
file_handler = _mark_managed(RotatingFileHandler(
|
||||
log_dir / "python-backend.log",
|
||||
maxBytes=_parse_size_to_bytes(settings.log_rotation_max_size),
|
||||
backupCount=max(1, settings.log_rotation_backup_count),
|
||||
encoding="utf-8"
|
||||
))
|
||||
file_handler.namer = _rotated_log_namer
|
||||
file_handler.setLevel(getattr(logging, settings.log_level.upper()))
|
||||
|
||||
# Style setting - Centered level names
|
||||
formatter = CenteredLevelNameFormatter(log_format, date_format)
|
||||
console_handler.setFormatter(formatter)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# Root Logger
|
||||
root_logger = logging.getLogger()
|
||||
_remove_managed_handlers(root_logger)
|
||||
root_logger.setLevel(getattr(logging, settings.log_level.upper()))
|
||||
root_logger.addHandler(console_handler)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
# Uvicorn
|
||||
uvicorn_logger = logging.getLogger("uvicorn")
|
||||
uvicorn_logger.handlers.clear()
|
||||
uvicorn_logger.addHandler(console_handler)
|
||||
uvicorn_logger.setLevel(logging.INFO)
|
||||
|
||||
uvicorn_access = logging.getLogger("uvicorn.access")
|
||||
uvicorn_access.handlers.clear()
|
||||
uvicorn_access.addHandler(console_handler)
|
||||
uvicorn_access.setLevel(logging.DEBUG)
|
||||
|
||||
uvicorn_error = logging.getLogger("uvicorn.error")
|
||||
uvicorn_error.handlers.clear()
|
||||
uvicorn_error.addHandler(console_handler)
|
||||
uvicorn_error.setLevel(logging.ERROR)
|
||||
|
||||
# SQLAlchemy (ERROR only)
|
||||
sqlalchemy_logger = logging.getLogger("sqlalchemy.engine")
|
||||
_remove_managed_handlers(sqlalchemy_logger)
|
||||
sqlalchemy_logger.setLevel(logging.ERROR)
|
||||
sqlalchemy_logger.addHandler(console_handler)
|
||||
sqlalchemy_logger.setLevel(logging.ERROR)
|
||||
|
||||
# Minimize noise from HTTPX and HTTPCore
|
||||
logging.getLogger("httpx").setLevel(logging.ERROR)
|
||||
logging.getLogger("httpcore").setLevel(logging.ERROR)
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
return logging.getLogger(name)
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Security utilities for handling sensitive data"""
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
SENSITIVE_FIELDS = [
|
||||
"secretKey",
|
||||
"accessKey",
|
||||
"password",
|
||||
"passwd",
|
||||
"pwd",
|
||||
"secret",
|
||||
"token",
|
||||
"apiKey",
|
||||
"api_key",
|
||||
"access_key",
|
||||
"secret_key",
|
||||
]
|
||||
|
||||
MASK_PATTERN = "**********"
|
||||
|
||||
|
||||
def mask_sensitive_value(value: str) -> str:
|
||||
"""Mask a single sensitive value"""
|
||||
return MASK_PATTERN
|
||||
|
||||
|
||||
def is_masked_value(value: str) -> bool:
|
||||
"""Check if a value is already masked"""
|
||||
return value == MASK_PATTERN
|
||||
|
||||
|
||||
def preserve_sensitive_values(
|
||||
new_config: Dict[str, Any],
|
||||
original_config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Preserve original sensitive values if masked pattern is detected in update request.
|
||||
|
||||
When frontend receives masked values and sends them back in update request,
|
||||
this function detects masked values and replaces them with original values
|
||||
from database.
|
||||
|
||||
Args:
|
||||
new_config: Config from update request (may contain masked values)
|
||||
original_config: Original config from database (contains real values)
|
||||
|
||||
Returns:
|
||||
Config with original sensitive values preserved
|
||||
"""
|
||||
if not isinstance(new_config, dict) or not isinstance(original_config, dict):
|
||||
return new_config
|
||||
|
||||
preserved_config = {}
|
||||
for key, new_value in new_config.items():
|
||||
# Check if key is a sensitive field
|
||||
key_lower = key.lower()
|
||||
is_sensitive = any(
|
||||
field.lower() == key_lower or field.lower() in key_lower
|
||||
for field in SENSITIVE_FIELDS
|
||||
)
|
||||
|
||||
# If value is masked and field is sensitive, use original value
|
||||
if is_sensitive and isinstance(new_value, str) and is_masked_value(new_value):
|
||||
original_value = original_config.get(key)
|
||||
preserved_config[key] = original_value if original_value else new_value
|
||||
elif isinstance(new_value, dict):
|
||||
# Recursively process nested dictionaries
|
||||
original_nested = original_config.get(key, {})
|
||||
preserved_config[key] = preserve_sensitive_values(new_value, original_nested)
|
||||
elif isinstance(new_value, list):
|
||||
original_list = original_config.get(key, [])
|
||||
preserved_list = []
|
||||
for i, new_item in enumerate(new_value):
|
||||
if i < len(original_list):
|
||||
orig_item = original_list[i]
|
||||
if isinstance(new_item, dict) and isinstance(orig_item, dict):
|
||||
preserved_list.append(preserve_sensitive_values(new_item, orig_item))
|
||||
else:
|
||||
preserved_list.append(new_item)
|
||||
else:
|
||||
preserved_list.append(new_item)
|
||||
preserved_config[key] = preserved_list
|
||||
else:
|
||||
# Keep non-sensitive/non-masked values
|
||||
preserved_config[key] = new_value
|
||||
|
||||
return preserved_config
|
||||
|
||||
|
||||
def mask_sensitive_dict(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Recursively mask sensitive fields in a dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary that may contain sensitive fields
|
||||
|
||||
Returns:
|
||||
Dictionary with sensitive values masked
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
masked_data = {}
|
||||
for key, value in data.items():
|
||||
# Check if the key is a sensitive field (case-insensitive)
|
||||
key_lower = key.lower()
|
||||
is_sensitive = any(
|
||||
field.lower() == key_lower or field.lower() in key_lower
|
||||
for field in SENSITIVE_FIELDS
|
||||
)
|
||||
|
||||
if is_sensitive and isinstance(value, str):
|
||||
# Mask the sensitive value
|
||||
masked_data[key] = MASK_PATTERN
|
||||
elif isinstance(value, dict):
|
||||
# Recursively mask nested dictionaries
|
||||
masked_data[key] = mask_sensitive_dict(value)
|
||||
elif isinstance(value, list):
|
||||
# Process list items
|
||||
masked_data[key] = [
|
||||
mask_sensitive_dict(item) if isinstance(item, dict) else item
|
||||
for item in value
|
||||
]
|
||||
else:
|
||||
# Keep non-sensitive values as-is
|
||||
masked_data[key] = value
|
||||
|
||||
return masked_data
|
||||
|
||||
|
||||
def mask_sensitive_info(text: str) -> str:
|
||||
"""
|
||||
Mask sensitive information in text by replacing values with **********
|
||||
|
||||
Args:
|
||||
text: Original text that may contain sensitive information
|
||||
|
||||
Returns:
|
||||
Text with sensitive values masked
|
||||
"""
|
||||
masked_text = text
|
||||
|
||||
for field in SENSITIVE_FIELDS:
|
||||
patterns = [
|
||||
rf'"{field}"\s*:\s*"[^"]*"',
|
||||
rf'{field}\s*=\s*[^\s,\]]+',
|
||||
rf"'{field}'\s*:\s*'[^']*'",
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
if '"' in pattern:
|
||||
masked_text = re.sub(pattern, f'"{field}": "{MASK_PATTERN}"', masked_text, flags=re.IGNORECASE)
|
||||
elif "'" in pattern:
|
||||
masked_text = re.sub(pattern, f"'{field}': '{MASK_PATTERN}'", masked_text, flags=re.IGNORECASE)
|
||||
else:
|
||||
masked_text = re.sub(pattern, f'{field}={MASK_PATTERN}', masked_text, flags=re.IGNORECASE)
|
||||
|
||||
return masked_text
|
||||
Reference in New Issue
Block a user