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)
|