【Python模块】logging 日志模块
Python 提供内置的日志模块 logging,从不同角度讨论日志的设置(基于basicConfig方法、基于Handler、基于配置文件等),重要的组件总结及记录异常日志。
当入门一门语言时,最简单最直观的打印日志信息方式就是使用 print() 函数了,而这毕竟是自己练习和测试才会这样做。当参与项目时一定会去使用日志模块实现日志信息的打印和记录,而 Python 提供了内置的日志模块 logging,有必要深入了解一下哦。
1、日志选项的基本设置
logging 日志的级别一共有五种,且存在输出的优先级:critical > error > warning > info > debug,默认情况下是不会打印 info 和 debug 级别日志的,测试如下:
引入 logging 日志模块后,如果不进行任何选项的设置,只会输出 warning 级别及高于 warning 级别的日志,且输出的格式也比较单一,默认格式为【日志级别:root:日志输出内容】。
此时,我们可使用 basicConfig() 方法进行更多的设置,该方法参数选项有:
- filename:指定输出日志文件的名称;
- filemode:日志信息写入文件的模式,默认a;
- format:格式化,选项有:
- %(levelno)s:打印日志级别的数值
- %(asctime)s:打印日志的时间
- %(levelname)s:打印日志级别的名称
- %(pathname)s:打印当前执行程序的路径,其实就是sys.argv[0]
- %(filename)s:打印当前执行程序名
- %(funcName)s:打印日志的当前函数
- %(lineno)d:打印日志当前的行号
- %(process)d:打印进程ID
- %(thread)d:打印线程ID
- %(threadName)s:打印线程名称
- %(message)s:打印日志信息
- datefmt:指定格式化的日期时间;
- style:指定格式化符号,默认%;
- level:设置日志级别;
- stream:指定日志输出流 stream 或文件 filename,与 filename 同时指定时, 会忽略 stream;
- handlers:指定日志记录处理器;
- force:指定为 true 时,会忽略根日志记录器里的任何程序。
根据该 API 的参数选项,就可以自定义配置日志输出的风格了,举例说明:
import logging
from datetime import date, datetime
filename = f'D:\\XXX\\{str(date.today())}.log'
format_option = '%(asctime)s - %(filename)s[line:%(lineno)d] - %(threadName)s - %(levelname)s: %(message)s'
logging.basicConfig(filename=filename, # 输出到指定log文件
filemode='a+', # 以a+写入
format=format_option,
datefmt='%Y-%m-%d %H:%M:%S', # 日期时间格式:2022-11-25 21:59:59
level=logging.INFO # 高于该级别的日志都可以输出显示)
logging.info(f"current datetime {datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')}")
logging.warning(f"current datetime {datetime.strftime(datetime.now(), '%Y/%m/%d %H:%M:%S')}")
输出到 2022-11-25.log 文件的日志,如下:
当然,使用 basicConfig() 设置日志风格的方式也存在局限性,比如:切割日志、将日志记录到多个文件中。
2、基于处理器 Handler 设置日志选项
Python 也是一门面向对象的语言,可以通过 Handler 对象进行日志选项的设置,有以下两种方式引入 Handler。
<1>、通过 logging 模块引入的 Handler 有:
- Handler:是一个基类,它定义了所有处理器应该具有的接口,并建立了子类可以使用(或覆盖)的一些默认行为;
- StreamHandler:将日志记录输出到数据流中,比如 sys.stdout(标准输出流), sys.stderr(标准错误流)或任何文件类对象(即任何支持 write() 和 flush() 方法的对象);
- FileHandler:将日志记录输出到磁盘文件,继承自 StreamHandler ;
- NullHandler:它不执行任何格式化或输出, 它实际上是一个供库开发者使用的“无操作”处理程序。
这里,最常用的是 StreamHandler 和 FileHandler ,后面会进行详细的说明。
<2>、通过 logging.handlers 模块引入的 Handler 有:
- HTTPHandler:使用 GET 或 POST 方法将消息发送到 HTTP 服务器;
- QueueHandler:将消息发送到队列,例如在 queue 或 multiprocessing 模块中实现的队列;
- SMTPHandler:将消息发送到指定的电子邮件地址;
- MemoryHandler:将消息发送到内存中的缓冲区,只要满足特定条件,缓冲区就会刷新;
- SocketHandler:将消息发送到 TCP/IP 套接字(从 3.4 开始,也支持 Unix 域套接字);
- DatagramHandler:将消息发送到 UDP 套接字(从 3.4 开始,也支持 Unix 域套接字);
- RotatingFileHandler:将消息发送到磁盘文件,支持最大日志文件大小和日志文件切割;
- TimedRotatingFileHandler:将消息发送到磁盘文件,以特定的时间间隔切割日志文件;
- BaseRotatingHandler:是轮换日志文件的处理器的基类。它并不应该直接实例化。而应该使用 RotatingFileHandler 或 TimedRotatingFileHandler 代替它;
- NTEventLogHandler:将消息发送到 Windows NT/2000/XP 事件日志;
- SysLogHandler:将消息发送到 Unix syslog 守护程序,可能在远程计算机上;
- WatchedFileHandler:会监视他们要写入日志的文件。如果文件发生更改,则会关闭该文件并使用文件名重新打开,此处理器仅在类 Unix 系统上有用,Windows 不支持依赖的基础机制。
logging 模块提供如此丰富的日志处理器,我们可以根据不同的使用场景选用相应的 Handler。比如,实现日志切割选用 RotatingFileHandler 就最为合适;解决多进程导致的日志记录阻塞问题,选用 QueueHandler 就比较合适;.....,后面也会进行详细的说明。
2.1 StreamHandler
Stream 日志记录处理器,支持自定义日志风格,会以流的形式将日志内容输出,默认输出到 sys.stderr,也可以在构造器内指定具体的 steam 实例。演示如下:
# 获取日志记录器
logger = logging.getLogger()
# 设置全局日志输出级别
logger.setLevel(logging.INFO)
# 创建stream日志记录处理器,默认使用sys.stderr
streamHandler = logging.StreamHandler()
# 定义日志输出风格(格式器)
format_option = '%(asctime)s - %(filename)s[line:%(lineno)d] - %(threadName)s - %(levelname)s: %(message)s'
streamHandler.setFormatter(logging.Formatter(format_option))
# 将日志记录处理器加入日志对象
logger.addHandler(streamHandler)
logger.info(f"current datetime {datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')}")
logger.info(f"current datetime {datetime.strftime(datetime.now(), '%Y/%m/%d %H:%M:%S')}")
效果如下:
2.2 FileHandler
文件日志记录处理器,也支持自定义日志风格,并将日志的内容输出到磁盘文件保存。构造器支持日志输出文件名称、写入模式、编码方式、是否延迟记录 delay 等选项设置。演示如下:
logger = logging.getLogger() # 获取日志记录器
logger.setLevel(logging.INFO) # 设置全局日志输出级别
# 创建文件日志记录处理器,并指定一些设置选项
fileHandler = logging.FileHandler(filename=f'D:\\XXX\\{str(date.today())}_fileHandler.log',
mode='a+', encoding='utf-8', delay=False)
# 定义日志输出风格(格式器)
format_option = '%(asctime)s - %(filename)s[line:%(lineno)d] - %(threadName)s - %(levelname)s: %(message)s'
fileHandler.setFormatter(logging.Formatter(format_option))
# 定义日志输出级别(这个不起作用,需要使用全局定义)
# fileHandler.setLevel(logging.DEBUG)
# 将日志记录处理器加入日志对象
logger.addHandler(fileHandler)
logger.info(f"current datetime {datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')}")
logger.info(f"current datetime {datetime.strftime(datetime.now(), '%Y/%m/%d %H:%M:%S')}")
效果如下:
请注意,如果 delay 参数为 True 时,则文件打开会被推迟至第一次调用 emit() 方法,而 emit() 方法需要引入 logging.LogRecord(),这里不再深入研究了。
2.3 RotatingFileHandler 实现切割日志
RotatingFileHandler 的父类为 BaseRotatingHandler,而 BaseRotatingHandler 的父类是 logging.FileHandler,因此 RotatingFileHandler 本质上是通过 FileHandler 实现操作磁盘文件的。实现日志分割,演示如下:
logger = logging.getLogger() # 获取日志记录器
logger.setLevel(logging.DEBUG) # 设置全局日志级别
# 基本设置:log文件路径、写入模式、切割后每个文件大小(5k)、文件数量(现分在4个)、编码方式、是否延迟记录日志
rotatingFileHandler = RotatingFileHandler('D:\\XXX\\test_rotatingFileHandler.log',
mode='a', maxBytes=5 * 1024,
backupCount=4, encoding='utf-8', delay=False)
# 定义日志输出风格(格式器)
format_option = '%(asctime)s - %(filename)s[line:%(lineno)d] - %(threadName)s - %(levelname)s: %(message)s'
rotatingFileHandler.setFormatter(logging.Formatter(format_option))
# 将日志记录处理器加入日志对象
logger.addHandler(rotatingFileHandler)
for i in range(1, 221):
logger.info(f"你可曾记得,这已经是我第{i}次说了,哎!")
切割效果如下:
2.4 QueueHandler 解决记录日志的阻塞问题
在 Web 应用程序中,经常会存在多个进程阻塞日志记录的问题,我们可采用 QueueHandler + QueueListener 方案加以解决。
实际中,对性能有要求的一些关键线程,可以只为日志对象连接一个 QueueHandler。日志对象只需简单地写入队列即可,这里可为队列设置足够大的容量,也可以在初始化时不设置容量上限。
QueueListener 的优势在于,可以用同一个实例为多个 QueueHandlers 服务。QueueListener 要求传入一个队列和一个或多个 handler,并启动一个内部的线程,用于侦听 QueueHandlers(或其他 LogRecords 源)发送的 LogRecord 队列,LogRecords 会从队列中移除并传给 handler 处理。演示如下:
3、基于配置文件设置日志选项
我们知道,在 Python 3.2 及以后的版本中,可以从字典中加载日志配置,也就意味着可以通过 JSON 或者 YAML 文件加载日志的配置,以 .yml 文件为例说明。
第一步,自定义 zdy_log.yml 文件,配置如下:
version: 1
disable_existing_loggers: False
# 格式器配置
formatters:
simple:
format: '%(asctime)s - %(filename)s[line:%(lineno)d] - %(threadName)s - %(levelname)s: %(message)s'
# 处理器配置
handlers:
console: # 控制台
class: logging.StreamHandler
level: DEBUG
formatter: simple
stream: ext://sys.stdout
info_file_handler: # info级别的日志文件配置
class: logging.handlers.RotatingFileHandler
level: INFO
formatter: simple
filename: info.log
maxBytes: 10485760
backupCount: 20
encoding: utf8
error_file_handler: # errors级别的日志文件配置
class: logging.handlers.RotatingFileHandler
level: ERROR
formatter: simple
filename: errors.log
maxBytes: 10485760
backupCount: 20
encoding: utf8
# 根日志
loggers:
my_module:
level: ERROR
handlers: [ info_file_handler ]
propagate: no
root:
level: INFO
handlers: [ console,info_file_handler,error_file_handler ]
第二步,编码代码,实现日志记录功能,如下:
import logging.config, yaml
from datetime import datetime
# 加载日志文件
with open('zdy_log.yml', 'r', encoding='utf-8') as f:
logging.config.dictConfig(yaml.load(f))
# 输出日志
logging.info(f"current datetime {datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')}")
logging.info(f"current datetime {datetime.strftime(datetime.now(), '%Y/%m/%d %H:%M:%S')}")
第三步,运行并查看结果:
当然,也可以选择使用 .conf 文件格式去配置日志选项,不过,这里推荐使用字典形式配置。
4、其他
4.1 logging 模块的重要组件总结
logging 日志库采用模块化方法,并提供了几类重要的组件:记录器、处理器、过滤器和格式器,作用描述如下:
- 记录器 Loggers:暴露了应用程序代码直接使用的接口,负责配置和发送日志消息到对应的处理器。
- 处理器 Handlers:负责将日志记录(由记录器创建)发送到指定的目标。
- 格式器 Formatter:负责指定最终输出中日志记录的样式。
- 过滤器 Filters:负责提供更细粒度的功能,用于确定要输出的日志记录。
日志事件信息会在 LogRecord 实例中的记录器、处理器、格式器和过滤器之间传递。
通常使用 getLogger(name=None) 方式创建记录器对象,当 name 为 None 的时候默认是根日志记录器;当设置 name 为具体名称,则按这个名称去寻找根日志记录器。记录器最常见的配置方法有:setLevel()、addFilter()和removeFilter()、addHandler()和removeHandler(),以及不同级别的日志方法(critical()/error()/....../debug()),需要注意,这种日志级别的配置是全局性的。
当创建了记录器对象后,通过 addHandler() 向自己添加0个或多个处理器对象。根据不同处理场景的需求,处理器会将日志消息分派到具体的 Handler 上。另外,处理器也提供了一些配置方法:setLevel()、setFormatter()、addFilter()和removeFilter(),其中 setLevel() 只会在该处理器内部生效。
格式器 Formatter,可轻松实现对输出日志内容的自定义,重要的参数有日志内容格式化,日期时间格式化,格式化风格,默认验证 style 的正确性,构造器如下所示:
logging.Formatter.__init__(self, fmt=None, datefmt=None, style='%', validate=True)
过滤器则是更细化的控制日志输出内容,可被记录器和处理器用来实现比按层级提供更复杂的过滤操作,基本过滤器类只允许低于日志记录器层级结构中低于特定层级的事件。通常使用 Filter(name='') 方式创建过滤器对象,如果 name 用空字符串初始化,则所有日志事件都会通过;如果用 'a.b' 初始化的过滤器,则允许 'a.b'、'a.b.c'、'a.b.c.d' 的日志事件通过,像 'a.c.b'、'b.c' 等是不允许。过滤器有3个常用的方法:addFilter(filter)、removeFilter(filter)、filter(record),如下:
class Filter(object):
def __init__(self, name=''):
.....
def filter(self, record):
.....
class Filterer(object):
def addFilter(self, filter):
.....
def removeFilter(self, filter):
.....
4.2 异常日志记录
记录日志的一个重要目的是,当程序出现问题时可以快速定位和解决,而 logging 模块提供的 error 级别日志,通过 exc_info 参数选项可以打印异常信息,如下:
try:
doSomeThings()
except Exception as e:
logging.error('error!!!!', exc_info=True)
当然,为了更方便把程序中的运行异常打印或者保存下来做异常分析,推荐使用跟踪异常信息的模块 traceback,常用 API 有:
- print_exc():直接把异常信息在终端打印出来;
- format_exc():返回异常信息的字符串,可以用来把信息记录到文件里;
将异常信息保存到文件,如下:
traceback.print_exc(file=open('traceback_ERROR.txt','w+'))
traceback 模块在跟踪异常信息方面相对灵活,建议使用。
【人生苦短,学习Python!Python模块系列将持续更新和记录......】
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)