1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-17 23:13:03 +08:00

feat(LogManager): 新增日志持久化功能

- 新增 LogManager 单例类,支持日志文件按日期滚动
- 创建 CallerInfoFormatter 自定义格式化器,提取真实调用位置
- 为 MsgBase._showTrace 方法添加日志级别参数,集成日志系统
- 新增 initializeLogManager 初始化函数,日志存储于 AppDataLocation/logs/
- 日志输出格式对齐:[时间] - [类名(15)|级别(8)] - [文件:行号(20:4)] - 消息
- 控制台/INFO级别,全量日志 / DEBUG 级别,错误日志 / ERROR级别
- 全量日志保留7天,错误日志保留14天
This commit is contained in:
2026-03-17 21:37:24 +08:00
parent 1d99ca92f2
commit c26f19b6b3
3 changed files with 214 additions and 3 deletions
+12 -2
View File
@@ -16,7 +16,8 @@ from PySide6.QtWidgets import QApplication
from gui.ALMainWindow import ALMainWindow
from gui.resources import ALResource
from utils.ConfigManager import instance
from utils.ConfigManager import instance as configInstance
from utils.LogManager import instance as logInstance
def initializeConfigManager():
@@ -25,7 +26,15 @@ def initializeConfigManager():
config_dir = os.path.join(app_dir, "config")
if not QDir(config_dir).exists():
QDir().mkpath(config_dir)
instance(config_dir)
configInstance(config_dir)
def initializeLogManager():
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
log_dir = os.path.join(app_dir, "logs")
if not QDir(log_dir).exists():
QDir().mkpath(log_dir)
logInstance(log_dir)
def main():
@@ -36,6 +45,7 @@ def main():
app.setStyle('Fusion')
app.setApplicationName("AutoLibrary")
initializeConfigManager()
initializeLogManager()
window = ALMainWindow()
window.show()
sys.exit(app.exec_())
+11 -1
View File
@@ -7,9 +7,12 @@ This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import logging
import queue
import datetime
from utils.LogManager import getLogger
class MsgBase:
"""
@@ -38,6 +41,10 @@ class MsgBase:
self._class_name = self.__class__.__name__
self._input_queue = input_queue
self._output_queue = output_queue
try:
self._logger = getLogger(self._class_name)
except RuntimeError:
self._logger = None
def _showMsg(
@@ -50,11 +57,14 @@ class MsgBase:
def _showTrace(
self,
msg: str
msg: str,
level: int = logging.INFO
):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
if self._logger:
self._logger.log(level, msg)
def _waitMsg(
+191
View File
@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import logging
import os
import threading
from logging.handlers import TimedRotatingFileHandler
from typing import Optional
class CallerInfoFormatter(logging.Formatter):
"""
Custom formatter to extract real caller information.
Skips MsgBase._showTrace to show the actual calling location.
Format:
- Logger name: left-aligned, max 15 chars
- Level name: left-aligned, max 8 chars
- Filename: left-aligned, max 20 chars
- Line number: left-aligned, max 4 digits
"""
def __init__(
self,
fmt=None,
datefmt=None,
style='%'
):
super().__init__(fmt, datefmt, style)
self.basefmt = fmt
def format(
self,
record
):
depth = 0
while depth < 10:
record.filename = os.path.basename(record.pathname)
if 'MsgBase.py' not in record.filename and record.funcName != '_showTrace':
break
if not hasattr(record, 'stack'):
record.stack = True
import traceback
record.stack_list = traceback.extract_stack()
depth += 1
if depth < len(record.stack_list):
frame = record.stack_list[-depth-1]
record.filename = os.path.basename(frame.filename)
record.lineno = frame.lineno
record.funcName = frame.name
record.name = record.name[-15:].ljust(15)
record.levelname = record.levelname.ljust(8)
record.filename = record.filename[-20:].ljust(20)
record.lineno = f"{record.lineno:04d}"
return super().format(record)
class LogManager:
"""
Log Manager Singleton Class
Args:
log_dir (str): The directory to store log files.
"""
def __init__(
self,
log_dir: str
):
self.__log_dir = os.path.abspath(log_dir)
self.__logger = None
self.__initialized = False
self.initialize()
def initialize(
self
):
if self.__initialized:
return
os.makedirs(self.__log_dir, exist_ok=True)
self.__logger = logging.getLogger("AutoLibrary")
self.__logger.setLevel(logging.DEBUG)
self.__logger.handlers.clear()
formatter = CallerInfoFormatter(
'[%(asctime)s] - [%(name)s] - [%(levelname)s] - [%(filename)s:%(lineno)s] - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
self.__logger.addHandler(console_handler)
all_log_file = os.path.join(self.__log_dir, "all.log")
file_handler_all = TimedRotatingFileHandler(
all_log_file,
when='midnight',
interval=1,
backupCount=7,
encoding='utf-8'
)
file_handler_all.suffix = "%Y-%m-%d.log"
file_handler_all.setLevel(logging.DEBUG)
file_handler_all.setFormatter(formatter)
self.__logger.addHandler(file_handler_all)
error_log_file = os.path.join(self.__log_dir, "error.log")
file_handler_error = TimedRotatingFileHandler(
error_log_file,
when='midnight',
interval=1,
backupCount=14,
encoding='utf-8'
)
file_handler_error.suffix = "%Y-%m-%d.log"
file_handler_error.setLevel(logging.ERROR)
file_handler_error.setFormatter(formatter)
self.__logger.addHandler(file_handler_error)
self.__initialized = True
def getLogger(
self,
name: Optional[str] = None
) -> logging.Logger:
if name:
return self.__logger.getChild(name)
return self.__logger
def setLevel(
self,
level: int
):
if self.__logger:
self.__logger.setLevel(level)
def logDir(
self
) -> str:
return self.__log_dir
# LogManager singleton instance.
_log_manager_instance = None
# Singleton instance lock.
_instance_lock = threading.Lock()
def instance(
log_dir: str = ""
) -> LogManager:
global _log_manager_instance
with _instance_lock:
if _log_manager_instance is None:
if not log_dir:
raise ValueError("LogManager initialization requires log_dir parameter")
_log_manager_instance = LogManager(log_dir)
else:
if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir):
raise ValueError("LogManager instance already initialized with a different log directory")
return _log_manager_instance
def getLogger(
name: Optional[str] = None
) -> logging.Logger:
if _log_manager_instance is None:
raise RuntimeError("LogManager not initialized, please call LogManager.instance(log_dir) first")
return _log_manager_instance.getLogger(name)