mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 15:33:03 +08:00
feat(LogManager): 新增日志持久化功能
This commit is contained in:
+4
-12
@@ -10,23 +10,15 @@ See the LICENSE file for details.
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PySide6.QtCore import QTranslator, QStandardPaths, QDir
|
from PySide6.QtCore import QTranslator
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from gui.ALMainWindow import ALMainWindow
|
from gui.ALMainWindow import ALMainWindow
|
||||||
from gui.resources import ALResource
|
from gui.resources import ALResource
|
||||||
|
|
||||||
from utils.ConfigManager import instance
|
from utils.AppInitializer import initializeApp
|
||||||
|
|
||||||
|
|
||||||
def initializeConfigManager():
|
|
||||||
|
|
||||||
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
|
||||||
config_dir = os.path.join(app_dir, "config")
|
|
||||||
if not QDir(config_dir).exists():
|
|
||||||
QDir().mkpath(config_dir)
|
|
||||||
instance(config_dir)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
@@ -35,12 +27,12 @@ def main():
|
|||||||
app.installTranslator(translator)
|
app.installTranslator(translator)
|
||||||
app.setStyle('Fusion')
|
app.setStyle('Fusion')
|
||||||
app.setApplicationName("AutoLibrary")
|
app.setApplicationName("AutoLibrary")
|
||||||
initializeConfigManager()
|
if not initializeApp():
|
||||||
|
sys.exit(-1)
|
||||||
window = ALMainWindow()
|
window = ALMainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
main()
|
main()
|
||||||
+11
-1
@@ -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.
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||||
See the LICENSE file for details.
|
See the LICENSE file for details.
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from utils.LogManager import getLogger
|
||||||
|
|
||||||
|
|
||||||
class MsgBase:
|
class MsgBase:
|
||||||
"""
|
"""
|
||||||
@@ -38,6 +41,10 @@ class MsgBase:
|
|||||||
self._class_name = self.__class__.__name__
|
self._class_name = self.__class__.__name__
|
||||||
self._input_queue = input_queue
|
self._input_queue = input_queue
|
||||||
self._output_queue = output_queue
|
self._output_queue = output_queue
|
||||||
|
try:
|
||||||
|
self._logger = getLogger(self._class_name)
|
||||||
|
except RuntimeError:
|
||||||
|
self._logger = None
|
||||||
|
|
||||||
|
|
||||||
def _showMsg(
|
def _showMsg(
|
||||||
@@ -50,11 +57,14 @@ class MsgBase:
|
|||||||
|
|
||||||
def _showTrace(
|
def _showTrace(
|
||||||
self,
|
self,
|
||||||
msg: str
|
msg: str,
|
||||||
|
level: int = logging.INFO
|
||||||
):
|
):
|
||||||
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||||
self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
|
self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
|
||||||
|
if self._logger:
|
||||||
|
self._logger.log(level, msg)
|
||||||
|
|
||||||
|
|
||||||
def _waitMsg(
|
def _waitMsg(
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
|
self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
|
||||||
self.__alConfigWidget.deleteLater()
|
self.__alConfigWidget.deleteLater()
|
||||||
self.__alConfigWidget = None
|
self.__alConfigWidget = None
|
||||||
|
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
|
||||||
self.setControlButtons(True, None, None)
|
self.setControlButtons(True, None, None)
|
||||||
|
|
||||||
@Slot(dict)
|
@Slot(dict)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (c) 2025 - 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 os
|
||||||
|
|
||||||
|
from PySide6.QtCore import QStandardPaths, QDir
|
||||||
|
|
||||||
|
from utils.ConfigManager import instance as configInstance
|
||||||
|
from utils.LogManager import instance as logInstance
|
||||||
|
|
||||||
|
|
||||||
|
def initializeConfigManager(
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||||
|
old_config_dir = os.path.join(app_dir, "config")
|
||||||
|
new_config_dir = os.path.join(app_dir, "configs")
|
||||||
|
if QDir(old_config_dir).exists(): # old config dir exists
|
||||||
|
#we rename it to compatible with new version
|
||||||
|
if not QDir().rename(old_config_dir, new_config_dir):
|
||||||
|
return False
|
||||||
|
elif not QDir(new_config_dir).exists():
|
||||||
|
if not QDir().mkpath(new_config_dir):
|
||||||
|
return False
|
||||||
|
configInstance(new_config_dir)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def initializeLogManager(
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||||
|
log_dir = os.path.join(app_dir, "logs")
|
||||||
|
if not QDir(log_dir).exists():
|
||||||
|
if not QDir().mkpath(log_dir):
|
||||||
|
return False
|
||||||
|
logInstance(log_dir)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def initializeApp(
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
if not initializeConfigManager():
|
||||||
|
return False
|
||||||
|
if not initializeLogManager():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user