文章目录(Table of Contents)
简介
Python 中 logging 模块定义的函数和类为应用程序和库的开发实现了一个灵活的事件日志系统。logging 模块是 Python 的一个标准库模块,由标准库模块提供日志记录 API 的关键好处是所有 Python 模块都可以使用这个日志记录功能。
这一篇会介绍 Python 自带的 logging 的模块的简单使用。
参考资料
- 之前在介绍 Python 调试的时候介绍过 logging 模块(这里只是非常简单的介绍),Python调试-logging和pdb的使用
- Python 使用 logging 指南(介绍非常详细,整体参考的是一篇的文章,推荐看一下),Python之日志处理(logging模块)
- 介绍在 Python 项目中如何配置 logging,python之配置日志的几种方式
- 关于 logging模块更加进阶的用法,可以在日志信息中添加上下文的信息,Python之向日志输出中添加上下文信息
- 一个比较简单版本的 logging 模块使用记录,Python Logging – Simplest Guide with Full Code and Examples
logging 的使用介绍
logging 模块的日志级别
logging 模块默认定义了以下几个日志等级。他允许开发人员自定义其他日志级别,但是这是不被推荐的,尤其是在开发供别人使用的库时,因为这会导致日志级别的混乱。我们看一下他默认的几个日志级别:
- DEBUG,最详细的日志信息,典型应用场景是问题诊断;
- INFO,信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作;
- WARNING,当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的;
- ERROR,由于一个更严重的问题导致某些功能不能正常运行时记录的信息;
- CRITICAL,当发生严重错误,导致应用程序不能继续运行时记录的信息;
开发应用程序或部署开发环境时,可以使用 DEBUG 或 INFO 级别的日志获取尽可能详细的日志信息来进行开发或部署调试;应用上线或部署生产环境时,应该使用 WARNING 或 ERROR 或 CRITICAL 级别的日志来降低机器的 I/O 压力和提高获取错误日志信息的效率。日志级别的指定通常都是在应用程序的配置文件中进行指定的
logging 模块的配置
在使用 logging 模块的时候,可以使用 logging.basicConfig()
来进行一些配置。该函数可以接受的关键词如下:
- filename, 指定日志输出目标文件的文件名,指定该设置项后日志就不会被输出到控制台了;
- filemode, 指定日志文件的打开模式,默认为「a」。需要注意的是,该选项要在filename指定时才有效。
- format, 指定日志格式字符串,即指定日志输出时所包含的字段信息以及它们的顺序。logging模块定义的格式字段下面会列出。
- datefmt, 指定日期/时间格式。需要注意的是,该选项要在format中包含时间字段%(asctime)s时才有效
- level, 指定日志器的日志级别,这里默认是 「WARNING」
- stream, 指定日志输出目标stream,如sys.stdout、sys.stderr 以及网络 stream。需要说明的是,stream 和 filename 不能同时提供,否则会引发 ValueError 异常
- style, Python 3.2中新添加的配置项。指定format格式字符串的风格。
- handlers, Python 3.3中新添加的配置项。该选项如果被指定,它应该是一个创建了多个Handler的可迭代对象,这些handler将会被添加到root logger。需要说明的是:filename、stream和handlers这三个配置项只能有一个存在,不能同时出现2个或3个,否则会引发 ValueError 异常。
对于上面的 format ,可以有以下的设置方式,原始的表格,可以参考链接,Python之日志处理(logging模块):
最简单的日志输出
知道了上面的配置之后,下面简单看一下如何使用 logging 来进行简单的日志记录。
- import logging
- logging.basicConfig(level=logging.DEBUG)
- logging.log(logging.DEBUG, "This is a debug log.")
- logging.log(logging.INFO, "This is a info log.")
- logging.log(logging.WARNING, "This is a warning log.")
- logging.log(logging.ERROR, "This is a error log.")
- logging.log(logging.CRITICAL, "This is a critical log.")
日志的输出结果如下所示:
接下来我们配置以下日志的格式,在日志中加上我们需要的时间和所在的文件。
- import logging
- LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s - %(filename)s"
- logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
- logging.log(logging.DEBUG, "This is a debug log.")
- logging.log(logging.INFO, "This is a info log.")
- logging.log(logging.WARNING, "This is a warning log.")
- logging.log(logging.ERROR, "This is a error log.")
- logging.log(logging.CRITICAL, "This is a critical log.")
最终的输出如下所示:
我们还可以进一步对时间的格式进行要求:
- import logging
- LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s - %(filename)s"
- DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"
- logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT)
- logging.log(logging.DEBUG, "This is a debug log.")
- logging.log(logging.INFO, "This is a info log.")
- logging.log(logging.WARNING, "This is a warning log.")
- logging.log(logging.ERROR, "This is a error log.")
- logging.log(logging.CRITICAL, "This is a critical log.")
最终的输出效果如下所示:
掌握了上面的内容之后,已经能够满足我们平时开发中需要的日志记录功能。
AttributeError: 'module' object has no attribute 'handlers'
如果只 import logging
,在使用 RotatingFileHandler
来处理日志的时候,会出现下面的报错, AttributeError: 'module' object has no attribute 'handlers'
,这是因为导入 logging 模块后并没有自动导入其子模块 handlers,我们需要将两个一起导入即可:
- import logging
- import logging.handlers
logging模块日志流处理流程
logging 日志模块的四大组件
首先我们来介绍一下 logging 模块的四大组件:
这些组件之间的关系描述:
- 「日志器(logger)」需要通过「处理器(handler)」将日志输出到指定的位置;
- 不同的「处理器(handler)」可以将日志输出到不同的位置;
- 「日志器(logger)」可以设置多个「处理器(handler)」将同一条日志输出到不同的位置;
- 每一个「处理器(handler)」可以有自己的「过滤器(filter)」实现日志过滤,只保留自己感兴趣的日志;
- 每一个「处理器(handler)」可以有自己的「格式器(formatter)」实现同一条日志以不同的格式进行输出;
logging 日志模块类的常用方法
下面介绍 logging 四大组件相关的类,Logger,Handler,Filter,Formatter;关于每一个类的具体的用法,会在后面 logging 模块的例子中进行介绍。
Logger 类
Logger 对象最常用的方法分为两个,分别是「配置方法」和「消息发送」。最常用的配置方法如下:
- Logger.setLevel(), 设置日志器将会处理的日志消息的最低严重级别;
- Logger.addHandler() 和 Logger.removeHandler(), 为该logger对象添加 和 移除一个handler对象;
- Logger.addFilter() 和 Logger.removeFilter(), 为该logger对象添加 和 移除一个filter对象;
logger 对象配置完成之后,可以使用下面的方法来创建日志记录(消息发送):
- Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), Logger.critical(), 创建一个与它们的方法名对应等级的日志记录
- Logger.exception(), 创建一个类似于Logger.error()的日志消息
- Logger.log(), 需要获取一个明确的日志level参数来创建一个日志记录
那么如何来获得 Logger 对象呢,可以使用 logging.getLogger
的方法。这里可以提供一个可选参数 name,如果不提供那么其值是「root」;
Handler 类
Handler 对象的作用是基于日志消息的 level,将消息分发到 handler 指定的位置。 Logger 可以通过 addHandler 为自己添加多个 handler 对象。
一个 Handler 中有一下几个方法是需要注意的:
我们通常是不会直接实例化 Handler 类的,我们会根据自己不同的需求(例如存储位置的不同),实例化 Handler 的子类。下面是一些常见的子类:
Formatter 类
Formatter 类用于配置日志信息的最终顺序、结构和内容。我们的代码可以直接实例化 Formatter 类。Formatter 类的构造方式如下所示:
- logging.Formatter.__init__(fmt=None, datefmt=None, style='%')
初始化的时候可以接受三个参数:
- fmt:指定消息格式化字符串,如果不指定该参数则默认使用message的原始值
- datefmt:指定日期格式字符串,如果不指定该参数则默认使用"%Y-%m-%d %H:%M:%S"
- style:Python 3.2新增的参数,可取值为 '%',
不用太看上面的介绍,只需要知道在这里设置 formatter 的时候,是和上面设置 logging.basicConfig()
是一样的设置方式。
Filter 类
Filter 可以被 Handler 和 Logger 用来做比 level 更细粒度的、更复杂的过滤功能。Filter 是一个过滤器基类,它只允许某个 logger 层级下的日志事件通过过滤。该类定义如下:
- class logging.Filter(name='')
- filter(record)
例如这些 logger 的 level 是一样的,但是我们只想得到其中部分的信息,这个时候就可以使用 filter 来进行筛选了。下面会有一个使用 Filter 将同一个级别的日志输出到不同的文件。
使用 logging 模块的例子
使用 logging 模块中的四大组件
我们使用 logging 模块的四大组件来完成一个简单的例子。现在有以下几个日志记录的需求:
- 要求将所有级别的所有日志都写入磁盘文件中
- all.log 文件中记录所有的日志信息,日志格式为:日期和时间 - 日志级别 - 日志信息
- error.log 文件中单独记录 error 及以上级别的日志信息,日志格式为:日期和时间 - 日志级别 - 文件名[:行号] - 日志信息
- 要求 all.log 在每天凌晨进行日志切割
我们对上面的需求进行简单的分析:
- 因为在 all.log 中要记录所有级别的日志,因此日志器的有效 level 需要设置为最低级别,DEBUG;
- 日志需要被发送到两个不同的目的地,因此需要为日志器设置两个 handler;另外,两个目的地都是磁盘文件,因此这两个 handler 都是与 FileHandler 相关的;
- all.log 要求按照时间进行日志切割,因此他需要用 logging.handlers.TimedRotatingFileHandler;
- error.log 没有要求日志切割,因此可以使用FileHandler;
下面是简单的代码实现:
- import logging
- import logging.handlers
- import datetime
- # 创建一个日志器logger并设置其日志级别为DEBUG
- logger = logging.getLogger('mylogger')
- logger.setLevel(logging.DEBUG)
- # 创建第一个处理器 handler, 将日志级别设置为 DEBUG
- rf_handler = logging.handlers.TimedRotatingFileHandler('all.log', when='midnight', interval=1, backupCount=7, atTime=datetime.time(0, 0, 0, 0))
- rf_handler.setLevel(logging.DEBUG)
- rf_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
- # 创建第二个处理器 handler, 日志级别是 ERROR
- f_handler = logging.FileHandler('error.log')
- f_handler.setLevel(logging.ERROR)
- f_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s"))
- # 为日志器添加处理器 handler
- logger.addHandler(rf_handler)
- logger.addHandler(f_handler)
- # 日志输出
- logger.debug('debug message')
- logger.info('info message')
- logger.warning('warning message')
- logger.error('error message')
- logger.critical('critical message')
需要注意的是,这里我们自己创建了一个日志器之后,下面日志的记录就是使用 logger.debug
而不是 logging.debug
. 上面的代码最终会生成两个日志文件:
其中 all.log 的内容如下所示:
error.log 的内容如下所示:
在一个项目中使用 logging 模块
在一个项目中,可能有多个文件,但是多个文件需要使用到同一个日志器来进行记录。这里我们就来介绍一下应该如何实现。这一部分的代码参考自项目,京东口罩爬虫-log 模块使用
我们可以在自己的项目中新建一个 log 的文件夹,里面初始化一个 logger(日志器)。整体的目录结构如下所示:
- ├─ExampleLog
- │ ├─log
- │ └─ExampleLog.py
- │ └─__init__.py
其中 ExampleLog.py 文件的内容如下所示:
- import logging
- import logging.handlers
- import os
- filePath = os.path.dirname(os.path.abspath(__file__)) # 获取当前的路径
- ALL_LOG_FILENAME = os.path.join(filePath, 'log', 'all_traffic.log')
- INFO_LOG_FILENAME = os.path.join(filePath, 'log', 'info_traffic.log')
- logger = logging.getLogger('Traffic_Scene_Log')
- def set_logger():
- """有两个 log 文件:
- - 第一个 log 文件会记录所有的内容, 方便调试的时候使用 (只输出到文件);
- - 第二个 log 文件只会记录 INFO 或以上的信息, 方便查看程序运行是否正常 (同时输出到控制台和文件);
- """
- logger.setLevel(logging.DEBUG)
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s [:%(lineno)d] - %(message)s')
- # 创建第一个 handler, 记录所有信息
- all_handler = logging.handlers.RotatingFileHandler(ALL_LOG_FILENAME, maxBytes=10485760, backupCount=3, encoding='utf-8')
- all_handler.setLevel(logging.DEBUG)
- all_handler.setFormatter(formatter)
- # 创建第二个 handler, 将 INFO 或以上的信息保存到文件
- info_file_handler = logging.handlers.RotatingFileHandler(INFO_LOG_FILENAME, maxBytes=10485760, backupCount=3, encoding='utf-8')
- info_file_handler.setLevel(logging.INFO)
- info_file_handler.setFormatter(formatter)
- # 创建第三个 handler, 将 INFO 或以上的信息输出到控制台
- info_console_handler = logging.StreamHandler()
- info_console_handler.setLevel(logging.INFO)
- info_console_handler.setFormatter(formatter)
- # 为日志器添加 handler
- logger.addHandler(all_handler)
- logger.addHandler(info_file_handler)
- logger.addHandler(info_console_handler)
- set_logger()
这个时候会把所有的日志信息输出到文件,INFO 和以上的信息会同时输出到控制台和文件中。在其他文件想要进行 log 的时候,只需要像下面把 logger 导入就可以了。(这里其实是在 ExampleLog.py 中已经初始化了一个日志器,并且已经把日志器的设置都已经设置好了,后面在使用的时候只不过是把这个日志器进行导入而已)
- from some.ExampleLog import logger
在一个项目中定义 root logger(推荐)
在上面的例子中我们会有一个 set_logger
,在每个文件只需要把 logger
导入即可。但是这样会有一个问题,及所有的日志文件会被写到模块里面,且使用不方便。于是我们这里的使用方法是定义一个 root logger
,在里面定义好 format
和 handler
(和上面类似,但是这里定义的是 root logger)
- import os
- import logging
- from datetime import datetime
- def init_logging(log_path: str = "logs", log_level: int = 0) -> None:
- """配置 root logger,该 logger 具有 3 种 handler:
- 1. sys.stdout << [NOTSET];
- 2. <log_path>_debug.log << [DEBUG].
- 3. <log_path>_info.log << [INFO];
- Args:
- log_path (str): 日志写入目录 (目录的路径)
- log_level (int): logging 的记录等级,
- - 0 < DEBUG:10 < INFO:20 < WARNING:30 < ERROR:40 < CRITICAL:50;
- - 低级别模式会记录高级别模式日志
- Returns:
- None
- """
- root_logger = logging.getLogger()
- root_logger.setLevel(log_level)
- # 创建 log 文件夹
- if not os.path.exists(log_path):
- os.makedirs(log_path, exist_ok=True)
- # logger formatter
- formatter = logging.Formatter(
- '%(asctime)s - %(levelname)s - %(filename)s [:%(lineno)d] - %(message)s')
- # 创建第一个 handler, 记录所有信息
- now = datetime.strftime(datetime.now(),'%Y-%m-%d_%H_%M_%S_%f')
- ALL_LOG_FILENAME = os.path.join(log_path, '{}.log'.format(now)) # 日志名称
- all_handler = logging.handlers.RotatingFileHandler(
- ALL_LOG_FILENAME, maxBytes=10485760, backupCount=3, encoding='utf-8')
- all_handler.setLevel(logging.DEBUG)
- all_handler.setFormatter(formatter)
- # 创建第二个 handler, 将 INFO 或以上的信息输出到控制台
- info_console_handler = logging.StreamHandler()
- info_console_handler.setFormatter(formatter)
- # 为日志器添加 handler
- root_logger.addHandler(all_handler)
- root_logger.addHandler(info_console_handler)
之后在模块内的其他文件中,可以定义其他的 logger
,他的样式会基于root logger
。例如我们可以使用如下的方式进行定义:
- import logging
- def test():
- logger = logging.getLogger(__name__)
- logger.info('xxx')
最后在外面使用的时候,只需要先调用上面的 init_logging
,接着再运行模块中其他内容,相应的日志就可以进行输出了:
- init_logging('./test_log/') # 日志初始化
- test() # 调用函数
使用 filter 实现同一个 level 的日志输出不同文件
有的时候在一个项目中,我们可能有不同功能的模块,他们的日志级别可能都是 INFO,但是我们希望将他们输出到不同的文件中去,这个时候就需要使用到 filter 这个模块了。这部分参考资料为,如何使用logging.Filter?
- import sys
- import logging
- class levelFilter(logging.Filter):
- def filter(self, record):
- if record.levelno < logging.WARNING:
- return False
- return True
- class stringFilter(logging.Filter):
- def filter(self, record):
- if record.msg.find('rl') != -1: # 这里 find 会输出字符的位置, 如果没有该字符, 则返回 -1
- return True
- return False
- log = logging.getLogger('test')
- log.setLevel(logging.INFO)
- stream_handler = logging.StreamHandler(sys.stdout)
- log.addHandler(stream_handler)
- # 没有限制
- log.warning('this is warning-1')
- log.info('this is info-1')
- # 增加一个 level 的限制, 所以只输出 warning
- log.addFilter(levelFilter())
- log.warning('this is warning-2')
- log.info('this is info-2')
- # 增加一个内容限制, 需要出现字符 rl
- stream_handler.addFilter(stringFilter())
- log.warning('this is warning-3 rl')
- log.info('this is info-3')
- log.warning('this is warning-abc')
这里最终的输出结果如下所示,这里可以对着上面的注释来看一下输出结果:
- 微信公众号
- 关注微信公众号
- QQ群
- 我们的QQ群号
评论