前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >三句话,让 logger 言听计从

三句话,让 logger 言听计从

作者头像
OpenMMLab 官方账号
发布2022-04-09 16:52:07
4220
发布2022-04-09 16:52:07
举报
文章被收录于专栏:OpenMMLab

最近要新开一个项目,配个 logger 来管理日志吧,我配!

代码语言:javascript
复制
import logging
ori_logger = logging.getLogger('custom_logger')
ori_logger.setLevel(logging.INFO)
ori_logger.addHandler(logging.StreamHandler())
ori_logger.info('learn log')
# learn log

Emmm... 感觉日志丑丑的,NOT GOOD。为啥不试试 mmcv 的 get_logger 呢,功能齐全,一步到位。呐~链接拿去:

https://github.com/open-mmlab/mmcv/blob/master/mmcv/utils/logging.py

终端日志

代码语言:javascript
复制
mmcv_logger = get_logger('mmcv_logger', log_file='a.log')
mmcv_logger.info('learn log')
# 2022-03-24 10:38:35,998 - mmcv_logger - INFO - learn log

日志文件

代码语言:javascript
复制
2022-03-24 10:37:25,832 - mmcv_logger - INFO - learn log

太方便了!时间戳、日志名、日志等级,甚至还贴心地为你保存了日志文件,一行代码轻松搞定。

mmcv 的 get_logger 到底是是如何配置出如此便利的 logger 的呢?只需要配置 logger 三宝:日志等级(loglevel)、格式控制器(formatter) 和 日志处理器(handler),让 logger 对你言听计从。

logging 之日志等级

logging 模块的日志等级如下:

代码语言:javascript
复制
CRITICAL = 50        
FATAL = CRITICAL
ERROR = 40           
WARNING = 30        
WARN = WARNING
INFO = 20            
DEBUG = 10           
NOTSET = 0          

Logging.Logger(本文中的 logger 为其实例)和 logging.Handler 的 level 属性,就表示对应实例的日志等级。当我们调用 logger.info(msg) 和 logger.warning(msg) 时,输出的消息也有日志等级。只有当消息的日志等级大于等于 logger.level 时,日志才有可能被输出。以我们上一期《是谁偷偷动了我的 logger》中提到的 logging.root 为例:

代码语言:javascript
复制
import logging

logger = logging.root
print(logger.level)  # 30, warning。root 日志等级为 WARNING (30)
logger.info("info msg")  # 日志等级为 INFO (20),小于 WARNING,无消息
logger.warning("warn msg")  # 日志等级为 WARNING (30),输出消息 warn msg
logger.error("erro msg")  # 日志等级为 ERROR (40),输出消息 erro msg
logger.setLevel(logging.ERROR) # 设置日志等级为 ERROR (40)
print(logger.level)  # 40, ERROR
logger.warning("warn msg")  # 无消息
logger.error("erro msg")  # erro msg

logging.root 的日志等级为 WARNING,因此无法输出 INFO 级别的消息。当我们把日志等级调整成 ERROR 后,则会无法输出 WARNING 级别的消息。但是奇怪的是,第一期中我们提到,logging.root 默认情况下是一个胚胎,本身不具备输出日志的能力,为什么这里仍然能够输出 warning 和 error 级别的日志呢?这里先留一个疑问,在 Handler 一节会给出答案。

logging 之 Handler

在解答上一节提出的问题之前,让我们回顾一下第一期的内容,既然 logging 模块是通过 Handler 来输出日志的,本节就先介绍 logging 模块的两大 Handler:streamHandler 和 fileHandler。

StreamHandler

向终端输出信息

配置了 StreamHandler 的 logger,可以和 print 一样向终端输出日志信息,示例如下:

代码语言:javascript
复制
logger = logging.root
# 创建 streamHandler
stream_handler = logging.StreamHandler()
# 设置日志等级
logger.setLevel(logging.INFO)
logger.info("learn logging")  # 没有配置 Handler,终端不会输出日志
# 为 root logger配置 Handler
logger.addHandler(stream_handler)
logger.info("learn logging")  #  配置 Handler,输出日志

向文件输出日志信息

实例化 StreamHandler 时,如果 stream 配置成写入的目标文件,就能将日志存储到文件中。

代码语言:javascript
复制
f = open('output.txt', 'w')
logger = logging.root
logger.setLevel(logging.INFO)
stream_handler = logging.StreamHandler(stream=f)
logger.addHandler(stream_handler)
logger.warning("learn logging")  # 此时 learn logging 会被写入到 output.txt 中
f.close()

FileHandler

FileHandler 继承自 StreamHandler,可以指定写入文件的编码格式,相比于 StreamHandler 更加灵活,易于使用:

代码语言:javascript
复制
logger = logging.root
logger.setLevel(logging.INFO)
# 设置输出文件和编码方式
file_handler = logging.FileHandler('output.txt', encoding='utf-8')
logger.addHandler(file_handler)
logger.info("WARNING")  # 此时 learn logging 会被写入到 output.txt 中

Handler 的日志等级

Handler 也有自己的日志等级。一条消息想经过 Handler 输出,需要同时满足消息的日志等级大于 logger 的日志等级和 handler 的日志等级。

代码语言:javascript
复制
logger = logging.root
logger.setLevel(logging.DEBUG)  # logger 的日志等级为 DEBUG
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)  # handler 的日志等级为 INFO
logger.addHandler(handler)
logger.debug('learn logging')  # 不满足 handler,无法被输出
logger.info('learn logging')  # 同时满足 logger 含 handler,正常输出

