|
@@ -0,0 +1,201 @@
|
|
|
+import logging
|
|
|
+import logging.handlers
|
|
|
+import os
|
|
|
+from typing import Dict, Optional
|
|
|
+from pathlib import Path
|
|
|
+import yaml
|
|
|
+import contextvars
|
|
|
+
|
|
|
+# 上下文变量,存储可选的上下文信息
|
|
|
+log_context = contextvars.ContextVar('log_context', default={})
|
|
|
+
|
|
|
+class ContextFilter(logging.Filter):
|
|
|
+ """添加上下文信息到日志记录"""
|
|
|
+ def filter(self, record):
|
|
|
+ ctx = log_context.get()
|
|
|
+ # 设置默认值,避免格式化错误
|
|
|
+ record.session_id = ctx.get('session_id', 'N/A')
|
|
|
+ record.user_id = ctx.get('user_id', 'anonymous')
|
|
|
+ record.request_id = ctx.get('request_id', 'N/A')
|
|
|
+ return True
|
|
|
+
|
|
|
+class LogManager:
|
|
|
+ """统一日志管理器 - 类似Log4j的功能"""
|
|
|
+
|
|
|
+ _instance = None
|
|
|
+ _loggers: Dict[str, logging.Logger] = {}
|
|
|
+ _initialized = False
|
|
|
+ _fallback_to_console = False # 标记是否降级到控制台
|
|
|
+
|
|
|
+ def __new__(cls):
|
|
|
+ if cls._instance is None:
|
|
|
+ cls._instance = super().__new__(cls)
|
|
|
+ return cls._instance
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ if not self._initialized:
|
|
|
+ self.config = None
|
|
|
+ self.base_log_dir = Path("logs")
|
|
|
+ self._setup_base_directory()
|
|
|
+ LogManager._initialized = True
|
|
|
+
|
|
|
+ def initialize(self, config_path: str = "config/logging_config.yaml"):
|
|
|
+ """初始化日志系统"""
|
|
|
+ self.config = self._load_config(config_path)
|
|
|
+ self._setup_base_directory()
|
|
|
+ self._configure_root_logger()
|
|
|
+
|
|
|
+ def get_logger(self, name: str, module: str = "default") -> logging.Logger:
|
|
|
+ """获取指定模块的logger"""
|
|
|
+ logger_key = f"{module}.{name}"
|
|
|
+
|
|
|
+ if logger_key not in self._loggers:
|
|
|
+ logger = logging.getLogger(logger_key)
|
|
|
+ self._configure_logger(logger, module)
|
|
|
+ self._loggers[logger_key] = logger
|
|
|
+
|
|
|
+ return self._loggers[logger_key]
|
|
|
+
|
|
|
+ def set_context(self, **kwargs):
|
|
|
+ """设置日志上下文(可选)"""
|
|
|
+ ctx = log_context.get()
|
|
|
+ ctx.update(kwargs)
|
|
|
+ log_context.set(ctx)
|
|
|
+
|
|
|
+ def clear_context(self):
|
|
|
+ """清除日志上下文"""
|
|
|
+ log_context.set({})
|
|
|
+
|
|
|
+ def _load_config(self, config_path: str) -> dict:
|
|
|
+ """加载配置文件"""
|
|
|
+ try:
|
|
|
+ with open(config_path, 'r', encoding='utf-8') as f:
|
|
|
+ return yaml.safe_load(f)
|
|
|
+ except FileNotFoundError:
|
|
|
+ import sys
|
|
|
+ sys.stderr.write(f"[WARNING] 配置文件 {config_path} 未找到,使用默认配置\n")
|
|
|
+ return self._get_default_config()
|
|
|
+ except Exception as e:
|
|
|
+ import sys
|
|
|
+ sys.stderr.write(f"[ERROR] 加载配置文件失败: {e},使用默认配置\n")
|
|
|
+ return self._get_default_config()
|
|
|
+
|
|
|
+ def _get_default_config(self) -> dict:
|
|
|
+ """获取默认配置"""
|
|
|
+ return {
|
|
|
+ 'global': {'base_level': 'INFO'},
|
|
|
+ 'default': {
|
|
|
+ 'level': 'INFO',
|
|
|
+ 'console': {
|
|
|
+ 'enabled': True,
|
|
|
+ 'level': 'INFO',
|
|
|
+ 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
|
|
+ },
|
|
|
+ 'file': {
|
|
|
+ 'enabled': True,
|
|
|
+ 'level': 'DEBUG',
|
|
|
+ 'filename': 'app.log',
|
|
|
+ 'format': '%(asctime)s [%(levelname)s] [%(name)s] [user:%(user_id)s] [session:%(session_id)s] %(filename)s:%(lineno)d - %(message)s',
|
|
|
+ 'rotation': {
|
|
|
+ 'enabled': True,
|
|
|
+ 'max_size': '50MB',
|
|
|
+ 'backup_count': 10
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 'modules': {}
|
|
|
+ }
|
|
|
+
|
|
|
+ def _setup_base_directory(self):
|
|
|
+ """创建日志目录(带降级策略)"""
|
|
|
+ try:
|
|
|
+ os.makedirs(self.base_log_dir, exist_ok=True)
|
|
|
+ self._fallback_to_console = False
|
|
|
+ except Exception as e:
|
|
|
+ import sys
|
|
|
+ sys.stderr.write(f"[WARNING] 无法创建日志目录 {self.base_log_dir},将只使用控制台输出: {e}\n")
|
|
|
+ self._fallback_to_console = True
|
|
|
+
|
|
|
+ def _configure_root_logger(self):
|
|
|
+ """配置根日志器"""
|
|
|
+ root_logger = logging.getLogger()
|
|
|
+ root_logger.setLevel(getattr(logging, self.config['global']['base_level'].upper()))
|
|
|
+
|
|
|
+ def _configure_logger(self, logger: logging.Logger, module: str):
|
|
|
+ """配置具体的logger"""
|
|
|
+ module_config = self.config.get('modules', {}).get(module, self.config['default'])
|
|
|
+
|
|
|
+ # 设置日志级别
|
|
|
+ level = getattr(logging, module_config['level'].upper())
|
|
|
+ logger.setLevel(level)
|
|
|
+
|
|
|
+ # 清除已有处理器
|
|
|
+ logger.handlers.clear()
|
|
|
+ logger.propagate = False
|
|
|
+
|
|
|
+ # 添加控制台处理器
|
|
|
+ if module_config.get('console', {}).get('enabled', True):
|
|
|
+ console_handler = self._create_console_handler(module_config['console'])
|
|
|
+ console_handler.addFilter(ContextFilter())
|
|
|
+ logger.addHandler(console_handler)
|
|
|
+
|
|
|
+ # 添加文件处理器(如果没有降级到控制台)
|
|
|
+ if not self._fallback_to_console and module_config.get('file', {}).get('enabled', True):
|
|
|
+ try:
|
|
|
+ file_handler = self._create_file_handler(module_config['file'], module)
|
|
|
+ file_handler.addFilter(ContextFilter())
|
|
|
+ logger.addHandler(file_handler)
|
|
|
+ except Exception as e:
|
|
|
+ import sys
|
|
|
+ sys.stderr.write(f"[WARNING] 无法创建文件处理器: {e}\n")
|
|
|
+ # 如果文件处理器创建失败,标记降级
|
|
|
+ self._fallback_to_console = True
|
|
|
+
|
|
|
+ def _create_console_handler(self, console_config: dict) -> logging.StreamHandler:
|
|
|
+ """创建控制台处理器"""
|
|
|
+ handler = logging.StreamHandler()
|
|
|
+ handler.setLevel(getattr(logging, console_config.get('level', 'INFO').upper()))
|
|
|
+
|
|
|
+ formatter = logging.Formatter(
|
|
|
+ console_config.get('format', '%(asctime)s [%(levelname)s] %(name)s: %(message)s'),
|
|
|
+ datefmt='%Y-%m-%d %H:%M:%S'
|
|
|
+ )
|
|
|
+ handler.setFormatter(formatter)
|
|
|
+ return handler
|
|
|
+
|
|
|
+ def _create_file_handler(self, file_config: dict, module: str) -> logging.Handler:
|
|
|
+ """创建文件处理器(支持自动轮转)"""
|
|
|
+ log_file = self.base_log_dir / file_config.get('filename', f'{module}.log')
|
|
|
+
|
|
|
+ # 使用RotatingFileHandler实现自动轮转和清理
|
|
|
+ rotation_config = file_config.get('rotation', {})
|
|
|
+ if rotation_config.get('enabled', False):
|
|
|
+ handler = logging.handlers.RotatingFileHandler(
|
|
|
+ log_file,
|
|
|
+ maxBytes=self._parse_size(rotation_config.get('max_size', '50MB')),
|
|
|
+ backupCount=rotation_config.get('backup_count', 10),
|
|
|
+ encoding='utf-8'
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
|
+
|
|
|
+ handler.setLevel(getattr(logging, file_config.get('level', 'DEBUG').upper()))
|
|
|
+
|
|
|
+ formatter = logging.Formatter(
|
|
|
+ file_config.get('format', '%(asctime)s [%(levelname)s] [%(name)s] %(filename)s:%(lineno)d - %(message)s'),
|
|
|
+ datefmt='%Y-%m-%d %H:%M:%S'
|
|
|
+ )
|
|
|
+ handler.setFormatter(formatter)
|
|
|
+ return handler
|
|
|
+
|
|
|
+ def _parse_size(self, size_str: str) -> int:
|
|
|
+ """解析大小字符串,如 '50MB' -> 字节数"""
|
|
|
+ size_str = size_str.upper()
|
|
|
+ if size_str.endswith('KB'):
|
|
|
+ return int(size_str[:-2]) * 1024
|
|
|
+ elif size_str.endswith('MB'):
|
|
|
+ return int(size_str[:-2]) * 1024 * 1024
|
|
|
+ elif size_str.endswith('GB'):
|
|
|
+ return int(size_str[:-2]) * 1024 * 1024 * 1024
|
|
|
+ else:
|
|
|
+ return int(size_str)
|