from __future__ import annotations import logging import os from datetime import datetime from typing import Optional # stdlib 级别 → PQLogGUI 标签 _LEVEL_TO_GUI = { logging.DEBUG: "debug", logging.INFO: "info", logging.WARNING: "warning", logging.ERROR: "error", logging.CRITICAL: "error", } # PQLogGUI 标签 → stdlib 级别 GUI_LEVEL_TO_LOG = { "debug": logging.DEBUG, "info": logging.INFO, "success": logging.INFO, "warning": logging.WARNING, "error": logging.ERROR, "separator": logging.INFO, "blank": logging.DEBUG, } # 用于在 LogRecord 上做 GUI 来源标记,避免回环 _FROM_GUI_FLAG = "_from_gui" # 文件日志:头一行元信息,第二行正文,记录之间空行隔开,方便阅读 _FILE_LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s\n%(message)s\n" _DATE_FORMAT = "%Y-%m-%d %H:%M:%S" _initialized = False def _resolve_log_dir(log_dir: Optional[str]) -> str: if log_dir: return log_dir # 默认放在工作目录下的 log/,方便用户直接打开查看 return os.path.join(os.getcwd(), "log") def setup_logging( level: int = logging.INFO, log_dir: Optional[str] = None, ) -> str: """配置全局 logging,返回日志目录。可重复调用,第二次起为空操作。 - 不再向终端输出,避免污染控制台。 - 日志文件按日期命名:``log/YYYY-MM-DD.log``。 """ global _initialized root = logging.getLogger() if _initialized: return _resolve_log_dir(log_dir) resolved = _resolve_log_dir(log_dir) os.makedirs(resolved, exist_ok=True) file_formatter = logging.Formatter(_FILE_LOG_FORMAT, datefmt=_DATE_FORMAT) root.setLevel(level) # 移除既有 handler,避免重复 / basicConfig 默认控制台输出 for handler in list(root.handlers): root.removeHandler(handler) today = datetime.now().strftime("%Y-%m-%d") log_file = os.path.join(resolved, f"{today}.log") file_handler = logging.FileHandler(log_file, encoding="utf-8", delay=True) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) root.addHandler(file_handler) _initialized = True logging.getLogger(__name__).info("日志系统初始化完成 dir=%s", resolved) return resolved class TkLogHandler(logging.Handler): """把 stdlib logging 的记录转发到 ``PQLogGUI`` 面板。 带 ``_from_gui`` 标记的记录会被忽略,避免 GUI → file → GUI 回环。 """ def __init__(self, log_gui): super().__init__() self._log_gui = log_gui def emit(self, record: logging.LogRecord) -> None: # noqa: D401 if getattr(record, _FROM_GUI_FLAG, False): return try: message = self.format(record) except Exception: try: message = record.getMessage() except Exception: return gui_level = _LEVEL_TO_GUI.get(record.levelno, "info") try: # PQLogGUI.log 内部已处理跨线程 self._log_gui.log(message, level=gui_level, _from_logging=True) except Exception: self.handleError(record) def attach_gui_handler(log_gui) -> TkLogHandler: """把 ``PQLogGUI`` 注册为 root logger 的 handler。已存在则替换。""" root = logging.getLogger() # 移除旧的 TkLogHandler,保证只挂一个 for h in list(root.handlers): if isinstance(h, TkLogHandler): root.removeHandler(h) handler = TkLogHandler(log_gui) handler.setLevel(logging.INFO) handler.setFormatter(logging.Formatter("%(name)s: %(message)s")) root.addHandler(handler) return handler