2026-08-18 14:49:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
操作日志工具模块
|
|
|
|
|
|
|
|
|
|
|
|
提供 @op_log 装饰器和 log_operation 函数,用于记录用户在各业务模块的详细操作。
|
|
|
|
|
|
自动捕获成功/失败状态、完整报错堆栈、操作耗时等。
|
|
|
|
|
|
|
|
|
|
|
|
核心设计:
|
|
|
|
|
|
- 失败操作必须清晰记录完整异常堆栈(traceback)
|
|
|
|
|
|
- 记录异常类型(如 RuntimeError / ValueError / ConnectionError)
|
|
|
|
|
|
- 记录具体出错的函数名和文件位置,方便定位 bug
|
|
|
|
|
|
- 记录 HTTP 状态码,方便区分用户错误(4xx)和系统错误(5xx)
|
|
|
|
|
|
|
|
|
|
|
|
使用示例:
|
|
|
|
|
|
from app.core.op_log import op_log, OpModule, OpAction
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/inference/start")
|
|
|
|
|
|
@op_log(module=OpModule.INFERENCE, action=OpAction.START, target_type="inference")
|
|
|
|
|
|
async def start_inference(...):
|
|
|
|
|
|
...
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import functools
|
|
|
|
|
|
import json
|
|
|
|
|
|
import time
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
from typing import Any, Callable, Optional, TypeVar
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import Request
|
|
|
|
|
|
|
2026-08-19 17:26:49 +08:00
|
|
|
|
from app.core.logging import get_logger, get_structured_logger, request_id_var
|
2026-08-18 14:49:12 +08:00
|
|
|
|
from app.db.platform_store import get_platform_store, new_id, utcnow
|
|
|
|
|
|
|
|
|
|
|
|
logger = get_logger("app.op_log")
|
2026-08-19 17:26:49 +08:00
|
|
|
|
biz_logger = get_structured_logger("app.biz")
|
2026-08-18 14:49:12 +08:00
|
|
|
|
|
|
|
|
|
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpModule:
|
|
|
|
|
|
"""业务模块常量"""
|
|
|
|
|
|
FINE_TUNE = "fine-tune" # 模型训练
|
|
|
|
|
|
MODEL_EVAL = "model-eval" # 模型评测
|
|
|
|
|
|
INFERENCE = "model-inference" # 模型推理
|
|
|
|
|
|
MODEL_MANAGE = "model-manage" # 模型管理
|
|
|
|
|
|
DATASET = "dataset" # 数据集
|
|
|
|
|
|
DATA_PROCESS = "data-process" # 数据处理
|
|
|
|
|
|
DATA_CONVERT = "data-convert" # 数据类型转换
|
|
|
|
|
|
COMPUTE = "compute" # 算力节点
|
|
|
|
|
|
SYSTEM = "system" # 系统
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpAction:
|
|
|
|
|
|
"""操作动作常量"""
|
|
|
|
|
|
CREATE = "create"
|
|
|
|
|
|
UPDATE = "update"
|
|
|
|
|
|
DELETE = "delete"
|
|
|
|
|
|
START = "start"
|
|
|
|
|
|
STOP = "stop"
|
|
|
|
|
|
UPLOAD = "upload"
|
|
|
|
|
|
DOWNLOAD = "download"
|
|
|
|
|
|
CONVERT = "convert"
|
|
|
|
|
|
MERGE = "merge"
|
|
|
|
|
|
IMPORT = "import"
|
|
|
|
|
|
LOGIN = "login"
|
|
|
|
|
|
LOGOUT = "logout"
|
|
|
|
|
|
PUBLISH = "publish"
|
|
|
|
|
|
RETRY = "retry"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpStatus:
|
|
|
|
|
|
"""操作状态常量"""
|
|
|
|
|
|
SUCCESS = "success"
|
|
|
|
|
|
FAILURE = "failure"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def op_log(
|
|
|
|
|
|
module: str,
|
|
|
|
|
|
action: str,
|
|
|
|
|
|
target_type: str = "",
|
|
|
|
|
|
*,
|
|
|
|
|
|
target_name_param: str = "name",
|
|
|
|
|
|
detail_params: Optional[list[str]] = None,
|
|
|
|
|
|
) -> Callable[[F], F]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
操作日志装饰器
|
|
|
|
|
|
|
|
|
|
|
|
自动记录:
|
|
|
|
|
|
- 谁在什么时间操作了什么
|
|
|
|
|
|
- 成功还是失败
|
|
|
|
|
|
- 失败时记录完整异常堆栈(traceback)、异常类型、异常消息
|
|
|
|
|
|
- 出错的函数名和文件位置,方便定位 bug
|
|
|
|
|
|
- 操作耗时(ms)
|
|
|
|
|
|
- 客户端 IP、请求路径
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
module: 业务模块(OpModule 常量)
|
|
|
|
|
|
action: 操作动作(OpAction 常量)
|
|
|
|
|
|
target_type: 资源类型
|
|
|
|
|
|
target_name_param: 从 kwargs 中提取目标名称的参数名
|
|
|
|
|
|
detail_params: 需要记录到 detail 的参数名列表
|
|
|
|
|
|
"""
|
|
|
|
|
|
def decorator(func: F) -> F:
|
|
|
|
|
|
func_name = f"{func.__module__}.{func.__qualname__}"
|
|
|
|
|
|
|
|
|
|
|
|
if asyncio.iscoroutinefunction(func):
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
|
|
async def async_wrapper(*args, **kwargs):
|
|
|
|
|
|
started_at = time.perf_counter()
|
|
|
|
|
|
trace_id = request_id_var.get("-")
|
|
|
|
|
|
|
|
|
|
|
|
user = _extract_user(args, kwargs)
|
|
|
|
|
|
request = _extract_request(args)
|
|
|
|
|
|
|
|
|
|
|
|
target_name = _get_param(kwargs, target_name_param, "")
|
|
|
|
|
|
target_id = _get_param(kwargs, "task_id", "") or _get_param(kwargs, "dataset_id", "") or _get_param(kwargs, "model_id", "")
|
|
|
|
|
|
|
|
|
|
|
|
detail_dict = _build_detail(detail_params, kwargs)
|
|
|
|
|
|
detail_str = json.dumps(detail_dict, ensure_ascii=False) if detail_dict else ""
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = await func(*args, **kwargs)
|
|
|
|
|
|
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
|
|
|
|
|
|
|
|
|
|
|
if not target_id and isinstance(result, dict):
|
|
|
|
|
|
target_id = str(result.get("id", ""))
|
|
|
|
|
|
|
|
|
|
|
|
_write_log(
|
|
|
|
|
|
module=module,
|
|
|
|
|
|
action=action,
|
|
|
|
|
|
target_type=target_type,
|
|
|
|
|
|
target_id=str(target_id) if target_id else None,
|
|
|
|
|
|
target_name=str(target_name) if target_name else None,
|
|
|
|
|
|
status=OpStatus.SUCCESS,
|
|
|
|
|
|
error_message="",
|
|
|
|
|
|
error_type="",
|
|
|
|
|
|
error_traceback="",
|
|
|
|
|
|
func_name=func_name,
|
|
|
|
|
|
detail=detail_str,
|
|
|
|
|
|
user=user,
|
|
|
|
|
|
request=request,
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
duration_ms=elapsed_ms,
|
|
|
|
|
|
)
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
|
|
|
|
|
|
|
|
|
|
|
# 捕获完整异常堆栈
|
|
|
|
|
|
tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
|
|
|
|
full_traceback = "".join(tb_lines)
|
|
|
|
|
|
error_msg = str(exc)[:1000]
|
|
|
|
|
|
error_type = type(exc).__name__
|
|
|
|
|
|
|
|
|
|
|
|
_write_log(
|
|
|
|
|
|
module=module,
|
|
|
|
|
|
action=action,
|
|
|
|
|
|
target_type=target_type,
|
|
|
|
|
|
target_id=str(target_id) if target_id else None,
|
|
|
|
|
|
target_name=str(target_name) if target_name else None,
|
|
|
|
|
|
status=OpStatus.FAILURE,
|
|
|
|
|
|
error_message=error_msg,
|
|
|
|
|
|
error_type=error_type,
|
|
|
|
|
|
error_traceback=full_traceback,
|
|
|
|
|
|
func_name=func_name,
|
|
|
|
|
|
detail=detail_str,
|
|
|
|
|
|
user=user,
|
|
|
|
|
|
request=request,
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
duration_ms=elapsed_ms,
|
|
|
|
|
|
)
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
return async_wrapper # type: ignore
|
|
|
|
|
|
else:
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
|
|
def sync_wrapper(*args, **kwargs):
|
|
|
|
|
|
started_at = time.perf_counter()
|
|
|
|
|
|
trace_id = request_id_var.get("-")
|
|
|
|
|
|
|
|
|
|
|
|
user = _extract_user(args, kwargs)
|
|
|
|
|
|
request = _extract_request(args)
|
|
|
|
|
|
|
|
|
|
|
|
target_name = _get_param(kwargs, target_name_param, "")
|
|
|
|
|
|
target_id = _get_param(kwargs, "task_id", "") or _get_param(kwargs, "dataset_id", "") or _get_param(kwargs, "model_id", "")
|
|
|
|
|
|
|
|
|
|
|
|
detail_dict = _build_detail(detail_params, kwargs)
|
|
|
|
|
|
detail_str = json.dumps(detail_dict, ensure_ascii=False) if detail_dict else ""
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = func(*args, **kwargs)
|
|
|
|
|
|
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
|
|
|
|
|
|
|
|
|
|
|
if not target_id and isinstance(result, dict):
|
|
|
|
|
|
target_id = str(result.get("id", ""))
|
|
|
|
|
|
|
|
|
|
|
|
_write_log(
|
|
|
|
|
|
module=module,
|
|
|
|
|
|
action=action,
|
|
|
|
|
|
target_type=target_type,
|
|
|
|
|
|
target_id=str(target_id) if target_id else None,
|
|
|
|
|
|
target_name=str(target_name) if target_name else None,
|
|
|
|
|
|
status=OpStatus.SUCCESS,
|
|
|
|
|
|
error_message="",
|
|
|
|
|
|
error_type="",
|
|
|
|
|
|
error_traceback="",
|
|
|
|
|
|
func_name=func_name,
|
|
|
|
|
|
detail=detail_str,
|
|
|
|
|
|
user=user,
|
|
|
|
|
|
request=request,
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
duration_ms=elapsed_ms,
|
|
|
|
|
|
)
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
|
|
|
|
|
|
|
|
|
|
|
tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
|
|
|
|
full_traceback = "".join(tb_lines)
|
|
|
|
|
|
error_msg = str(exc)[:1000]
|
|
|
|
|
|
error_type = type(exc).__name__
|
|
|
|
|
|
|
|
|
|
|
|
_write_log(
|
|
|
|
|
|
module=module,
|
|
|
|
|
|
action=action,
|
|
|
|
|
|
target_type=target_type,
|
|
|
|
|
|
target_id=str(target_id) if target_id else None,
|
|
|
|
|
|
target_name=str(target_name) if target_name else None,
|
|
|
|
|
|
status=OpStatus.FAILURE,
|
|
|
|
|
|
error_message=error_msg,
|
|
|
|
|
|
error_type=error_type,
|
|
|
|
|
|
error_traceback=full_traceback,
|
|
|
|
|
|
func_name=func_name,
|
|
|
|
|
|
detail=detail_str,
|
|
|
|
|
|
user=user,
|
|
|
|
|
|
request=request,
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
duration_ms=elapsed_ms,
|
|
|
|
|
|
)
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
return sync_wrapper # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def log_operation(
|
|
|
|
|
|
*,
|
|
|
|
|
|
module: str,
|
|
|
|
|
|
action: str,
|
|
|
|
|
|
target_type: str = "",
|
|
|
|
|
|
target_id: str = "",
|
|
|
|
|
|
target_name: str = "",
|
|
|
|
|
|
status: str = OpStatus.SUCCESS,
|
|
|
|
|
|
error_message: str = "",
|
|
|
|
|
|
error_type: str = "",
|
|
|
|
|
|
error_traceback: str = "",
|
|
|
|
|
|
detail: str = "",
|
|
|
|
|
|
func_name: str = "",
|
|
|
|
|
|
user: Optional[dict] = None,
|
|
|
|
|
|
request: Optional[Request] = None,
|
|
|
|
|
|
duration_ms: float = 0,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""手动记录操作日志(不方便用装饰器时使用)"""
|
|
|
|
|
|
trace_id = request_id_var.get("-")
|
|
|
|
|
|
_write_log(
|
|
|
|
|
|
module=module,
|
|
|
|
|
|
action=action,
|
|
|
|
|
|
target_type=target_type,
|
|
|
|
|
|
target_id=target_id or None,
|
|
|
|
|
|
target_name=target_name or None,
|
|
|
|
|
|
status=status,
|
|
|
|
|
|
error_message=error_message,
|
|
|
|
|
|
error_type=error_type,
|
|
|
|
|
|
error_traceback=error_traceback,
|
|
|
|
|
|
func_name=func_name,
|
|
|
|
|
|
detail=detail,
|
|
|
|
|
|
user=user,
|
|
|
|
|
|
request=request,
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
duration_ms=duration_ms,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_detail(detail_params: Optional[list[str]], kwargs: dict) -> dict:
|
|
|
|
|
|
"""从 kwargs 中提取需要记录的参数"""
|
|
|
|
|
|
detail_dict = {}
|
|
|
|
|
|
if detail_params:
|
|
|
|
|
|
for p in detail_params:
|
|
|
|
|
|
val = kwargs.get(p)
|
|
|
|
|
|
if val is not None:
|
|
|
|
|
|
detail_dict[p] = str(val)[:200]
|
|
|
|
|
|
return detail_dict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_user(args: tuple, kwargs: dict) -> Optional[dict]:
|
|
|
|
|
|
"""从函数参数中提取 current_user dict"""
|
|
|
|
|
|
for arg in args:
|
|
|
|
|
|
if isinstance(arg, dict) and "id" in arg and "username" in arg:
|
|
|
|
|
|
return arg
|
|
|
|
|
|
for v in kwargs.values():
|
|
|
|
|
|
if isinstance(v, dict) and "id" in v and "username" in v:
|
|
|
|
|
|
return v
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_request(args: tuple) -> Optional[Request]:
|
|
|
|
|
|
"""从函数参数中提取 Request 对象"""
|
|
|
|
|
|
for arg in args:
|
|
|
|
|
|
if isinstance(arg, Request):
|
|
|
|
|
|
return arg
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_param(kwargs: dict, key: str, default: str = "") -> str:
|
|
|
|
|
|
"""安全获取参数值"""
|
|
|
|
|
|
val = kwargs.get(key, default)
|
|
|
|
|
|
if val is None:
|
|
|
|
|
|
return default
|
|
|
|
|
|
return str(val)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_log(
|
|
|
|
|
|
module: str,
|
|
|
|
|
|
action: str,
|
|
|
|
|
|
target_type: str,
|
|
|
|
|
|
target_id: Optional[str],
|
|
|
|
|
|
target_name: Optional[str],
|
|
|
|
|
|
status: str,
|
|
|
|
|
|
error_message: str,
|
|
|
|
|
|
error_type: str,
|
|
|
|
|
|
error_traceback: str,
|
|
|
|
|
|
func_name: str,
|
|
|
|
|
|
detail: str,
|
|
|
|
|
|
user: Optional[dict],
|
|
|
|
|
|
request: Optional[Request],
|
|
|
|
|
|
trace_id: str,
|
|
|
|
|
|
duration_ms: float,
|
|
|
|
|
|
) -> None:
|
2026-08-19 17:26:49 +08:00
|
|
|
|
"""写入操作日志到数据库 + 文件日志"""
|
|
|
|
|
|
user_id = user.get("id") if user else None
|
|
|
|
|
|
username = user.get("username") if user else None
|
|
|
|
|
|
client_ip = None
|
|
|
|
|
|
req_method = None
|
|
|
|
|
|
req_path = None
|
|
|
|
|
|
if request:
|
|
|
|
|
|
client_ip = request.client.host if request.client else None
|
|
|
|
|
|
req_method = request.method
|
|
|
|
|
|
req_path = request.url.path
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 写文件日志(app-biz)----
|
|
|
|
|
|
log_fields = {
|
|
|
|
|
|
"bizModule": module,
|
|
|
|
|
|
"action": action,
|
|
|
|
|
|
"targetType": target_type or "",
|
|
|
|
|
|
"targetId": target_id or "",
|
|
|
|
|
|
"targetName": target_name or "",
|
|
|
|
|
|
"opStatus": status,
|
|
|
|
|
|
"durationMs": round(duration_ms, 2),
|
|
|
|
|
|
"username": username or "",
|
|
|
|
|
|
}
|
|
|
|
|
|
if detail:
|
|
|
|
|
|
log_fields["detail"] = detail[:500]
|
|
|
|
|
|
if error_message:
|
|
|
|
|
|
log_fields["errorMessage"] = error_message[:500]
|
|
|
|
|
|
if error_type:
|
|
|
|
|
|
log_fields["errorType"] = error_type
|
|
|
|
|
|
|
|
|
|
|
|
if status == OpStatus.SUCCESS:
|
|
|
|
|
|
biz_logger.info(f"{module}:{action} {status} - {target_name or target_id or ''}", **log_fields)
|
|
|
|
|
|
elif status == OpStatus.FAILURE:
|
|
|
|
|
|
biz_logger.error(f"{module}:{action} {status} - {target_name or target_id or ''} - {error_type}: {error_message[:200]}", **log_fields)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 写数据库 ----
|
2026-08-18 14:49:12 +08:00
|
|
|
|
try:
|
|
|
|
|
|
store = get_platform_store()
|
|
|
|
|
|
log_id = new_id("op")
|
|
|
|
|
|
with store.connect() as conn:
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO operation_logs
|
|
|
|
|
|
(id, user_id, username, module, action, target_type, target_id,
|
|
|
|
|
|
target_name, status, error_message, error_type, error_traceback,
|
|
|
|
|
|
func_name, detail, client_ip,
|
|
|
|
|
|
request_method, request_path, trace_id, duration_ms, create_time)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
|
""",
|
|
|
|
|
|
(
|
|
|
|
|
|
log_id, user_id, username, module, action,
|
|
|
|
|
|
target_type or None, target_id, target_name,
|
|
|
|
|
|
status,
|
|
|
|
|
|
error_message[:1000] if error_message else None,
|
|
|
|
|
|
error_type or None,
|
|
|
|
|
|
error_traceback[:5000] if error_traceback else None,
|
|
|
|
|
|
func_name or None,
|
|
|
|
|
|
detail[:2000] if detail else None,
|
|
|
|
|
|
client_ip, req_method, req_path, trace_id,
|
|
|
|
|
|
round(duration_ms, 2), utcnow(),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
2026-08-19 17:26:49 +08:00
|
|
|
|
logger.error("写入操作日志到数据库失败 module=%s action=%s", module, action, exc_info=True)
|