import os import logging import datetime import sys import glob import time import traceback from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler from config.config import LOG_DIR, LOG_FILE, LOG_LEVEL, LOG_MAX_SIZE, LOG_BACKUP_COUNT, LOG_RETENTION_DAYS from .url_censor import censor_url class URLCensorFormatter(logging.Formatter): """自定义的日志格式化器,用于隐藏日志消息中的URL""" def format(self, record): # 先使用原始的format方法格式化日志 formatted_message = super().format(record) # 临时禁用URL隐藏,直接返回原始消息 return formatted_message # 然后对格式化后的消息进行URL审查(已禁用) # return censor_url(formatted_message) class Logger: def __init__(self, filename, stream): self.terminal = stream try: # 确保目录存在 log_dir = os.path.dirname(filename) if log_dir and not os.path.exists(log_dir): os.makedirs(log_dir, exist_ok=True) print(f"已创建日志目录: {log_dir}") # 以追加模式打开,避免覆盖现有内容 self.log = open(filename, "a", encoding="utf-8", errors="replace") self.log.write("\n\n--- New logging session started ---\n\n") except (IOError, OSError) as e: # 如果打开文件失败,记录错误并使用空的写入操作 print(f"Error opening log file {filename}: {e}") self.log = None def write(self, message): try: # 临时禁用URL隐藏 # censored_message = censor_url(message) censored_message = message # 直接使用原始消息 self.terminal.write(censored_message) if self.log: self.log.write(censored_message) self.flush() except Exception as e: # 发生错误时记录到控制台 self.terminal.write(f"Error writing to log: {e}\n") def flush(self): try: self.terminal.flush() if self.log: self.log.flush() except Exception: pass def close(self): try: if self.log: self.log.write("\n--- Logging session ended ---\n") self.log.close() self.log = None except Exception: pass # 增加异常钩子,确保未捕获的异常也会记录到日志文件中 def log_uncaught_exceptions(exc_type, exc_value, exc_traceback): """处理未捕获的异常,记录到日志中""" if issubclass(exc_type, KeyboardInterrupt): # 对于键盘中断,使用默认处理 sys.__excepthook__(exc_type, exc_value, exc_traceback) return # 获取主日志记录器 logger = logging.getLogger('main') # 格式化异常信息 lines = traceback.format_exception(exc_type, exc_value, exc_traceback) error_message = '未捕获的异常:\n' + ''.join(lines) # 记录到日志中 logger.critical(error_message) # 同时也显示在控制台 sys.__excepthook__(exc_type, exc_value, exc_traceback) # 设置全局异常处理器 sys.excepthook = log_uncaught_exceptions def cleanup_old_logs(retention_days=7): """清理超过指定天数的旧日志文件 Args: retention_days: 日志保留天数,默认7天 """ try: now = time.time() cutoff = now - (retention_days * 86400) # 86400秒 = 1天 # 获取所有日志文件 log_files = glob.glob(os.path.join(LOG_DIR, "log-*.txt")) for log_file in log_files: # 检查文件修改时间 if os.path.getmtime(log_file) < cutoff: try: os.remove(log_file) print(f"已删除过期日志: {log_file}") except Exception as e: print(f"删除日志文件失败 {log_file}: {e}") except Exception as e: print(f"清理旧日志文件时出错: {e}") def setup_logger(name): """设置并返回一个命名的logger 使用统一的日志文件,添加日志轮转功能,实现自动清理过期日志 Args: name: logger的名称 Returns: logging.Logger: 配置好的logger对象 """ # 创建logger logger = logging.getLogger(name) # 避免重复添加处理器 if logger.hasHandlers(): return logger # 根据配置设置日志级别 log_level = getattr(logging, LOG_LEVEL.upper(), logging.DEBUG) logger.setLevel(log_level) # 确保日志目录存在 os.makedirs(LOG_DIR, exist_ok=True) # 清理过期日志文件 cleanup_old_logs(LOG_RETENTION_DAYS) # 创建主日志文件的轮转处理器 try: # 确保主日志文件目录存在 log_file_dir = os.path.dirname(LOG_FILE) if log_file_dir and not os.path.exists(log_file_dir): os.makedirs(log_file_dir, exist_ok=True) print(f"已创建主日志目录: {log_file_dir}") # 使用RotatingFileHandler实现日志轮转 main_file_handler = RotatingFileHandler( LOG_FILE, maxBytes=LOG_MAX_SIZE, backupCount=LOG_BACKUP_COUNT, encoding="utf-8" ) main_file_handler.setLevel(log_level) except (IOError, OSError) as e: print(f"无法创建主日志文件处理器: {e}") main_file_handler = None # 创建控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # 控制台只显示INFO以上级别 # 创建更详细的格式器,包括模块名、文件名和行号 formatter = URLCensorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') # 设置处理器的格式化器 console_handler.setFormatter(formatter) if main_file_handler: main_file_handler.setFormatter(formatter) # 添加处理器到logger logger.addHandler(console_handler) if main_file_handler: logger.addHandler(main_file_handler) # 确保异常可以被正确记录 logger.propagate = True return logger