Files
archery/logger_manager.py

213 lines
7.6 KiB
Python
Raw Permalink Normal View History

2026-01-20 11:25:17 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
日志管理器模块
提供异步日志功能使用 QueueHandler + QueueListener
"""
import logging
from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
import queue
import os
import config
from version import VERSION
class LoggerManager:
"""日志管理器(单例)"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(LoggerManager, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
# 私有状态
self._log_queue = None
self._queue_listener = None
self._logger = None
self._initialized = True
# ==================== 状态访问(只读属性)====================
@property
def logger(self):
"""获取logger对象只读"""
return self._logger
@property
def log_queue(self):
"""获取日志队列(只读)"""
return self._log_queue
# ==================== 业务方法 ====================
def init_logging(self, log_level=logging.INFO, log_file=None, max_bytes=None, backup_count=None):
"""
初始化异步日志系统使用 QueueHandler + QueueListener
Args:
log_level: 日志级别默认 INFO
log_file: 日志文件路径默认使用 config.LOG_FILE
max_bytes: 单个日志文件最大大小字节默认使用 config.LOG_MAX_BYTES
backup_count: 保留的备份文件数量默认使用 config.LOG_BACKUP_COUNT
"""
if log_file is None:
log_file = config.LOG_FILE
if max_bytes is None:
max_bytes = config.LOG_MAX_BYTES
if backup_count is None:
backup_count = config.LOG_BACKUP_COUNT
try:
# 创建日志队列(无界队列)
self._log_queue = queue.Queue(-1)
# 确保日志文件所在的目录存在
log_dir = os.path.dirname(log_file)
if log_dir: # 如果日志路径包含目录
try:
os.makedirs(log_dir, exist_ok=True)
except Exception as e:
print(f"[WARN] 无法创建日志目录 {log_dir}: {e}")
# 尝试创建文件Handler带日志轮转
try:
file_handler = RotatingFileHandler(
log_file,
maxBytes=max_bytes,
backupCount=backup_count,
encoding='utf-8',
mode='a' # 追加模式,确保不覆盖
)
except Exception as e:
# 如果RotatingFileHandler不可用降级为普通FileHandler
print(f"[WARN] RotatingFileHandler不可用使用普通FileHandler: {e}")
try:
file_handler = logging.FileHandler(log_file, encoding='utf-8', mode='a')
except Exception as e2:
# 如果文件Handler创建失败只使用控制台Handler
print(f"[WARN] 无法创建文件Handler仅使用控制台输出: {e2}")
file_handler = None
# 自定义Formatter包含版本信息
class CustomFormatter(logging.Formatter):
"""自定义日志格式,包含版本信息和行号"""
def format(self, record):
record.version = VERSION
return super().format(record)
# 如果file_handler存在设置格式和级别
if file_handler is not None:
file_handler.setFormatter(CustomFormatter(
'%(asctime)s [v%(version)s] [%(levelname)s] %(filename)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
file_handler.setLevel(log_level)
# 创建控制台Handler保留原有的print输出
console_handler = logging.StreamHandler()
console_handler.setFormatter(CustomFormatter(
'[v%(version)s] [%(levelname)s] %(filename)s:%(lineno)d - %(message)s'
))
console_handler.setLevel(log_level)
# 创建QueueListener后台线程处理日志写入
# 如果file_handler为None只使用console_handler
handlers = [console_handler]
if file_handler is not None:
handlers.append(file_handler)
self._queue_listener = QueueListener(
self._log_queue,
*handlers,
respect_handler_level=True
)
self._queue_listener.start()
# 创建QueueHandler用于记录日志
queue_handler = QueueHandler(self._log_queue)
# 配置根logger
self._logger = logging.getLogger()
self._logger.addHandler(queue_handler)
self._logger.setLevel(log_level)
# 避免日志向上传播到其他logger
self._logger.propagate = False
# 添加启动标记
self._logger.info("=" * 60)
self._logger.info("程序启动 - 日志系统初始化")
self._logger.info(f"版本: {VERSION}")
self._logger.info(f"日志文件: {log_file}")
self._logger.info("=" * 60)
return True
except Exception as e:
# 如果日志初始化失败,至少保证程序能运行
print(f"[ERROR] 日志系统初始化失败: {e}")
import traceback
try:
traceback.print_exc()
except:
pass
return False
def stop_logging(self):
"""停止日志系统(程序退出时调用)"""
try:
if self._logger:
# 确保所有日志都写入
self._logger.info("程序退出,正在保存日志...")
import time as std_time
std_time.sleep(0.5) # 给一点时间让日志写入
if self._queue_listener:
self._queue_listener.stop()
if self._logger:
# 等待队列中的日志处理完成
if self._log_queue:
import time as std_time
timeout = 5
start = std_time.time()
while not self._log_queue.empty() and (std_time.time() - start) < timeout:
std_time.sleep(0.1)
print("[LOG] 日志系统已停止")
except Exception as e:
print(f"[ERROR] 停止日志系统失败: {e}")
# 创建全局单例实例
logger_manager = LoggerManager()
# ==================== 向后兼容的函数接口 ====================
def init_logging(log_level=logging.INFO, log_file=None, max_bytes=None, backup_count=None):
"""初始化日志系统(向后兼容接口)"""
return logger_manager.init_logging(log_level, log_file, max_bytes, backup_count)
def stop_logging():
"""停止日志系统(向后兼容接口)"""
return logger_manager.stop_logging()
def get_logger():
"""
获取全局logger对象向后兼容接口
如果日志系统未初始化返回None此时可以使用print作为fallback
"""
return logger_manager.logger