Python 之 日志处理及封装

日志

软件开发中通过日志记录程序的运行情况是一个开发的好习惯,对于错误排查和系统运维都有很大帮助。

Python 标准库自带了强大的 logging 日志模块,在各种 python 模块中得到广泛应用。

一、简单使用

1. 入门小案例

import logging

# 默认的warning级别,只输出warning以上的
# 使用basicConfig()来指定日志级别和相关信息
logging.basicConfig(level=logging.DEBUG,  # 设置级别,根据等级显示
                    format='%(asctime)s-[%(filename)s-->line:%(lineno)d]-%(levelname)s:%(message)s',  # 设置输出格式
                    datefmt="%Y-%m-%d %H:%M:%S"  # 时间输出的格式

                    )
logging.debug("This is  DEBUG !!")
logging.info("This is  INFO !!")
logging.warning("This is  WARNING !!")
logging.error("This is  ERROR !!")
logging.critical("This is  CRITICAL !!")
 
 

输出:

C:\Users\12446\AppData\Local\Programs\Python\Python39\python.exe D:/Lemon/py45/day18/common/log_handler.py
2021-11-13 12:35:45-[log_handler.py-->line:16]-DEBUG:This is  DEBUG !!
2021-11-13 12:35:45-[log_handler.py-->line:17]-INFO:This is  INFO !!
2021-11-13 12:35:45-[log_handler.py-->line:18]-WARNING:This is  WARNING !!
2021-11-13 12:35:45-[log_handler.py-->line:19]-ERROR:This is  ERROR !!
2021-11-13 12:35:45-[log_handler.py-->line:20]-CRITICAL:This is  CRITICAL !!

Process finished with exit code 0
 
 

2. 日志级别

根据不同情况设置了五种日志等级,不同情况输出不同等级的日志。

日志等级(level)

描述

DEBUG

调试信息,通常在诊断问题的时候用得着

INFO

普通信息,确认程序按照预期运行

WARNING

警告信息,表示发生意想不到的事情,或者指示接下来可能会出现一些问题,但是程序还是继续运行

ERROR

错误信息,程序运行中出现了一些问题,程序某些功能不能执行

CRITICAL

危险信息,一个严重的错误,导致程序无法继续运行

日志器设置的级别会过滤掉低于这个级别的日志

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/13 12:27
# @Author  : shisuiyi
# @File    : log_handler.py
# @Software: win10 Tensorflow1.13.1 python3.9
import logging

# 默认的warning级别,只输出warning以上的
# 使用basicConfig()来指定日志级别和相关信息
logging.basicConfig(level=logging.ERROR,  # 设置级别,根据等级显示
                    format='%(asctime)s-[%(filename)s-->line:%(lineno)d]-%(levelname)s:%(message)s',  # 设置输出格式
                    datefmt="%Y-%m-%d %H:%M:%S"  # 时间输出的格式
                    )
logging.debug("This is a debug log")
logging.info("This is  INFO log")
logging.warning("This is  WARNING log")
logging.error("This is  ERROR log")
logging.critical("This is  CRITICAL log")
 
 

输出

2021-11-13 12:39:13-[log_handler.py-->line:19]-ERROR:This is  ERROR log
2021-11-13 12:39:13-[log_handler.py-->line:20]-CRITICAL:This is  CRITICAL log
 
 

3. 配置basicConfig方法支持一下关键字参数进行配置。

参数

描述

filename

使用指定的文件名而不是 StreamHandler 创建 FileHandler。

filemode

如果指定了 filename,则用此 模式 打开该文件。 默认模式为 'a'。

format

处理器使用的指定格式字符串。

datefmt

使用指定的日期/时间格式,与 time.strftime() 所接受的格式相同。

style

如果指定了 format,将为格式字符串使用此风格。 '%', '{' 或 '$' 分别对应于 printf 风格,str.format() 或 string.Template。 默认为 '%'。

level

设置根记录器级别去指定 level.

stream

使用指定的流初始化 StreamHandler。 请注意此参数与 filename 是不兼容的 - 如果两者同时存在,则会引发 ValueError。

handlers

如果指定,这应为一个包含要加入根日志记录器的已创建处理程序的可迭代对象。 任何尚未设置格式描述符的处理程序将被设置为在此函数中创建的默认格式描述符。 请注意此参数与 filename 或 stream 不兼容 —— 如果两者同时存在,则会引发 ValueError。

