1. 为什么需要封装logging模块
在Python接口自动化测试中,日志记录是排查问题的重要依据。原生logging模块虽然功能强大,但直接使用存在几个典型问题:
- 配置繁琐:每次都需要重复设置日志级别、格式和输出位置
- 格式不统一:不同模块可能使用不同的日志格式
- 文件管理混乱:日志文件可能分散在不同目录
- 多进程写入冲突:并发测试时可能出现日志丢失
我在金融行业做接口测试时,曾遇到一个典型案例:支付接口返回502错误,但由于测试脚本没有规范的日志记录,花了3天才定位到是网关超时问题。这促使我系统性地改进了日志管理方案。
2. 日志封装设计思路
2.1 基础封装方案
最基础的日志封装可以这样实现:
python复制import logging
from datetime import datetime
class LogUtil:
def __init__(self, name=__name__):
self.logger = logging.getLogger(name)
self.logger.setLevel(logging.DEBUG)
# 避免重复添加handler
if not self.logger.handlers:
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# 控制台输出
ch = logging.StreamHandler()
ch.setFormatter(formatter)
self.logger.addHandler(ch)
# 文件输出
fh = logging.FileHandler(
f'logs/api_test_{datetime.now().strftime("%Y%m%d")}.log'
)
fh.setFormatter(formatter)
self.logger.addHandler(fh)
这个方案解决了:
- 自动包含时间戳、模块名、日志级别
- 同时输出到控制台和文件
- 按天分割日志文件
2.2 进阶功能扩展
实际项目中还需要考虑:
- 日志文件轮转:使用RotatingFileHandler或TimedRotatingFileHandler
- 异常堆栈记录:自动捕获并格式化异常信息
- 日志分级存储:不同级别日志写入不同文件
- 多进程安全:使用QueueHandler解决并发写入问题
改进后的核心代码:
python复制from logging.handlers import RotatingFileHandler, QueueHandler
import queue
class AdvancedLogUtil(LogUtil):
def __init__(self, name=__name__, max_bytes=10*1024*1024, backup_count=5):
super().__init__(name)
self.log_queue = queue.Queue()
self.queue_handler = QueueHandler(self.log_queue)
# 替换原来的文件handler
for h in self.logger.handlers[:]:
if isinstance(h, logging.FileHandler):
self.logger.removeHandler(h)
# 添加轮转文件handler
rf_handler = RotatingFileHandler(
f'logs/api_test.log',
maxBytes=max_bytes,
backupCount=backup_count
)
rf_handler.setFormatter(self.formatter)
self.logger.addHandler(rf_handler)
# 添加队列handler
self.logger.addHandler(self.queue_handler)
3. 实战应用技巧
3.1 接口测试中的最佳实践
在unittest或pytest框架中,建议这样集成日志:
python复制import unittest
from utils.log_util import AdvancedLogUtil
class APITestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.logger = AdvancedLogUtil(__name__)
def test_transfer_api(self):
self.logger.info("开始执行转账接口测试")
try:
response = call_transfer_api()
self.assertEqual(response.status_code, 200)
self.logger.debug(f"接口响应: {response.text}")
except Exception as e:
self.logger.error(f"接口调用失败: {str(e)}", exc_info=True)
raise
关键技巧:
- 在setUpClass中初始化logger,避免每个test方法重复创建
- 使用不同的日志级别区分信息重要性
- 捕获异常时使用exc_info=True记录完整堆栈
- 敏感信息如密码需要脱敏处理
3.2 日志分析与问题定位
良好的日志格式可以快速定位问题。推荐包含以下字段:
| 字段 | 示例 | 作用 |
|---|---|---|
| timestamp | 2023-08-20 14:30:45 | 精确到毫秒更好 |
| trace_id | req-123456 | 请求唯一标识 |
| module | payment_api | 模块名称 |
| level | ERROR | 日志级别 |
| message | 账户余额不足 | 关键信息 |
| extra | 附加上下文 |
实现方式:
python复制class ContextFilter(logging.Filter):
def filter(self, record):
record.trace_id = getattr(record, 'trace_id', 'default')
return True
logger.addFilter(ContextFilter())
formatter = logging.Formatter(
'%(asctime)s [%(trace_id)s] %(module)s %(levelname)s - %(message)s'
)
4. 常见问题解决方案
4.1 日志文件不生成
可能原因:
- 目录权限不足
- 路径不存在
- 磁盘空间已满
解决方案:
python复制import os
log_dir = 'logs'
if not os.path.exists(log_dir):
try:
os.makedirs(log_dir, exist_ok=True)
except Exception as e:
print(f"创建日志目录失败: {e}")
raise
4.2 多进程日志丢失
典型现象:
- 并发测试时部分日志缺失
- 日志内容错乱
推荐方案:
- 使用QueueHandler+QueueListener模式
- 或者每个进程独立日志文件
实现代码:
python复制from logging.handlers import QueueListener
class SafeLogUtil(AdvancedLogUtil):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.listener = QueueListener(
self.log_queue,
*self.logger.handlers[1:] # 排除queue_handler
)
self.listener.start()
def __del__(self):
self.listener.stop()
4.3 日志性能优化
当日志量很大时,需要注意:
- 避免同步阻塞:使用异步handler
- 减少IO操作:批量写入
- 合理设置级别:生产环境适当提高级别
异步日志实现:
python复制import threading
class AsyncFileHandler(logging.Handler):
def __init__(self, filename):
super().__init__()
self.filename = filename
self.queue = queue.Queue()
self.thread = threading.Thread(target=self._write_loop)
self.thread.daemon = True
self.thread.start()
def _write_loop(self):
with open(self.filename, 'a') as f:
while True:
record = self.queue.get()
if record is None: # 终止信号
break
f.write(self.format(record) + '\n')
def emit(self, record):
self.queue.put(record)
def close(self):
self.queue.put(None)
self.thread.join()
super().close()
5. 企业级日志方案建议
对于大型项目,建议:
- 使用ELK等日志系统集中管理
- 添加日志采样机制避免数据爆炸
- 实现日志分级报警(如ERROR级别触发告警)
- 关键业务日志单独存储
与ELK集成的示例配置:
python复制from pythonjsonlogger import jsonlogger
class ELKLogUtil(AdvancedLogUtil):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 替换为JSON格式
formatter = jsonlogger.JsonFormatter(
'%(asctime)s %(name)s %(levelname)s %(message)s'
)
for handler in self.logger.handlers:
handler.setFormatter(formatter)
日志采样实现:
python复制import random
class SamplingFilter(logging.Filter):
def __init__(self, sample_rate=0.1):
self.sample_rate = sample_rate
def filter(self, record):
if record.levelno >= logging.ERROR:
return True
return random.random() < self.sample_rate
在接口自动化项目中,我通常会根据测试阶段调整日志配置:
- 开发调试:DEBUG级别,完整日志
- 日常运行:INFO级别,关键步骤日志
- 生产环境:WARNING级别,只记录异常
最后分享一个实用技巧:在Jenkins等CI工具中运行时,可以通过环境变量动态调整日志级别:
python复制import os
log_level = os.getenv('LOG_LEVEL', 'INFO').upper()
logger.setLevel(getattr(logging, log_level, logging.INFO))