1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
| import os import sys import logging from logging.handlers import BaseRotatingHandler from typing import Optional, Dict, Any, List import allure from datetime import datetime
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ROOT_LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
LEVEL_DIR_MAP = { logging.DEBUG: "debug_log", logging.INFO: "info_log", logging.WARNING: "warning_log", logging.ERROR: "error_log", logging.CRITICAL: "critical_log" }
def _ensure_level_dirs(): if not os.path.exists(ROOT_LOG_DIR): os.makedirs(ROOT_LOG_DIR, exist_ok=True) for level_dir in LEVEL_DIR_MAP.values(): level_dir_path = os.path.join(ROOT_LOG_DIR, level_dir) if not os.path.exists(level_dir_path): os.makedirs(level_dir_path, exist_ok=True)
class AllureLogHandler(logging.Handler): """同步日志到Allure报告"""
def emit(self, record): try: if record.levelno < logging.INFO: return log_msg = self.format(record) allure.attach( log_msg, name=f"[{record.levelname}] {record.filename}:{record.lineno}", attachment_type=allure.attachment_type.TEXT ) except Exception: pass
class LevelDirDailyFileHandler(BaseRotatingHandler): """ 按「级别文件夹+日期」存储日志 例如:logs/info_log/info_2026-02-27.log、logs/error_log/error_2026-02-27.log """
def __init__(self, target_level, encoding='utf-8', backupCount=7): self.target_level = target_level self.target_level_name = logging.getLevelName(target_level).lower() self.level_dir_name = LEVEL_DIR_MAP[target_level] self.level_dir_path = os.path.join(ROOT_LOG_DIR, self.level_dir_name)
self.backupCount = backupCount self.current_date = self._get_today() self.current_filename = os.path.join( self.level_dir_path, f"{self.target_level_name}_{self.current_date}.log" ) super().__init__(self.current_filename, 'a', encoding=encoding)
def _get_today(self): """获取当天日期(YYYY-MM-DD)""" return datetime.now().strftime("%Y-%m-%d")
def shouldRollover(self, record): """仅跨天时切换文件""" today = self._get_today() if today != self.current_date: return True return False
def emit(self, record): """严格过滤:仅写入指定级别的日志""" if record.levelno != self.target_level: return super().emit(record)
def doRollover(self): """跨天切换文件,清理当前级别文件夹下的过期日志""" if self.stream: self.stream.close() self.stream = None
self.current_date = self._get_today() self.current_filename = os.path.join( self.level_dir_path, f"{self.target_level_name}_{self.current_date}.log" )
self._clean_old_logs()
self.stream = self._open()
def _clean_old_logs(self): """清理当前级别文件夹下的过期日志""" try: log_files = [ f for f in os.listdir(self.level_dir_path) if f.startswith(f"{self.target_level_name}_") and f.endswith('.log') ] log_files.sort( key=lambda x: datetime.strptime( x.replace(f"{self.target_level_name}_", "").replace(".log", ""), "%Y-%m-%d" ) ) if len(log_files) > self.backupCount: for old_file in log_files[:-self.backupCount]: os.remove(os.path.join(self.level_dir_path, old_file)) except Exception as e: print(f"清理{self.target_level_name}级别过期日志失败: {e}", file=sys.stderr)
class Logger: _instance: Optional['Logger'] = None _logger: Optional[logging.Logger] = None
def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._init_logger() return cls._instance
def _init_logger(self): if Logger._logger is not None: return
_ensure_level_dirs()
Logger._logger = logging.getLogger("LeopardTestLogger") Logger._logger.setLevel(logging.DEBUG) Logger._logger.propagate = False
formatter = logging.Formatter( '[%(asctime)s] [%(process)d-%(threadName)s] [%(filename)s:%(lineno)d] - %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' )
debug_handler = LevelDirDailyFileHandler(logging.DEBUG, backupCount=7) debug_handler.setFormatter(formatter)
info_handler = LevelDirDailyFileHandler(logging.INFO, backupCount=7) info_handler.setFormatter(formatter)
warning_handler = LevelDirDailyFileHandler(logging.WARNING, backupCount=7) warning_handler.setFormatter(formatter)
error_handler = LevelDirDailyFileHandler(logging.ERROR, backupCount=7) error_handler.setFormatter(formatter)
critical_handler = LevelDirDailyFileHandler(logging.CRITICAL, backupCount=7) critical_handler.setFormatter(formatter)
class ColoredStreamHandler(logging.StreamHandler): COLORS = { logging.DEBUG: '\033[0;36m', logging.INFO: '\033[0;32m', logging.WARNING: '\033[0;33m', logging.ERROR: '\033[0;31m', logging.CRITICAL: '\033[0;35m' } RESET = '\033[0m'
def format(self, record): msg = super().format(record) color = self.COLORS.get(record.levelno, self.RESET) return f"{color}{msg}{self.RESET}"
console_handler = ColoredStreamHandler(stream=sys.stdout) console_handler.setLevel(logging.DEBUG) console_handler.setFormatter(formatter)
allure_handler = AllureLogHandler() allure_handler.setFormatter(formatter)
if not Logger._logger.handlers: Logger._logger.addHandler(debug_handler) Logger._logger.addHandler(info_handler) Logger._logger.addHandler(warning_handler) Logger._logger.addHandler(error_handler) Logger._logger.addHandler(critical_handler) Logger._logger.addHandler(console_handler) Logger._logger.addHandler(allure_handler)
def debug(self, message: str) -> None: """DEBUG → logs/debug_log/debug_2026-02-27.log""" try: self._logger.debug(message.strip()) except Exception as e: print(f"[日志错误] 记录DEBUG日志失败: {str(e)}", file=sys.stderr)
def info(self, message: str) -> None: """INFO → logs/info_log/info_2026-02-27.log""" try: self._logger.info(message.strip()) except Exception as e: print(f"[日志错误] 记录INFO日志失败: {str(e)}", file=sys.stderr)
def warning(self, message: str) -> None: """WARNING → logs/warning_log/warning_2026-02-27.log""" try: self._logger.warning(message.strip()) except Exception as e: print(f"[日志错误] 记录WARNING日志失败: {str(e)}", file=sys.stderr)
def error(self, message: str, exc_info: bool = False) -> None: """ERROR → logs/error_log/error_2026-02-27.log""" try: self._logger.error(message.strip(), exc_info=exc_info) except Exception as e: print(f"[日志错误] 记录ERROR日志失败: {str(e)}", file=sys.stderr)
def critical(self, message: str, exc_info: bool = True) -> None: """CRITICAL → logs/critical_log/critical_2026-02-27.log""" try: self._logger.critical(message.strip(), exc_info=exc_info) except Exception as e: print(f"[日志错误] 记录CRITICAL日志失败: {str(e)}", file=sys.stderr)
def api_logs( self, url: str, data: Dict[str, Any], codes: int, responses: Dict[str, Any], data_name: str, data_list: List[Any] ) -> None: """接口日志 → logs/info_log/info_2026-02-27.log""" log_msg = ( "\n" + "=" * 80 + f"\n【请求URL】:{url}" f"\n【请求参数】:{data}" f"\n【返回状态码】:{codes}" f"\n【返回结果】:{responses}" f"\n【数据库验证字段】:{data_name}" f"\n【数据库查询数据】:{data_list}" + "\n" + "=" * 80 + "\n" ) self.info(log_msg)
log = Logger()
|