log_manager.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import logging
  2. import os
  3. import sys
  4. import threading
  5. from logging.handlers import QueueHandler, QueueListener
  6. from queue import Queue
  7. from pathlib import Path
  8. def resource_path():
  9. """ 获取资源绝对路径,兼容开发环境和单文件模式 """
  10. if hasattr(sys, '_MEIPASS'):
  11. base_path = sys._MEIPASS
  12. else:
  13. base_path = os.path.abspath(".")
  14. return base_path
  15. class LogManager:
  16. _instance = None
  17. _lock = threading.Lock()
  18. _configured = False # 确保单例配置唯一性
  19. def __new__(cls, log_path=None):
  20. base_path = resource_path()
  21. with cls._lock:
  22. if not cls._instance:
  23. cls._instance = super().__new__(cls)
  24. # 路径处理逻辑
  25. if log_path is None:
  26. log_path = Path(base_path) / "app.log"
  27. cls._instance._full_path = log_path
  28. cls._instance._init_logger()
  29. return cls._instance
  30. @classmethod
  31. def _validate_path(cls, path):
  32. """路径验证与创建"""
  33. default_path = os.path.join(os.getcwd(), "logs")
  34. target_path = path or default_path
  35. try:
  36. os.makedirs(target_path, exist_ok=True)
  37. # 测试写入权限
  38. test_file = os.path.join(target_path, "write_test.tmp")
  39. with open(test_file, "w") as f:
  40. f.write("permission_test")
  41. os.remove(test_file)
  42. return target_path
  43. except PermissionError:
  44. logging.error(f"Insufficient permissions for {target_path}, using default")
  45. os.makedirs(default_path, exist_ok=True)
  46. return default_path
  47. except Exception as e:
  48. logging.error(f"Path error: {str(e)}, using default")
  49. return default_path
  50. @staticmethod
  51. def _sanitize_filename(name):
  52. """文件名合法性过滤"""
  53. invalid_chars = {'/', '\\', ':', '*', '?', '"', '<', '>', '|'}
  54. cleaned = ''.join(c for c in name if c not in invalid_chars)
  55. return cleaned[:50] # 限制文件名长度
  56. def _init_logger(self):
  57. """初始化日志系统 - 移除了控制台输出"""
  58. self.log_queue = Queue(-1)
  59. self.logger = logging.getLogger("GlobalLogger")
  60. self.logger.setLevel(logging.DEBUG)
  61. if not self.logger.handlers:
  62. # 创建带线程标识和行号的格式器
  63. formatter = logging.Formatter(
  64. "[%(asctime)s][%(levelname)s][%(threadName)s][%(filename)s:%(lineno)d] %(message)s"
  65. )
  66. # 文件处理器(自动UTF-8编码)
  67. file_handler = logging.FileHandler(
  68. self._full_path,
  69. encoding='utf-8',
  70. delay=True # 延迟打开文件直到实际写入
  71. )
  72. file_handler.setFormatter(formatter)
  73. # 异步监听器 - 仅包含文件处理器
  74. self.listener = QueueListener(
  75. self.log_queue,
  76. file_handler, # 只保留文件处理器
  77. respect_handler_level=True
  78. )
  79. self.listener.start()
  80. # 队列处理器配置
  81. queue_handler = QueueHandler(self.log_queue)
  82. queue_handler.setLevel(logging.DEBUG)
  83. self.logger.addHandler(queue_handler)
  84. self.logger.propagate = False
  85. def get_logger(self):
  86. """获取线程安全日志器"""
  87. return self.logger
  88. @classmethod
  89. def shutdown(cls):
  90. """安全关闭日志系统"""
  91. if cls._instance:
  92. cls._instance.listener.stop()
  93. cls._instance = None
  94. # 使用示例
  95. if __name__ == "__main__":
  96. # 自定义路径和文件名
  97. custom_logger = LogManager(
  98. log_path="/home/kevin/kevin/zhaoyuan/zhaoyuan/log/runtime.log"
  99. ).get_logger()
  100. custom_logger.info("这条信息只会写入文件,不会显示在终端") # 终端不再显示
  101. # 默认配置
  102. default_logger = LogManager().get_logger()
  103. default_logger.warning("警告信息也只会写入文件")
  104. # 安全关闭
  105. LogManager.shutdown()