Spaces:
Running
Running
| """ | |
| 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({}) | |