from __future__ import annotations import json import os import re import traceback from datetime import datetime from typing import Any from app.services.log_config import LOG_DATE_FORMAT, LOG_DIR, is_enabled MAX_LOG_FIELD_LENGTH = 20000 MAX_TRACEBACK_LENGTH = 12000 MODULE_LOG_ROOT = os.path.join(os.path.dirname(LOG_DIR), "ModuleGeneration") def _safe_module_name(module: str | None) -> str: value = str(module or "unknown_module").strip() or "unknown_module" value = re.sub(r"[^a-zA-Z0-9_.-]+", "_", value) return value[:120] or "unknown_module" def _safe_dump_value(value: Any) -> Any: """限制单字段长度,避免超长 base64 / 响应体把日志打爆。""" if value is None: return None if isinstance(value, str): if len(value) > MAX_LOG_FIELD_LENGTH: return value[:MAX_LOG_FIELD_LENGTH] + f"..." return value if isinstance(value, dict): return {str(k): _safe_dump_value(v) for k, v in value.items()} if isinstance(value, list): return [_safe_dump_value(v) for v in value] return value def build_exception_detail(exc: BaseException | None, extra: dict[str, Any] | None = None) -> dict[str, Any]: """构造统一异常日志 detail。日志方法必须吞异常,业务不能被日志影响。""" detail: dict[str, Any] = dict(extra or {}) if exc is not None: tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) if len(tb) > MAX_TRACEBACK_LENGTH: tb = tb[:MAX_TRACEBACK_LENGTH] + f"..." detail.update( { "exception_type": type(exc).__name__, "exception_message": str(exc), "traceback": tb, } ) return detail def _append_module_log(module: str, entry: dict[str, Any]) -> None: if not is_enabled(): return try: module_dir = os.path.join(MODULE_LOG_ROOT, _safe_module_name(module)) os.makedirs(module_dir, exist_ok=True) today = datetime.now().strftime(LOG_DATE_FORMAT) log_file = os.path.join(module_dir, f"{today}.log") with open(log_file, "a", encoding="utf-8") as f: f.write(json.dumps(entry, ensure_ascii=False, default=str) + "\n") except Exception: # 日志失败绝不能影响业务主流程。 pass def log_module_event_file( *, module: str, event_type: str, project_id: str | None = None, step_id: str | None = None, user_id: str | None = None, message: str | None = None, detail: dict[str, Any] | None = None, error: str | None = None, ) -> None: """记录模块流程事件到 JSONL 文件。 统一落盘目录:log/ModuleGeneration/{module}/YYYY-MM-DD.log 不再写 module_generation_events 表。 """ entry = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "log_type": "module_event", "module": module, "event_type": event_type, "project_id": project_id, "step_id": step_id, "user_id": user_id, "message": message, "detail": _safe_dump_value(detail or {}), "error": error, } _append_module_log(module, entry) def log_module_prompt_event( *, event_type: str, project_id: str, step_id: str, user_id: str, module: str, prompt_type: str, request: dict[str, Any] | None = None, response: dict[str, Any] | None = None, token_usage: dict[str, Any] | None = None, error: str | None = None, ) -> None: """记录模块 AI 提词/分析请求和响应到 JSONL 文件。""" entry = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "log_type": "module_prompt", "module": module, "event_type": event_type, "prompt_type": prompt_type, "project_id": project_id, "step_id": step_id, "user_id": user_id, "request": _safe_dump_value(request or {}), "response": _safe_dump_value(response or {}), "token_usage": _safe_dump_value(token_usage or {}), "error": error, } _append_module_log(module, entry) def log_module_error( *, module: str, event_type: str, project_id: str | None = None, step_id: str | None = None, user_id: str | None = None, message: str | None = None, detail: dict[str, Any] | None = None, error: str | None = None, exc: BaseException | None = None, ) -> None: """记录模块异常日志。 - 兼容原有 detail/error 参数。 - 新增 exc 后自动记录 exception_type、message、traceback。 - 日志写入失败会被底层吞掉,不影响主流程。 """ merged_detail = build_exception_detail(exc, detail) entry = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "log_type": "module_error", "module": module, "event_type": event_type, "project_id": project_id, "step_id": step_id, "user_id": user_id, "message": message, "detail": _safe_dump_value(merged_detail), "error": error if error is not None else (str(exc) if exc is not None else None), } _append_module_log(module, entry)