ai-model-studio / src /utils /logger.py
slk1st's picture
Deploy AI Model Studio
8a03d2c verified
Raw
History Blame Contribute Delete
8.97 kB
"""
Vcore AI Proxy 日志系统
优化后的日志系统:
- 简洁、直观的彩色输出
- 自动提取模块名和上下文
- 增强的调试支持 (支持字典/JSON 自动美化)
- 兼容标准 logging 接口
"""
import logging
import sys
import os
import json
import uuid
from logging.handlers import RotatingFileHandler
from datetime import datetime
from typing import Any
from contextvars import ContextVar
# ==================== 上下文变量 ====================
request_id_var: ContextVar[str] = ContextVar('request_id', default='')
# 用于存储当前请求的元数据
request_info_var: ContextVar[dict[str, Any]] = ContextVar('request_info', default={})
# ==================== ANSI 颜色代码 ====================
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_YELLOW = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_MAGENTA = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
# 自定义级别
SUCCESS_LEVEL = 25
logging.addLevelName(SUCCESS_LEVEL, "SUCCESS")
LEVEL_CONFIG = {
logging.DEBUG: (Colors.DIM + Colors.CYAN, "🔍", "DEBUG"),
logging.INFO: (Colors.CYAN, "ℹ️ ", "INFO"),
SUCCESS_LEVEL: (Colors.BRIGHT_GREEN, "✅", "SUCCESS"),
logging.WARNING: (Colors.BRIGHT_YELLOW, "⚠️ ", "WARN"),
logging.ERROR: (Colors.BRIGHT_RED, "❌", "ERROR"),
logging.CRITICAL: (Colors.BOLD + Colors.RED, "💀", "FATAL"),
}
# 模块缩写映射,保持对齐美观
MODULE_ABBR = {
'vcore_client': 'Vcore',
'error_logger': 'ErrLog',
'diff_fixer': 'Diff',
'processor': 'Stream',
}
class BetterFormatter(logging.Formatter):
"""更美观的格式化器"""
def __init__(self, use_colors: bool = True):
super().__init__()
self.use_colors = use_colors and sys.stdout.isatty() and os.environ.get("NO_COLOR", "").strip().lower() not in ("1", "true", "yes", "on")
def _plain_text(self, text: str) -> str:
"""为不支持 ANSI/Emoji 的 Windows 控制台生成更稳的纯文本日志。"""
if os.environ.get("PLAIN_LOG", "").strip().lower() not in ("1", "true", "yes", "on"):
return text
return "".join(
ch for ch in text
if ord(ch) <= 0xFFFF and ch not in ("\ufe0f", "\u200d")
)
def format(self, record: logging.LogRecord) -> str:
# 1. 基本信息
now = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3]
level_tuple = LEVEL_CONFIG.get(record.levelno, (Colors.WHITE, "•", "LOG"))
# 显式解包
level_color = level_tuple[0]
level_icon = level_tuple[1]
level_name = level_tuple[2]
# 2. 模块名处理
module = record.name.split('.')[-1]
module = MODULE_ABBR.get(module, module[:8].capitalize())
# 3. 上下文信息 (Request ID)
req_id = request_id_var.get()
req_id_str = f" {Colors.DIM}|{Colors.RESET} {Colors.YELLOW}{req_id[:8]}{Colors.RESET}" if req_id else ""
# 4. 消息处理
message = self._plain_text(record.getMessage())
# 处理 debug_json 的 extra_data
extra_data = getattr(record, 'extra_data', None)
if extra_data is not None:
try:
# 保留原始消息(标签),然后附加格式化的JSON
formatted_json = json.dumps(extra_data, indent=2, ensure_ascii=False, default=str)
indented_json = "\n".join(f" {line}" for line in formatted_json.splitlines())
message += f"\n{indented_json}"
except Exception as e:
# 如果JSON序列化失败,附加错误信息
message += f" (JSON序列化失败: {e})"
# 处理直接传入的字典/列表消息
elif isinstance(record.msg, (dict, list)):
try:
formatted_json = json.dumps(record.msg, indent=2, ensure_ascii=False, default=str)
message = f"\n{formatted_json}"
# 缩进
message = "\n".join(f" {line}" for line in message.splitlines())
except Exception:
# 失败则保持原始消息
message = str(record.msg)
# 5. 异常处理
exc_text = ""
if record.exc_info:
exc_text = "\n" + self.formatException(record.exc_info)
# 缩进异常信息
exc_text = "\n".join(f" {line}" for line in exc_text.splitlines())
if self.use_colors:
return (
f"{Colors.DIM}{now}{Colors.RESET} "
f"{level_color}{level_icon} {level_name:<7}{Colors.RESET} "
f"{Colors.MAGENTA}[{module:^10}]{Colors.RESET}"
f"{req_id_str} "
f"{message}{exc_text}"
)
else:
return self._plain_text(f"{now} {level_name:<7} [{module:^10}] {req_id[:8] if req_id else ''} {message}{exc_text}")
class LoggerManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized: return
self._initialized = True
self._log_level = logging.INFO
self._setup_root_logger()
def _setup_root_logger(self):
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.handlers.clear()
# Console Handler
console = logging.StreamHandler(sys.stdout)
console.setFormatter(BetterFormatter())
console.setLevel(self._log_level)
root.addHandler(console)
# 屏蔽噪音
for logger_name in ['httpx', 'httpcore', 'uvicorn', 'fastapi', 'hpack', 'h2', 'uvicorn.error', 'uvicorn.access']:
l = logging.getLogger(logger_name)
l.setLevel(logging.WARNING)
l.propagate = True # 确保 uvicorn 的日志能传递到 root logger
def configure(
self,
debug: bool = False,
log_file: str | None = None,
max_bytes: int = 10 * 1024 * 1024,
backup_count: int = 3,
):
self._log_level = logging.DEBUG if debug else logging.INFO
root = logging.getLogger()
for h in root.handlers:
h.setLevel(self._log_level)
if log_file:
os.makedirs(os.path.dirname(log_file), exist_ok=True)
file_h = RotatingFileHandler(
log_file,
mode='a',
maxBytes=max_bytes,
backupCount=backup_count,
encoding='utf-8',
delay=False,
)
file_h.setFormatter(BetterFormatter(use_colors=False))
file_h.setLevel(self._log_level)
# 立即刷新,确保日志写入
file_h.flush()
root.addHandler(file_h)
class LoggerAdapter(logging.LoggerAdapter): # type: ignore[type-arg]
"""增加便捷方法的 Logger 适配器"""
def success(self, msg: object, *args: object, **kwargs: Any) -> None:
self.log(SUCCESS_LEVEL, msg, *args, **kwargs)
def debug_json(self, label: str, data: Any) -> None:
"""专门用于调试 JSON 数据"""
if getattr(self.logger, "isEnabledFor", lambda x: False)(logging.DEBUG):
try:
# 只有在 debug 模式下才进行复杂的序列化操作
formatted_data = json.loads(json.dumps(data, default=str)) if not isinstance(data, (dict, list)) else data
self.logger._log(logging.DEBUG, f"{label}:", (), extra={'extra_data': formatted_data})
except Exception as e:
self.logger._log(logging.DEBUG, f"{label} (JSON解析失败: {e})", ())
def debug_large(self, label: str, data: str) -> None:
"""专门用于调试大文本数据"""
if getattr(self.logger, "isEnabledFor", lambda x: False)(logging.DEBUG):
self.logger.debug(f"{label}:\n{data}")
def get_logger(name: str) -> LoggerAdapter:
logger = logging.getLogger(name)
return LoggerAdapter(logger, {})
# 全局便捷实例
manager = LoggerManager()
def configure_logging(debug: bool = False, log_dir: str = "logs"):
log_path = os.path.join(log_dir, "app.log") if log_dir else None
manager.configure(debug=debug, log_file=log_path)
def set_request_id(request_id: str | None = None):
rid = request_id or uuid.uuid4().hex[:12]
request_id_var.set(rid)
return rid
def get_request_id() -> str:
return request_id_var.get()
def clear_context():
request_id_var.set('')
request_info_var.set({})