force

如果将此关键字参数指定为 true,则在执行其他参数指定的配置之前,将移除并关闭附加到根记录器的所有现有处理器。

4. 格式化规则

日志的输出格式可以通过下面格式自由组合输出

规则

描述

%(asctime)s

日志事件发生的时间

%(levelname)s

该日志记录的日志级别

%(message)s

日志记录的文本内容

%(name)s

所使用的日志器名称,默认是'root'

%(pathname)s

调用日志记录函数的文件的全路径

%(filename)s

调用日志记录函数的文件

%(module)s

模块 (filename 的名称部分)。

%(funcName)s

调用日志记录函数的函数名

%(lineno)d

调用日志记录函数的代码所在的行号

常用格式:%(asctime)s-[%(filename)s-->line:%(lineno)d]-%(levelname)s:%(message)s

5.日志写到文件

只需要配置 filename 参数即可

import logging
# 设置日志
logging.basicConfig(
    level=logging.WARNING,  # 设置日志等级
    format='%(asctime)s-[%(filename)s-->line:%(lineno)d]-%(levelname)s:%(message)s', # 设置输出格式
    datefmt='%Y/%m/%d %H:%M:%S',
    filename='my.log',
    filemode='a'
)
logging.debug('This is a debug log')
logging.info('This is a info log')
logging.warning('This is a warning log')
logging.error('This is a error log')
logging.critical('This is a critical log')
 
 

输出

# my.log 文件
2021/11/13 13:13:58-[log_handler.py-->line:18]-WARNING:This is a warning log
2021/11/13 13:13:58-[log_handler.py-->line:19]-ERROR:This is a error log
2021/11/13 13:13:58-[log_handler.py-->line:20]-CRITICAL:This is a critical log
 
 

二、高级用法

简单的代码通过 logging 直接使用即可,如果要深入使用需要按照面向对象的方式使用 logging。

1. 日志组件

logging 模块包含一下几个组件。

组件

说明

Loggers(日志记录器)

提供程序直接使用的接口

Handlers(日志处理器)

将记录的日志发送到指定的位置

Filters(日志过滤器)

用于过滤特定的日志记录

Formatters(日志格式器)

用于控制日志信息的输出格式

2.步骤

2.1 创建日志记录器

import logging

第一步创建一个 logger,用来产生日志

import logging
# 1. 创建日志器
logger = logging.getLogger('py45')
logger.setLevel(logging.DEBUG)  # 给日志器设置等级 通过 getLogger 这个方法可以创建一个日志记录器,注意要给名字否则返回根日志记录器。
 
 

通过 setLevel 设置日志记录器的等级。

2.2 创建日志处理器

创建一个文本处理器用来将日志写入到文件

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/13 12:27
# @Author  : shisuiyi
# @File    : log_handler.py
# @Software: win10 Tensorflow1.13.1 python3.9
import logging

# 1. 创建日志记录器
logger = logging.getLogger('py45')
logger.setLevel(logging.DEBUG)  # 给日志器设置等级

# 2.1 创建日志处理器
file_handler = logging.FileHandler(
    filename='py34.log', encoding='utf-8')
file_handler.setLevel('WARNING')  # 设置处理器的日志等级


# 2.2创建一个控制台处理器用来将日志输出到控制台
console_handler = logging.StreamHandler()
console_handler.setLevel('INFO')  # 设置控制台处理器的日志等级日志处理器就是将日志发送到指定的位置。
 
 
  • FileHandler 将日志发送到文件

  • StreaHandler将它可将日志记录输出发送到数据流例如 sys.stdout, sys.stderr 或任何文件类对象默认 sys.stdout 即控制台。

  • RotatingFileHandler支持根据日志文件大小进行轮转

  • TimedRotatingFileHandler 支持根据时间进行轮转日志文件

更多详情见官方文档

2.3 创建格式化器

formatter = logging.Formatter(fmt='%(asctime)s-[%(filename)s-->line:%(lineno)d]-%(levelname)s:%(message)s')
 
 

格式化器需要设置到处理器上

console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
 
 

2.4 创建过滤器

