185 lines
5.0 KiB
Python
185 lines
5.0 KiB
Python
"""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))
|