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

435 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GUI 日志工作机制说明
## 概述
本文档说明了 ERP 自动化工具中 GUI 日志系统的工作机制,包括日志从产生到显示的完整流程。
## 架构概览
```mermaid
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`
**日志输出方式**:
```python
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)
**职责**: 用户交互和业务逻辑调用
**日志处理**:
```python
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)
```
**初始化**:
```python
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 组件
**职责**: 日志显示和格式化
**核心方法**:
```python
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') # 自动滚动到底部
```
**级别颜色映射**:
```python
LOG_COLORS = {
'INFO': '#000000', # 黑色
'SUCCESS': '#008000', # 绿色
'WARNING': '#FF8C00', # 深橙色
'ERROR': '#FF0000', # 红色
'DEBUG': '#808080', # 灰色
}
```
### 3. Logging 框架层
#### 3.1 Log Config (gui/log_config.py)
**职责**: 全局日志配置
```python
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
**核心逻辑**:
```python
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)
```
**清理冗余级别前缀**:
```python
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
```mermaid
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
```mermaid
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: 消息中包含级别前缀
**现状**:
```python
# Utils 代码
logger.info("[INFO] 读取 ProductionID 文件")
```
**影响**:
- 消息格式不统一
- 需要额外的清理逻辑
**建议**:
```python
# 推荐做法
logger.info("读取 ProductionID 文件") # 不包含级别前缀
```
### 问题 3: 线程同步复杂度
**现状**:
- Utils 脚本在后台线程执行
- 使用 `progress_callback` 线程安全地更新 GUI
- GuiTextHandler 也使用 `after()` 确保主线程更新
**影响**:
- 两次线程转换
- 代码路径复杂
**建议**:
- 统一使用 logging 模块
- 利用 logging 的线程安全特性
- GuiTextHandler 内部处理线程同步
## 改进建议
### 短期优化(保持兼容)
1. **统一 Utils 脚本的日志格式**
```python
# 当前
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**
```python
# 当前
self._report_progress("log", 0, 0, message, log_level=level.upper())
# 改进:移除 log 级别通过 progress_callback 传递
# 直接使用 loggingGuiTextHandler 会处理
```
### 长期重构(破坏性变更)
1. **移除 progress_callback 中的日志路径**
- Utils 脚本只使用 logging 模块
- GuiTextHandler 统一处理控制台和 GUI 输出
2. **配置化日志目标**
```python
# 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` | 冗余级别清理测试 |
### 参考文档
- [Python logging 模块文档](https://docs.python.org/3/library/logging.html)
- [Tkinter 线程安全最佳实践](https://docs.python.org/3/library/tkinter.html#thread-safety)
- `docs/LOGGING_REFACTORING_SUMMARY.md` - 重构总结文档