过滤器用来过滤指定日志。具体使用略,一般用不到。

详情见官方文档

2.5 将处理器添加到记录器上

logger.addHandler(console_handler)
logger.addHandler(file_handler)
 

2.6 记录日志

logger.info('aaaa')
logger.warning('你再不输出试试123')
 
 

三、日志模块封装

1. 功能分析

  1. 能够自定义日志器名

  2. 能够自定义日志文件名和路径

  3. 能够自定义日志文件编码方式

  4. 能够自定义日志格式

  5. 使用时间轮转处理器,并能够配置

2.封装成函数

在 common 目录下创建模块 log_handler.py 在其中创建如下函数。

# -*- coding: utf-8 -*-
# @Time : 2019/11/18 10:17
# @Author : kira
# @Email : 262667641@qq.com
# @File : logger.py.py
# @Project : risk_api_project


import logging
import time
from logging import handlers

from common.base_datas import BaseDates


# filename = BaseDates.log_path


class MyLog:
    level_relations = {
        "debug": logging.DEBUG,
        "info": logging.INFO,
        "warning": logging.WARNING,
        "error": logging.ERROR,
        "critic": logging.CRITICAL
    }  # 日志级别关系映射

    def my_log(self, msg, level="error", when="D", back_count=10):
        file_name = BaseDates.log_path

        my_logger = logging.getLogger()  # 定义日志收集器 my_logger
        my_logger.setLevel(self.level_relations.get(level))  # 设置日志级别

        format_str = logging.Formatter(
            "%(asctime)s-%(levelname)s-%(filename)s-[ line:%(lineno)d ] - 日志信息:%(message)s")  # 设置日志格式
        # 创建输出渠道
        sh = logging.StreamHandler()  # 往屏幕输出
        sh.setFormatter(format_str)  # 设置屏幕上显示的格式
        current = time.strftime("%Y-%m-%d", time.localtime())  # 设置当前日期
        if level == "error":
            th = handlers.TimedRotatingFileHandler(filename=f'{file_name}/{current}_{level}.log', when=when,
                                                   backupCount=back_count, encoding="utf-8")
        else:
            th = handlers.TimedRotatingFileHandler(filename=file_name + "/{}_info.log".format(current), when=when,
                                                   backupCount=back_count,
                                                   encoding="utf-8")  # 往文件里写日志
        """
        实例化 TimeRotatingFileHandler 
        interval 是时间间隔, backupCount 是备份文件的个数,如果超过这个个数,就会自动删除,when 是间隔的时间单位,单位有以下几种
        S 秒
        M 分
        H 小时
        D 天
        每星期(interval == 0 时代表星期一
        midnight 每天凌晨
        """
        th.setFormatter(format_str)  # 设置文件里写入的格式
        my_logger.addHandler(sh)  # 将对象加入logger里
        my_logger.addHandler(th)
        if level == "debug":
            my_logger.debug(msg)
        elif level == "error":
            my_logger.error(msg)
        elif level == "info":
            my_logger.info(msg)
        elif level == "warning":
            my_logger.warning(msg)
        else:
            my_logger.critical(msg)

        my_logger.removeHandler(sh)
        my_logger.removeHandler(th)

    def decorator_log(self, msg=None):
        def warp(fun):
            def inner(*args, **kwargs):
                try:
                    return fun(*args, **kwargs)
                except Exception as e:
                    self.my_log(f"{msg}: {e}", "error")

            return inner

        return warp


if __name__ == '__main__':
    # for i in range(2):
    #     MyLog().my_log("hhhh{}".format(i), "info")
    #     time.sleep(0.04)
    @MyLog().decorator_log
    def add():
        raise
 

四、应用到项目中

1. 导入

日志器生成函数的导入不能像 Excel 数据读取函数一样,每个用例模块里都导入一遍。因为它返回一个日志器对象,当多次调用日志器生成函数,且日志器名称相同时,会给同一个日志器添加多个日志处理器,从而出现重复记录日志器的问题。

为了解决上面的问题,在 common 文件夹下创建一个名为 __init__.py 的文件,在 common 模块被导入时会自动执行这个文件里的代码,且只会执行一次。

在 __init__.py 文件编写如下代码:

