Files
playwrite/docs/LOGGING_MECHANISM.md
Misaka 24053c6a3b feat: implement unified logging system for GUI components
Add centralized logging mechanism that simultaneously outputs to console
and GUI log components, improving code maintainability and consistency.

Changes:
- Add gui/log_config.py for centralized logging configuration
- Add gui/widgets/log_handler.py as bridge between logging and LogText
- Integrate unified logging into DataExtractionTab and MaterialValidationTab
- Initialize logging system in MainWindow on startup
- Improve error messages in material_status_validator for empty results
- Add documentation for logging mechanism and refactoring

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 20:31:59 +08:00

12 KiB
Raw Blame History

GUI 日志工作机制说明

概述

本文档说明了 ERP 自动化工具中 GUI 日志系统的工作机制,包括日志从产生到显示的完整流程。

架构概览

graph TB
    subgraph Utils["Utils 脚本层"]
        A1[离散备料计划维护数据提取.py]
        A2[离散备料计划维护数据清理.py]
        A3[material_status_validator.py]
        A4[_log 方法]
    end

    subgraph GUI["GUI 层"]
        B1[DataExtractionTab]
        B2[MaterialValidationTab]
        B3[_update_log 方法]
        B4[LogText 组件]
    end

    subgraph Logging["Logging 框架"]
        C1[Python logging 模块]
        C2[GuiTextHandler]
    end

    A1 -->|logger.info| C1
    A2 -->|logger.info| C1
    A3 -->|logger.info| C1
    A1 -->|progress_callback| B1
    A2 -->|progress_callback| B2
    A3 -->|progress_callback| B2

    B1 -->|_update_log| C1
    B2 -->|_update_log| C1
    B1 -->|直接调用| B4
    B2 -->|直接调用| B4

    C1 -->|日志记录| C2
    C2 -->|清理消息| B4
    B4 -->|添加格式| Display[用户界面]

    style A1 fill:#e1f5ff
    style A2 fill:#e1f5ff
    style A3 fill:#e1f5ff
    style B1 fill:#fff4e1
    style B2 fill:#fff4e1
    style B4 fill:#e8f5e9
    style C1 fill:#f3e5f5
    style C2 fill:#f3e5f5

组件职责

1. Utils 脚本层

职责: 业务逻辑执行和日志产生

主要文件:

  • utils/离散备料计划维护数据提取.py
  • utils/离散备料计划维护数据清理.py
  • utils/material_status_validator.py

日志输出方式:

def _log(self, message, level="info"):
    """统一日志出口:同步分发到控制台和 UI 回调"""
    level = level.lower()
    # 方式1: 输出到控制台(添加级别标记)
    log_map = {
        "info": logger.info,
        "warn": logger.warning,
        "error": logger.error
    }
    log_func = log_map.get(level, logger.info)
    log_func(message)  # 输出: "2026-02-13 21:42:03 [INFO] message"

    # 方式2: 同步到 UI通过回调
    if self.progress_callback:
        self._report_progress("log", 0, 0, message, log_level=level.upper())

问题: 消息中可能包含 [INFO][ERROR] 等级别前缀

2. GUI 层

2.1 Tab 组件 (DataExtractionTab, MaterialValidationTab)

职责: 用户交互和业务逻辑调用

日志处理:

def _update_log(self, message: str, level: str = "INFO"):
    """线程安全的日志更新"""
    # 将自定义级别映射到 logging 级别
    level_upper = level.upper()
    if level_upper == "SUCCESS":
        self.logger.info(message)
    else:
        log_level = getattr(logging, level_upper, logging.INFO)
        self.logger.log(log_level, message)

初始化:

def __init__(self, parent, config: ConfigManager, main_window=None):
    # ...
    self.logger = get_logger(__name__)
    self._gui_handler = None  # 将在 _create_log_panel 中设置