logging 的暗箱操作

没有 handler 也能输出日志?

首先讲结论,logger 在没有 handler 的情况下,其本身是不具备输出消息能力的,streamHandler 的第一个例子已经说明了这个问题。但是为什么 logger.warning(msg) 和 logger.error(msg) 能够在不配置 handler 的情况下,输出日志呢?这其实是 logging 模块的保护机制,对于 warning 和 error 级别的消息,如果消息的日志等级大于 logger 的日志等级,且 logger 没有配置任何的 handler,则会调用 logging 模块内置的 streamHandler 来输出信息。

为了证明这个逻辑,我们给 logger 配置一个 fileHandler,此时 logger.warning(msg) 就不会在终端输出日志了。

代码语言:javascript
复制
logger = logging.root
logger.warning("learn logging")  # 输出日志到终端
file_handler = logging.FileHandler('output.txt', encoding='utf-8')
logger.addHandler(file_handler)
logger.warning("learn logging")  # 不会输出日志到终端

坑爹的 logging.xxx

当你开心地调用 logging.info,logging.warning 时,你以为只是输出了一条日志,实际上可能给 logging.root 偷偷配置了 streamHandler。

代码语言:javascript
复制
logging.info('learn logging')
print(logging.root.handlers)
# [<StreamHandler <stderr> (NOTSET)>]

你可能会想,就多了个 streamHandler 嘛,这有啥。还记得被多重日志支配的恐怖么 。为什么使用 pytorch 1.10 突然出现了多重日志?追根溯源,是因为 pytorch 1.10 的 DistributedDataParallel 模块在 forward 过程中调用了 logging.info (https://github.com/pytorch/pytorch/blob/71f889c7d265b9636b93ede9d651c0a9c4bee191/torch/nn/parallel/distributed.py#L874),进而 logging.root 多出了一个 streamHandler(该过程发生在 mmcv.get_logger 之后,handler 的日志等级没有被设置成 ERROR),最终导致多重日志的发生。

logging 之 Formatter

如果说配置 Handler 相当于教 logger 说话,那么为 handler 配置 formatter 就相当于教 logger “优雅” 地说话。

给日志加上“主语”

我们可以通过配置 formatter,让 logger 输出的日志自带 logger 名。

代码语言:javascript
复制
logger = logging.root
handler = logging.StreamHandler()
handler.setFormatter(Formatter('%(name)s - %(message)s'))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info('learn logging')
# root - learn logging

这样输出的日志就会自带 “root” 日志名。

给日志加上时间

小学语文老师肯定教过,时间地点人物是七要素的前三甲,时间更是位居榜首,因此我们可以通过配置 formatter,让日志携带时间信息。

代码语言:javascript
复制
logger = logging.root
handler = logging.StreamHandler()
handler.setFormatter(Formatter('%(asctime)s - %(name)s - %(message)s'))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info('learn logging')
# 2022-03-22 01:36:42,900 - root - learn logging

给日志加上等级

日志等级可以凸显日志的重要性,例如我们会特别注意携带 ERROR 字段的日志。因此我们可以通过 formatter 让日志携带等级信息。

代码语言:javascript
复制
logger = logging.root
handler = logging.StreamHandler()
handler.setFormatter(Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info('learn logging')
# 2022-03-22 01:46:26,667 - root - INFO - learn logging

欸,有那味儿了,是不是和 OpenMMLab 系列的日志如出一辙?实际上 mmcv 配置 logger 的过程也是类似的,并且还有着更加全面的 handler 配置逻辑:mmcv/logging.py at master · open-mmlab/mmcv (github.com),大家可以参考借鉴哈。

小结

理解 loglevel、handler 和 formatter 的概念后,我们自己也可以动手写一个简易版的配置 logger 函数。

代码语言:javascript
复制
def custom_get_logger(name, out_file, log_level):
    # 设置日志名
    logger = logging.getLogger(name)
    # 设置日志登记
    logger.setLevel(log_level)
    # 设置格式
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s'
                                  ' - %(message)s')
    # 配置输出日志到终端的 Handler
    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(formatter)
    # 配置保存日志到文件的 Handler
    file_handler = logging.FileHandler(out_file)
    file_handler.setFormatter(formatter)
    # 添加 Handler
    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)
    return logger

custom_logger = custom_get_logger('custom_logger', 'a.log', 'INFO')
custom_logger.info('learn log')
# 2022-03-24 11:18:52,120 - custom_logger - INFO - learn log

通过 custom_get_logger 接口获取的 logger,日志格式好看,也能存储到本地备份,基本和 mmcv 对齐。不过这样配置的 logger 还存在一些隐患,例如上期提到的多重日志。要想解决这些问题,我们不妨再回过头去看 mmcv.get_logger 的代码,相信经过这一期的学习,很多逻辑就变得容易理解。

至此 logging 模块的基本功能就介绍得差不多了,但是 logging 还存在一些隐式的暗箱操作,如果想彻底搞懂 logging 模块,敬请期待下一期的内容~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 OpenMMLab 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
日志服务
日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集、日志存储到日志检索,图表分析、监控告警、日志投递等多项服务,协助用户通过日志来解决业务运维、服务监控、日志审计等场景问题。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档