日志

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

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 在其中创建如下函数。

#!/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
from logging.handlers import TimedRotatingFileHandler


def get_logger(name, filename, encoding='utf-8',
               fmt=None, when='d', interval=1, backup_count=7, debug=False):
    """
    @param name:日志器的名字
    @param filename:日志文件名(包含路径)
    @param encoding:字符编码
    @param fmt:日志格式
    @param when:日志轮转时间单位
    @param interval:间隔
    @param backup_count:日志文件个数
    @param debug:调试模式
    """
    # 1. 创建日志器
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)
    # 文件处理器的等级一般情况一定比控制台要高
    if debug:
        file_level = logging.DEBUG
        console_level = logging.DEBUG
    else:
        file_level = logging.WARNING
        console_level = logging.INFO

    if fmt is None:
        fmt = '%(levelname)s %(asctime)s [%(filename)s-->line:%(lineno)d]:%(message)s'

    file_handler = TimedRotatingFileHandler(
        filename=filename, when=when, interval=interval, backupCount=backup_count, encoding=encoding)
    file_handler.setLevel(file_level)

    console_handler = logging.StreamHandler()
    console_handler.setLevel(console_level)

    formatter = logging.Formatter(fmt=fmt)
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)

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

    return logger


if __name__ == '__main__':
    log = get_logger(name='py48', filename='my.log', debug=True, when='s')
    log.info('我是普通信息')
    import time
    time.sleep(3)
    log.warning('我是3秒之后的日志')

四、应用到项目中

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']))

整体代码如下

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/8 21:08
# @Author  : shisuiyi
# @File    : test_login.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
from testcode import login
from unittestreport import ddt, list_data
# from common.read_excel_tool import get_data_from_excel
from common import get_data_from_excel  # 事先在common 中的__init__导入了get_data_from_excel
from common import logger

cases = get_data_from_excel(r'D:\Lemon\py45\day18\testdata\testdata.xlsx', 'login')


@ddt
class TestLogin(unittest.TestCase):
    """
    测试登录函数
    """

    @classmethod
    def setUpClass(cls) -> None:
        """
        类级前置
        :return:
        """
        print('我是类级前置,在整个类的测试前执行')

    @classmethod
    def tearDownClass(cls) -> None:
        """
        类级后置
        :return:
        """
        print('我是类级后置,在整个测试类测试完成后执行')

    def setUp(self) -> None:
        """
        方法级前置
        :return:
        """
        print('======我会在每个测试执行前执行====')

    def tearDown(self) -> None:
        """
        方法级后置
        :return:
        """
        print('======我会在每个测试执行后执行======')

    # @data(*cases)
    @list_data(cases)
    def test_login(self, case):
        # 1. 准备测试数据
        # print(case)
        # print('{}开始测试'.format(case['title']))
        logger.info('测试用例【{}】开始测试'.format(case['title']))
        logger.info('测试用例【{}】的测试数据是:{}'.format(case['title'], case))
        # 2. 测试步骤
        res = login(**eval(case['request']))
        logger.info('测试用例【{}】的测试结果是:{}'.format(case['title'], res))
        # 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']))


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