123 lines
3.7 KiB
Python
123 lines
3.7 KiB
Python
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
|