def _create_log_panel(self, parent):
    self.log_text = LogText(parent, height=15, readonly=True)
    self.log_text.pack(fill=tk.BOTH, expand=True)

    # 设置 GUI 日志处理器
    self._gui_handler = GuiTextHandler(self.log_text)
    self._gui_handler.setFormatter(logging.Formatter(
        '%(asctime)s [%(levelname)s] %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    ))
    self.logger.addHandler(self._gui_handler)

2.2 LogText 组件

职责: 日志显示和格式化

核心方法:

def log(self, message: str, level: str = 'INFO') -> None:
    """添加日志消息"""
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    log_message = f"[{timestamp}] [{level}] {message}\n"

    # 插入文本并设置颜色
    tag = level.lower()
    self.text.insert('end', log_message, (tag,))
    self.text.see('end')  # 自动滚动到底部

级别颜色映射:

LOG_COLORS = {
    'INFO': '#000000',      # 黑色
    'SUCCESS': '#008000',   # 绿色
    'WARNING': '#FF8C00',   # 深橙色
    'ERROR': '#FF0000',     # 红色
    'DEBUG': '#808080',     # 灰色
}

3. Logging 框架层

3.1 Log Config (gui/log_config.py)

职责: 全局日志配置

def setup_gui_logging(level=logging.INFO):
    """初始化 GUI 应用的日志配置"""
    logging.basicConfig(
        level=level,
        format=LOG_FORMAT,      # '%(asctime)s [%(levelname)s] %(message)s'
        datefmt=DATE_FORMAT,   # '%Y-%m-%d %H:%M:%S'
        force=True
    )
    return logging.getLogger()

3.2 GuiTextHandler (gui/widgets/log_handler.py)

职责: 桥接 logging 模块和 GUI

核心逻辑:

class GuiTextHandler(logging.Handler):
    def emit(self, record: logging.LogRecord):
        """实现日志输出"""
        if not self.log_text:
            return

        try:
            # 1. 获取日志级别
            level = self.level_map.get(record.levelno, 'INFO')

            # 2. 获取纯消息内容(不含格式)
            message = record.getMessage()

            # 3. 移除冗余级别前缀(如 "[INFO] "
            message = self._strip_redundant_level_prefix(message)

            # 4. 线程安全地更新 GUI
            def update():
                self.log_text.log(message, level)

            # 5. 使用 after 确保在主线程更新
            widget.master.after(0, update)
        except Exception:
            self.handleError(record)

清理冗余级别前缀:

def _strip_redundant_level_prefix(self, message: str) -> str:
    """移除消息开头的冗余级别标记"""
    level_pattern = r'^\[(?:INFO|WARNING|ERROR|DEBUG|CRITICAL|WARN|SUCCESS)\]\s*'
    match = re.match(level_pattern, message)
    if match:
        return message[match.end():]
    return message

日志流程详解

场景 1: Utils 脚本 → 控制台 → GUI

sequenceDiagram
    participant U as Utils 脚本
    participant L as Logger
    participant C as Console
    participant G as GuiTextHandler
    participant T as LogText
    participant UI as 用户界面

    U->>U: _log("读取文件", "info")
    Note over U: 业务逻辑执行

    U->>L: logger.info("[INFO] 读取文件")
    Note over U,L: 1. 控制台输出

    L->>C: 2026-02-13 21:42:03 [INFO] [INFO] 读取文件
    Note over C: 控制台显示(可能有冗余级别)

    U->>G: progress_callback(log, message, log_level="INFO")
    Note over U,G: 2. UI 回调

    G->>G: _strip_redundant_level_prefix("[INFO] 读取文件")
    Note over G: 清理: "[INFO] " -> ""

    G->>T: log("读取文件", "INFO")
    Note over G,T: 纯净消息

    T->>UI: [2026-02-13 21:42:03] [INFO] 读取文件
    Note over UI: GUI 显示(格式统一)

场景 2: GUI 直接调用 → Logging → GUI

sequenceDiagram
    participant Tab as Tab 组件
    participant L as Logger
    participant G as GuiTextHandler
    participant T as LogText
    participant UI as 用户界面

    Tab->>L: _update_log("开始校验", "INFO")
    Note over Tab,L: 用户操作触发

    L->>L: logger.info("开始校验")

    L->>G: emit(LogRecord)
    Note over L,G: Logging 框架分发

    G->>G: record.getMessage() = "开始校验"
    Note over G: 获取纯消息

    G->>G: _strip_redundant_level_prefix("开始校验")
    Note over G: 检查并清理(此处无冗余)

    G->>T: log("开始校验", "INFO")
    Note over G,T: 跨线程调用

    T->>UI: [2026-02-13 21:42:03] [INFO] 开始校验
    Note over UI: GUI 显示

数据流转分析

消息内容的变化

阶段 消息内容 说明
Utils 原始消息 "读取 ProductionID 文件" 业务逻辑产生
logger.info() 后 "2026-02-13 21:42:03 [INFO] 读取 ProductionID 文件" 控制台格式化
progress_callback 传递 "读取 ProductionID 文件" 原始消息(可能含 [INFO] 前缀)
GuiTextHandler 处理后 "读取 ProductionID 文件" 移除冗余前缀
LogText.log() 添加 "[2026-02-13 21:42:03] [INFO] 读取 ProductionID 文件" GUI 格式化
用户界面显示 [2026-02-13 21:42:03] [INFO] 读取 ProductionID 文件 最终显示

级别映射

层级 级别值 说明
Utils _log() "info" / "warn" / "error" 小写字符串
progress_callback "INFO" / "WARNING" / "ERROR" 大写字符串
logging 模块 logging.INFO / logging.WARNING / logging.ERROR 整数常量
LogText 组件 "INFO" / "WARNING" / "ERROR" / "SUCCESS" 字符串
GuiTextHandler level_map 字典映射 logging.INFO -> 'INFO' 转换逻辑

当前问题分析

问题 1: 双重输出路径

现状: Utils 脚本同时通过两种方式输出日志

  1. logger.info(message) → 控制台
  2. progress_callback(log, message) → GUI

影响:

  • 控制台日志和 GUI 日志可能不一致
  • 增加维护复杂度

建议:

  • 统一使用 logging 模块
  • GuiTextHandler 自动输出到控制台和 GUI

问题 2: 消息中包含级别前缀

现状:

# Utils 代码
logger.info("[INFO] 读取 ProductionID 文件")

影响:

  • 消息格式不统一
  • 需要额外的清理逻辑

建议:

# 推荐做法
logger.info("读取 ProductionID 文件")  # 不包含级别前缀

问题 3: 线程同步复杂度

现状:

  • Utils 脚本在后台线程执行
  • 使用 progress_callback 线程安全地更新 GUI
  • GuiTextHandler 也使用 after() 确保主线程更新

影响:

  • 两次线程转换
  • 代码路径复杂

建议:

  • 统一使用 logging 模块
  • 利用 logging 的线程安全特性
  • GuiTextHandler 内部处理线程同步

改进建议

短期优化(保持兼容)

  1. 统一 Utils 脚本的日志格式

    # 当前
    def _log(self, message, level="info"):
        log_func(message)  # 可能包含 "[INFO] " 前缀
    
    # 改进
    def _log(self, message, level="info"):
        # 确保消息不包含级别前缀
        clean_message = self._strip_level_prefix(message)
        log_func(clean_message)
    
  2. 简化 progress_callback

    # 当前
    self._report_progress("log", 0, 0, message, log_level=level.upper())
    
    # 改进:移除 log 级别通过 progress_callback 传递
    # 直接使用 loggingGuiTextHandler 会处理
    

长期重构(破坏性变更)

  1. 移除 progress_callback 中的日志路径

    • Utils 脚本只使用 logging 模块
    • GuiTextHandler 统一处理控制台和 GUI 输出
  2. 配置化日志目标

    # config.py
    LOGGING = {
        'version': 1,
        'handlers': {
            'console': {'class': 'logging.StreamHandler'},
            'gui': {'class': 'GuiTextHandler', 'log_text': ...}
        },
        'root': {
            'handlers': ['console', 'gui']
        }
    }
    
  3. 统一级别系统

    • 移除自定义的 "SUCCESS" 级别
    • 使用标准的 logging.INFO + 额外的元数据

附录

相关文件清单

文件路径 职责
gui/log_config.py 日志配置
gui/widgets/log_handler.py GuiTextHandler
gui/widgets/log_text.py LogText 组件
gui/main_window.py 初始化日志系统
gui/material_validation_tab.py 物料校验标签页
gui/data_extraction_tab.py 数据提取标签页
utils/离散备料计划维护数据提取.py 业务逻辑 + _log
utils/离散备料计划维护数据清理.py 业务逻辑 + _log
utils/material_status_validator.py 业务逻辑 + _log

测试文件

文件路径 说明
tests/test_logging_simple.py 简单日志测试
tests/test_logging_system.py 完整 GUI 测试
tests/test_log_handler_fix.py 冗余级别清理测试

参考文档