from .log_handler import get_logger
logger = get_logger(name='py48', filename='D:\Lemon\py45\day18\log\my.log', debug=True, when='S', interval=10)
 
 

那么在项目中的其他模块中就可以通过如下代码导入

from common import logger从而可以保证在项目执行过程中,get_logger 方法只会执行一遍。

2. 记录日志

日志的作用是记录程序的运行状态和当程序出现问题时能提供定位分析错误的依据。

什么时候需要记录日志,记录什么日志,根据每个人对程序的理解,以及经验。

我们的项目中,在用例执行的过程是核心,所以我们的日志也是围绕着用例的执行。

使用日志记录每个用例的测试数据,和测试结果,代码如下:

@list_data(cases)
def test_login(self, case):
    # 1. 准备测试数据
    # print(case)
    # print('{}开始测试'.format(case['title']))
    logger.info('测试用例【{}】开始测试'.format(case['title']))
 
 

3.测试数据

传入进来的 case 参数

logger.info('测试用例【{}】的测试数据是:{}'.format(case['title'], case))
 
 

4. 测试步骤

 # 2. 测试步骤
        res = login(**eval(case['request']))
        logger.info('测试用例【{}】的测试结果是:{}'.format(case['title'], res))
 
 

5. 断言

# 3. 断言
# self.assertEqual(res, eval(case['expect']))
try:
    self.assertEqual(res, eval(case['expect']))
except AssertionError as e:
    logger.error('测试用例【{}】断言失败'.format(case['title']))
    raise e
else:
    logger.info('测试用例【{}】断言成功'.format(case['title']))
finally:
    logger.info('测试用例【{}】测试结束'.format(case['title']))
 
 

整体代码如下

import sys
import time
import unittest
import warnings

from ddt import ddt, data

from common.base_datas import BaseDates

sys.path.append("../../")
sys.path.append("../../common")
from test_script.auto_script.login import login
from common.files_tools.get_excel_init import get_init
from common.extractor.dependent_parameter import DependentParameter
from common.extractor.data_extractor import DataExtractor
from common.encryption.encryption_main import do_encrypt
from common.do_sql.do_mysql import DoMysql
from common.tools.req import req
from common.tools.logger import MyLog
from common.comparator import loaders
from common.dependence import Dependence
from common.comparator.validator import Validator

warnings.simplefilter('ignore', ResourceWarning)

test_file = BaseDates.test_api  # 获取 excel 文件路径
excel_handle, init_data, test_case = get_init(test_file)
databases = init_data.get('databases')  # 获取数据库配置信息
mysql = DoMysql(databases)  # 初始化 mysql 链接
dep = Dependence
dep.set_dep(eval(init_data.get("initialize_data")))  # 初始化依赖表
dep_par = DependentParameter()  # 参数提取类实例化
logger = MyLog()


