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>
435 lines
12 KiB
Markdown
435 lines
12 KiB
Markdown
# 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 传递
|
||
# 直接使用 logging,GuiTextHandler 会处理
|
||
```
|
||
|
||
### 长期重构(破坏性变更)
|
||
|
||
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` - 重构总结文档
|