"""VP V3 虚拟素材库专用日志服务。 统一记录所有 VP V3 相关操作日志到 logs/virtual_portrait_v3/ 目录。 按天分文件,便于管理和排查问题。 """ import json import logging import os from datetime import datetime, timezone from app.config import settings # === 日志目录 === BASE_LOG_DIR = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))), "log", "virtual_portrait_v3", ) os.makedirs(BASE_LOG_DIR, exist_ok=True) class _DailyFileHandler(logging.Handler): """按天写入不同日志文件的处理器。""" def __init__(self, log_dir: str): super().__init__() self.log_dir = log_dir self._current_date = None self._file_handler = None self._open_file() def _open_file(self): """打开当天的日志文件。""" now = datetime.now(timezone.utc) date_str = now.strftime("%Y-%m-%d") if date_str == self._current_date and self._file_handler: return if self._file_handler: self._file_handler.close() self._current_date = date_str filepath = os.path.join(self.log_dir, f"{date_str}.log") self._file_handler = open(filepath, "a", encoding="utf-8") def emit(self, record): try: self._open_file() msg = self.format(record) self._file_handler.write(msg + "\n") self._file_handler.flush() except Exception: self.handleError(record) def close(self): if self._file_handler: self._file_handler.close() super().close() def _create_logger(name: str, filename: str | None = None) -> logging.Logger: """创建专用 Logger。""" logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) # 避免重复添加 handler if logger.handlers: return logger # 按天写入文件 handler = _DailyFileHandler(BASE_LOG_DIR) handler.setLevel(logging.DEBUG) formatter = logging.Formatter( "%(asctime)s | %(levelname)s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) handler.setFormatter(formatter) logger.addHandler(handler) # 不向上传播到 root logger(避免重复输出到控制台) logger.propagate = False return logger # === 专用 Logger 实例 === asset_logger = _create_logger("vp_v3.asset") project_logger = _create_logger("vp_v3.project") quota_logger = _create_logger("vp_v3.quota") api_logger = _create_logger("vp_v3.api") def log_asset_event( event_type: str, api_key_id: str, asset_id: str | None = None, project_id: str | None = None, status: str | None = None, detail: dict | None = None, error: str | None = None, ): """记录素材相关事件。""" log_data = { "event_type": event_type, "api_key_id": api_key_id, "asset_id": asset_id, "project_id": project_id, "status": status, "detail": detail or {}, } if error: log_data["error"] = error asset_logger.error(json.dumps(log_data, ensure_ascii=False, default=str)) else: asset_logger.info(json.dumps(log_data, ensure_ascii=False, default=str)) def log_project_event( event_type: str, api_key_id: str, project_id: str | None = None, status: str | None = None, detail: dict | None = None, error: str | None = None, ): """记录项目相关事件。""" log_data = { "event_type": event_type, "api_key_id": api_key_id, "project_id": project_id, "status": status, "detail": detail or {}, } if error: log_data["error"] = error project_logger.error(json.dumps(log_data, ensure_ascii=False, default=str)) else: project_logger.info(json.dumps(log_data, ensure_ascii=False, default=str)) def log_quota_event( event_type: str, api_key_id: str, quota_type: str, amount: float, quota_before: float | None = None, quota_after: float | None = None, detail: dict | None = None, ): """记录配额相关事件。""" log_data = { "event_type": event_type, "api_key_id": api_key_id, "quota_type": quota_type, "amount": amount, "quota_before": quota_before, "quota_after": quota_after, "detail": detail or {}, } quota_logger.info(json.dumps(log_data, ensure_ascii=False, default=str)) def log_api_request( method: str, path: str, api_key_id: str, status_code: int, duration_ms: int, error: str | None = None, ): """记录 API 请求。""" log_data = { "method": method, "path": path, "api_key_id": api_key_id, "status_code": status_code, "duration_ms": duration_ms, } if error: log_data["error"] = error api_logger.error(json.dumps(log_data, ensure_ascii=False, default=str)) else: api_logger.info(json.dumps(log_data, ensure_ascii=False, default=str))