""" 统一日志管理模块 提供结构化日志记录功能,支持控制台和文件输出 """ import os import sys import logging import logging.handlers from pathlib import Path from typing import Optional, Dict, Any from datetime import datetime from .config_manager import get_config class ColoredFormatter(logging.Formatter): """带颜色的日志格式化器""" # ANSI颜色代码 COLORS = { 'DEBUG': '\033[36m', # 青色 'INFO': '\033[32m', # 绿色 'WARNING': '\033[33m', # 黄色 'ERROR': '\033[31m', # 红色 'CRITICAL': '\033[35m', # 紫色 } RESET = '\033[0m' # 重置颜色 def format(self, record): # 检查是否已经包含ANSI颜色代码 if '\033[' in record.levelname: # 如果已经着色,直接返回原始格式 return super().format(record) # 添加颜色 if record.levelname in self.COLORS: # 为levelname添加颜色 colored_levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.RESET}" # 使用原始levelname来确定消息的颜色 record.msg = f"{self.COLORS[record.levelname]}{record.msg}{self.RESET}" record.levelname = colored_levelname return super().format(record) class Logger: """统一日志管理器""" _instance = None _initialized = 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._loggers = {} self._config = None self._file_handler = None # 全局文件处理器,避免重复创建 self._initialized = True def init_logger(self, name: str = "gx_gp_monitor", config=None) -> logging.Logger: """ 初始化日志器 Args: name: 日志器名称 config: 配置对象,如果为None则从全局配置加载 Returns: logging.Logger: 配置好的日志器实例 """ if name in self._loggers: return self._loggers[name] # 获取配置 if config is None: try: self._config = get_config() except RuntimeError: # 配置未加载,使用默认配置 self._config = self._get_default_config() else: self._config = config # 创建日志器 logger = logging.getLogger(name) logger.setLevel(getattr(logging, self._config.log_level.value)) # 防止消息传播到父logger,避免重复记录 logger.propagate = False # 避免重复添加处理器 if logger.handlers: return logger # 创建格式化器 formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # 控制台处理器 console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(getattr(logging, self._config.log_level.value)) # 使用彩色格式化器(如果支持) if sys.platform != 'win32' and 'TERM' in os.environ: colored_formatter = ColoredFormatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) console_handler.setFormatter(colored_formatter) else: console_handler.setFormatter(formatter) logger.addHandler(console_handler) # 文件处理器(如果配置了日志文件)- 使用全局文件处理器避免重复 if self._config.log_file and self._file_handler is None: log_dir = Path(self._config.log_file).parent log_dir.mkdir(parents=True, exist_ok=True) self._file_handler = logging.handlers.RotatingFileHandler( self._config.log_file, maxBytes=self._config.log_max_size, backupCount=self._config.log_backup_count, encoding='utf-8' ) self._file_handler.setLevel(getattr(logging, self._config.log_level.value)) self._file_handler.setFormatter(formatter) # 为所有logger添加全局文件处理器 if self._file_handler is not None: logger.addHandler(self._file_handler) self._loggers[name] = logger return logger def _get_default_config(self): """获取默认配置""" from .config_manager import LogLevel class DefaultConfig: def __init__(self): self.log_level = LogLevel.INFO self.log_file = "logs/gx_gp_monitor.log" self.log_max_size = 10485760 # 10MB self.log_backup_count = 5 return DefaultConfig() def get_logger(self, name: str = "gx_gp_monitor") -> logging.Logger: """ 获取日志器 Args: name: 日志器名称 Returns: logging.Logger: 日志器实例 """ if name not in self._loggers: return self.init_logger(name) return self._loggers[name] def log_crawl_start(self, source_name: str, logger: Optional[logging.Logger] = None): """记录爬取开始""" if logger is None: logger = self.get_logger() logger.info(f"开始爬取 {source_name}") def log_crawl_success(self, source_name: str, count: int, duration: float, logger: Optional[logging.Logger] = None): """记录爬取成功""" if logger is None: logger = self.get_logger() logger.info(f"{source_name} 爬取完成,共获取 {count} 条公告,耗时 {duration:.2f}秒") def log_crawl_error(self, source_name: str, error: str, logger: Optional[logging.Logger] = None): """记录爬取错误""" if logger is None: logger = self.get_logger() logger.error(f"{source_name} 爬取失败: {error}") def log_announcement_filtered(self, reason: str, count: int, logger: Optional[logging.Logger] = None): """记录公告筛选信息""" if logger is None: logger = self.get_logger() logger.info(f"公告筛选 - {reason}: {count} 条") def log_database_operation(self, operation: str, table: str, count: int = 0, logger: Optional[logging.Logger] = None): """记录数据库操作""" if logger is None: logger = self.get_logger() if count > 0: logger.info(f"数据库操作 - {operation} {table}: {count} 条记录") else: logger.info(f"数据库操作 - {operation} {table}") def log_notification_sent(self, channel: str, recipient_count: int, logger: Optional[logging.Logger] = None): """记录通知发送""" if logger is None: logger = self.get_logger() logger.info(f"通知发送 - {channel}: 向 {recipient_count} 个接收者发送") def log_system_metrics(self, metrics: Dict[str, Any], logger: Optional[logging.Logger] = None): """记录系统指标""" if logger is None: logger = self.get_logger() metrics_str = ", ".join([f"{k}={v}" for k, v in metrics.items()]) logger.info(f"系统指标: {metrics_str}") def log_performance_warning(self, operation: str, duration: float, threshold: float, logger: Optional[logging.Logger] = None): """记录性能警告""" if logger is None: logger = self.get_logger() logger.warning(f"性能警告 - {operation} 耗时 {duration:.2f}秒,超过阈值 {threshold:.2f}秒") # 全局日志管理器实例 _logger_manager = Logger() def get_logger(name: str = "gx_gp_monitor") -> logging.Logger: """ 获取日志器 Args: name: 日志器名称 Returns: logging.Logger: 日志器实例 """ return _logger_manager.get_logger(name) def init_logger(name: str = "gx_gp_monitor", config=None) -> logging.Logger: """ 初始化并获取日志器 Args: name: 日志器名称 config: 配置对象 Returns: logging.Logger: 日志器实例 """ return _logger_manager.init_logger(name, config) def log_function_call(func_name: str, args: Optional[Dict[str, Any]] = None, logger: Optional[logging.Logger] = None): """ 装饰器:记录函数调用 Args: func_name: 函数名称 args: 函数参数 logger: 日志器实例 """ def decorator(func): def wrapper(*args, **kwargs): nonlocal logger if logger is None: logger = get_logger() start_time = datetime.now() logger.debug(f"调用函数: {func_name}") try: result = func(*args, **kwargs) duration = (datetime.now() - start_time).total_seconds() logger.debug(f"函数 {func_name} 执行完成,耗时 {duration:.3f}秒") return result except Exception as e: duration = (datetime.now() - start_time).total_seconds() logger.error(f"函数 {func_name} 执行失败,耗时 {duration:.3f}秒: {str(e)}") raise return wrapper return decorator # 便捷函数 def log_info(message: str, logger: Optional[logging.Logger] = None): """记录信息日志""" if logger is None: logger = get_logger() logger.info(message) def log_warning(message: str, logger: Optional[logging.Logger] = None): """记录警告日志""" if logger is None: logger = get_logger() logger.warning(message) def log_error(message: str, logger: Optional[logging.Logger] = None): """记录错误日志""" if logger is None: logger = get_logger() logger.error(message) def log_debug(message: str, logger: Optional[logging.Logger] = None): """记录调试日志""" if logger is None: logger = get_logger() logger.debug(message) # 便捷的爬取日志记录函数 def log_crawl_start(source_name: str): """记录爬取开始""" _logger_manager.log_crawl_start(source_name) def log_crawl_success(source_name: str, count: int, duration: float): """记录爬取成功""" _logger_manager.log_crawl_success(source_name, count, duration) def log_crawl_error(source_name: str, error: str): """记录爬取错误""" _logger_manager.log_crawl_error(source_name, error) def log_announcement_filtered(reason: str, count: int): """记录公告筛选信息""" _logger_manager.log_announcement_filtered(reason, count) def log_database_operation(operation: str, table: str, count: int = 0): """记录数据库操作""" _logger_manager.log_database_operation(operation, table, count) def log_notification_sent(channel: str, recipient_count: int): """记录通知发送""" _logger_manager.log_notification_sent(channel, recipient_count) def log_system_metrics(metrics: Dict[str, Any]): """记录系统指标""" _logger_manager.log_system_metrics(metrics) def log_performance_warning(operation: str, duration: float, threshold: float): """记录性能警告""" _logger_manager.log_performance_warning(operation, duration, threshold)