271 lines
8.7 KiB
Python
271 lines
8.7 KiB
Python
|
|
"""
|
|||
|
|
日志模块
|
|||
|
|
|
|||
|
|
Python: =========================
|
|||
|
|
|
|||
|
|
from umi_log import logger
|
|||
|
|
|
|||
|
|
logger.debug("调试信息")
|
|||
|
|
logger.info("普通信息")
|
|||
|
|
logger.warning("警告信息")
|
|||
|
|
logger.error("错误信息"))
|
|||
|
|
logger.critical("严重错误信息")
|
|||
|
|
|
|||
|
|
# exc_info 只能在 except 块中开启
|
|||
|
|
logger.error("错误信息", exc_info=True, stack_info=True)
|
|||
|
|
# 覆盖 LogRecord 的属性
|
|||
|
|
logger.debug("信息", extra={"cover": {"filename": "test.txt", "lineno": 999}}
|
|||
|
|
|
|||
|
|
Qml: =========================
|
|||
|
|
|
|||
|
|
console.log("调试信息")
|
|||
|
|
console.info("普通信息")
|
|||
|
|
console.warn("警告信息")
|
|||
|
|
console.error("错误信息")
|
|||
|
|
console.trace() // 堆栈信息,级别debug,含函数名、文件名、行号
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
from datetime import datetime
|
|||
|
|
from logging.handlers import RotatingFileHandler
|
|||
|
|
from logging import LogRecord
|
|||
|
|
|
|||
|
|
# 保存的日志级别,可在UI修改
|
|||
|
|
Save_Log_Level: int = logging.ERROR
|
|||
|
|
|
|||
|
|
# 日志保存目录
|
|||
|
|
Logs_Dir = "./logs"
|
|||
|
|
Logs_Dir = os.path.abspath(Logs_Dir)
|
|||
|
|
|
|||
|
|
# 日志级别,对应的int值由小到大
|
|||
|
|
_Log_Levels = {
|
|||
|
|
"DEBUG": logging.DEBUG,
|
|||
|
|
"INFO": logging.INFO,
|
|||
|
|
"WARNING": logging.WARNING,
|
|||
|
|
"ERROR": logging.ERROR,
|
|||
|
|
"CRITICAL": logging.CRITICAL,
|
|||
|
|
"NONE": logging.CRITICAL + 10, # 最大,表示不记录日志
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 忽略日志列表,屏蔽QT框架自发产生的一些日志。(目前仅对 get_qt_message_handler 生效)
|
|||
|
|
Log_Ignore_List = [
|
|||
|
|
"Retrying to obtain clipboard.", # https://bugreports.qt.io/browse/QTBUG-97930
|
|||
|
|
"Unable to obtain clipboard.",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 覆盖过滤器
|
|||
|
|
class _CoverFilter(logging.Filter):
|
|||
|
|
def filter(self, record: LogRecord):
|
|||
|
|
try:
|
|||
|
|
# 提取自定义信息,覆盖给 record
|
|||
|
|
cover = record.__dict__.get("cover", {})
|
|||
|
|
for k, v in cover.items():
|
|||
|
|
if hasattr(record, k):
|
|||
|
|
setattr(record, k, v)
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
logger.error("日志过滤错误", exc_info=True, stack_info=True)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 简化日志级别符号的格式化器
|
|||
|
|
class _LevelFormatter(logging.Formatter):
|
|||
|
|
# 定义日志级别和对应符号的映射
|
|||
|
|
LEVEL_SYMBOLS = {
|
|||
|
|
"DEBUG": " ",
|
|||
|
|
"INFO": "√",
|
|||
|
|
"WARNING": "?",
|
|||
|
|
"ERROR": "×",
|
|||
|
|
"CRITICAL": "×××",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def format(self, record):
|
|||
|
|
# 获取符号,如果没有定义则使用默认级别名称
|
|||
|
|
levelname = record.levelname
|
|||
|
|
record.levelsymbol = self.LEVEL_SYMBOLS.get(levelname, levelname)
|
|||
|
|
return super().format(record)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# json 日志文件处理器
|
|||
|
|
class _JsonRotatingFileHandler(RotatingFileHandler):
|
|||
|
|
# 日志信息转字典
|
|||
|
|
def _record_to_dict(self, record: LogRecord):
|
|||
|
|
# 时间戳格式化
|
|||
|
|
dt_object = datetime.fromtimestamp(record.created)
|
|||
|
|
formatted_time = dt_object.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|||
|
|
# 构造消息字典
|
|||
|
|
log_dict = {
|
|||
|
|
# 时间
|
|||
|
|
"time": formatted_time,
|
|||
|
|
# 日志级别 ( DEBUG, INFO, WARNING, ERROR, CRITICAL )
|
|||
|
|
"level": record.levelname,
|
|||
|
|
# 日志消息
|
|||
|
|
"message": record.getMessage(),
|
|||
|
|
# =====
|
|||
|
|
# 代码所在文件
|
|||
|
|
"filename": record.filename,
|
|||
|
|
# 代码行号
|
|||
|
|
"lineno": record.lineno,
|
|||
|
|
# 模块名
|
|||
|
|
"module": record.module,
|
|||
|
|
# 函数名
|
|||
|
|
"funcName": record.funcName,
|
|||
|
|
# 异常信息,需在 except 块中开启 exc_info=True
|
|||
|
|
"exc_text": record.exc_text,
|
|||
|
|
# 堆栈信息,需开启 stack_info=True
|
|||
|
|
"stack_info": record.stack_info,
|
|||
|
|
# =====
|
|||
|
|
# 线程标识符
|
|||
|
|
"thread": record.thread,
|
|||
|
|
# 线程名称
|
|||
|
|
"threadName": record.threadName,
|
|||
|
|
# 进程标识符
|
|||
|
|
"process": record.process,
|
|||
|
|
# 进程名称
|
|||
|
|
"processName": record.processName,
|
|||
|
|
# 日志记录器的名称
|
|||
|
|
"name": record.name,
|
|||
|
|
}
|
|||
|
|
return log_dict
|
|||
|
|
|
|||
|
|
# 发送日志
|
|||
|
|
def emit(self, record: LogRecord):
|
|||
|
|
# 跳过忽略等级
|
|||
|
|
if record.levelno < Save_Log_Level:
|
|||
|
|
return
|
|||
|
|
# 检查文件大小并进行轮转
|
|||
|
|
if self.shouldRollover(record):
|
|||
|
|
self.doRollover()
|
|||
|
|
# 日志信息转字典
|
|||
|
|
try:
|
|||
|
|
log_dict = self._record_to_dict(record)
|
|||
|
|
except Exception:
|
|||
|
|
self.handleError(record)
|
|||
|
|
# 输出到日志文件
|
|||
|
|
try:
|
|||
|
|
with open(self.baseFilename, "a", encoding=self.encoding) as f:
|
|||
|
|
json.dump(log_dict, f, ensure_ascii=False)
|
|||
|
|
f.write("\n")
|
|||
|
|
except Exception:
|
|||
|
|
self.handleError(record)
|
|||
|
|
# TODO: 输出到UI界面
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 日志记录器 管理类
|
|||
|
|
class _LogManager:
|
|||
|
|
@staticmethod # 控制台处理器
|
|||
|
|
def _get_console_handler():
|
|||
|
|
# 显式规定输出到 stderr ,避免干涉命令行使用
|
|||
|
|
console_handler = logging.StreamHandler(sys.stderr)
|
|||
|
|
console_handler.setLevel(logging.DEBUG)
|
|||
|
|
fmt = "%(asctime)s %(levelsymbol)s %(funcName)s | %(message)s"
|
|||
|
|
formatter = _LevelFormatter(fmt, datefmt="%H:%M:%S") # 使用自定义格式化器
|
|||
|
|
console_handler.setFormatter(formatter)
|
|||
|
|
return console_handler
|
|||
|
|
|
|||
|
|
@staticmethod # json处理器,输出到本地文件及UI
|
|||
|
|
def _get_json_handler():
|
|||
|
|
# 确保日志目录存在
|
|||
|
|
if not os.path.exists(Logs_Dir):
|
|||
|
|
os.makedirs(Logs_Dir)
|
|||
|
|
# 获取当前日期
|
|||
|
|
current_date = datetime.now().strftime("%Y-%m-%d")
|
|||
|
|
# 构造日志文件路径
|
|||
|
|
log_file = os.path.join(Logs_Dir, f"log_{current_date}.jsonl.txt")
|
|||
|
|
# 创建json处理器
|
|||
|
|
json_handler = _JsonRotatingFileHandler(
|
|||
|
|
log_file,
|
|||
|
|
mode="a", # 追加写入
|
|||
|
|
maxBytes=10485760, # 单个文件最大:10MB
|
|||
|
|
backupCount=3, # 文件备份数量
|
|||
|
|
encoding="utf-8", # 文件编码
|
|||
|
|
delay=True, # 延迟创建文件
|
|||
|
|
)
|
|||
|
|
json_handler.setLevel(logging.DEBUG)
|
|||
|
|
return json_handler
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def create_logger(name):
|
|||
|
|
"""创建并返回一个新的日志记录器。"""
|
|||
|
|
logger = logging.getLogger(name)
|
|||
|
|
logger.addFilter(_CoverFilter()) # 添加覆盖过滤器
|
|||
|
|
logger.setLevel(logging.DEBUG)
|
|||
|
|
logger.addHandler(_LogManager._get_console_handler())
|
|||
|
|
logger.addHandler(_LogManager._get_json_handler())
|
|||
|
|
return logger
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 全局单例日志记录器
|
|||
|
|
logger = _LogManager.create_logger("Umi-OCR")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 获取 QT 日志重定向器
|
|||
|
|
def get_qt_message_handler():
|
|||
|
|
# 确保在初次调用时才导入QT模块
|
|||
|
|
from PySide2.QtCore import QtMsgType, QMessageLogContext
|
|||
|
|
|
|||
|
|
# 处理 QT (QML) 抛出的日志
|
|||
|
|
def qt_message_handler(mode: QtMsgType, context: QMessageLogContext, msg: str):
|
|||
|
|
try:
|
|||
|
|
if msg in Log_Ignore_List:
|
|||
|
|
return
|
|||
|
|
# 提取信息
|
|||
|
|
filepath = getattr(context, "file", "?")
|
|||
|
|
if filepath:
|
|||
|
|
filename = os.path.basename(filepath)
|
|||
|
|
else:
|
|||
|
|
filepath = filename = "?"
|
|||
|
|
funcName = getattr(context, "function", "?")
|
|||
|
|
if not funcName: # 匿名函数
|
|||
|
|
funcName = r"()=>{}"
|
|||
|
|
# 覆盖字典
|
|||
|
|
extra = {
|
|||
|
|
"cover": {
|
|||
|
|
"category": getattr(context, "category", "?"),
|
|||
|
|
"filename": filename,
|
|||
|
|
"funcName": funcName,
|
|||
|
|
"lineno": getattr(context, "line", "?"),
|
|||
|
|
"version": getattr(context, "version", "?"),
|
|||
|
|
"module": "qml",
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if mode == QtMsgType.QtDebugMsg:
|
|||
|
|
logger.debug(msg, extra=extra)
|
|||
|
|
elif mode == QtMsgType.QtInfoMsg:
|
|||
|
|
logger.info(msg, extra=extra)
|
|||
|
|
elif mode == QtMsgType.QtWarningMsg:
|
|||
|
|
logger.warning(msg, extra=extra)
|
|||
|
|
elif mode == QtMsgType.QtCriticalMsg:
|
|||
|
|
logger.error(msg, extra=extra)
|
|||
|
|
elif mode == QtMsgType.QtFatalMsg:
|
|||
|
|
logger.critical(msg, extra=extra)
|
|||
|
|
except Exception:
|
|||
|
|
logger.warning(
|
|||
|
|
"qt_message_handler error",
|
|||
|
|
exc_info=True,
|
|||
|
|
stack_info=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return qt_message_handler
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 更改保存的日志级别,成功返回T
|
|||
|
|
def change_save_log_level(levelname):
|
|||
|
|
global Save_Log_Level
|
|||
|
|
if levelname in _Log_Levels.keys():
|
|||
|
|
Save_Log_Level = _Log_Levels[levelname]
|
|||
|
|
logger.info(f"设置保存日志级别: {levelname}")
|
|||
|
|
return True
|
|||
|
|
logger.error(f"设置保存日志级别 {levelname} 失败。")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 打开日志保存目录
|
|||
|
|
def open_logs_dir():
|
|||
|
|
os.startfile(Logs_Dir)
|