@ddt
class TestProjectApi(unittest.TestCase):
    maxDiff = None

    @classmethod
    def setUpClass(cls) -> None:
        loaders.set_bif_fun()  # 加载内置方法
        # 获取初始化基础数据
        cls.host = init_data.get('host')
        cls.path = init_data.get("path")
        username = dep.get_dep("{{account}}")
        password = dep.get_dep("{{passwd}}")
        cls.headers = login(cls.host + cls.path, username, password)
        dep.update_dep("headers", cls.headers)
        # 加载内置方法
        logger.my_log(f"内置方法:{dep.get_dep()}", "info")

    def setUp(self) -> None:
        logger.my_log(f"获取当前依赖参数表:{dep.get_dep()}")
        logger.my_log("-----------------------------------start_test_api-----------------------------------", "info")

    @data(*test_case)  # {"":""}
    def test_api(self, item):  # item = {測試用例}
        # f"""用例描述:{item.get("name")}_{item.get("desc")}"""
        sheet = item.get("sheet")
        item_id = item.get("Id")
        name = item.get("name")
        description = item.get("description")
        host = self.host
        path = self.path
        headers = self.headers
        url = item.get("Url")
        run = item.get("Run")
        method = item.get("Method")
        sql_variable = item.get("sql变量")
        sqlps = item.get("SQL")
        item_headers = item.get("Headers") if item.get("Headers") else {}
        parameters = item.get("请求参数")
        parameters_key = item.get("提取请求参数")
        encryption = item.get("参数加密方式")
        regex = item.get("正则表达式")
        keys = item.get("正则变量")
        deps = item.get("绝对路径表达式")
        jp_dict = item.get("Jsonpath")
        sql_key = item.get("sql变量")
        expect = item.get("预期结果")
        if run.upper() != "YES":
            return
        if method == "TIME":
            try:
                time.sleep(int(url))
                logger.my_log(f"暂存成功:{url}", "info")
                return
            except Exception as e:
                MyLog().my_log(f'暂停时间必须是数字')
                raise e
        # 首先执行sql替换,将sql替换为正确的sql语句
        sql = dep_par.replace_dependent_parameter(sqlps)
        if method == "SQL" and mysql:
            try:
                execute_sql_results = mysql.do_mysql(sql)
                logger.my_log(f'sql执行成功:{execute_sql_results}', "info")
                if execute_sql_results and sql_variable:
                    # 执行sql数据提取
                    DataExtractor(execute_sql_results).substitute_data(jp_dict=sql_variable)
                    logger.my_log(f'sql 提取成功', "info")
                    return
            except Exception as e:
                logger.my_log(f'sql:{sql},异常:{e}')
                raise e

        try:
            # 执行 sql 操作
            sql_res = mysql.do_mysql(sql)
            # 执行sql数据提取
            DataExtractor(sql_res).substitute_data(jp_dict=sql_key)
        except:
            sql_res = "想啥呢?数据库都没有配置还想执行数据库操作?"
            logger.my_log(sql_res)
        # 替换 URL, PARAMETERS, HEADER,期望值
        url = dep_par.replace_dependent_parameter(url)
        parameters = dep_par.replace_dependent_parameter(parameters)
        item_headers = dep_par.replace_dependent_parameter(item_headers)
        headers = {**headers, **item_headers}
        expected = dep_par.replace_dependent_parameter(expect)
        # 提取请求参数信息
        DataExtractor(parameters).substitute_data(jp_dict=parameters_key)
        # 判断是否执行加密
        if encryption:
            parameters = do_encrypt(encryption, parameters)  # 数据加密:MD5 or sha1
        logger.my_log(f"当前用例所在的sheet--> {sheet}", "info")
        logger.my_log(f"请求地址--> {host + path + url}", "info")
        logger.my_log(f"请求头--> {headers}", "info")
        logger.my_log(f"请求body--> {parameters}", "info")
        logger.my_log(f"执行SQL语句--> {sql}", "info")
        logger.my_log(f"执行sql结果--> {sql_res}", "info")
        logger.my_log(f"预期结果--> {expected}", "info")
        try:
            # 执行请求操作
            response = req(host + path, method, url, data=parameters, headers=headers)
            logger.my_log(f"接口响应--> {response.text}", "info")
            logger.my_log(f"接口耗时--> {response.elapsed}", "info")
        except Exception as e:
            result = "失败"
            logger.my_log(f'{result}:{item_id}-->{name}_{description},异常:{e}')
            raise e

        result_tuple = Validator().run_validate(expected, response.json())  # 执行断言 返回结果元组
        result = "PASS"
        try:
            self.assertNotIn("FAIL", result_tuple, "FAIL 存在结果元组中")
        except Exception as e:
            result = "FAIL"
            raise e
        finally:
            pass
            # 响应结果及测试结果回写 excel
            # excel_handle.write_back(
            #     sheet_name=sheet,
            #     i=item_id,
            #     response_value=response.text,
            #     test_result=result,
            #     assert_log=str(result_tuple)
            # )
        try:
            # 提取响应
            DataExtractor(response.json()).substitute_data(regex=regex, keys=keys, deps=deps, jp_dict=jp_dict)
        except:
            logger.my_log(
                f"提取响应失败:{name}_{description}:"
                f"\nregex={regex},"
                f" \nkeys={keys}, "
                f"\ndeps={deps}, "
                f"\njp_dict={jp_dict}")

    def tearDown(self) -> None:
        logger.my_log("-----------------------------------end_test_api-----------------------------------", "info")

    @classmethod
    def tearDownClass(cls) -> None:
        pass


if __name__ == '__main__':
    unittest.main()

 

posted @ 2023-04-10 16:25  测试玩家勇哥  阅读(898)  评论(0)    收藏  举报