Python Logging 最佳实践
背景
最近在工作中要新增一个服务,需要处理服务日志,之前每次使用 logging 都是很简单的配置,没有
了解过最佳实践, google 到了 fangpenlin 写的一篇博客(https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/),
很受用,因此翻译过来便于自己以后回顾使用,也希望大家能够正确配置日志,对运维同学友好些~
Good Logging practice in Python
在显示中,日志是重要的。当你在转账时,它们是转账记录。当飞机起飞时,黑匣子会记录所有事情。
如果某些事情发生了错误,人们可以通过阅读日志有机会弄清楚发生了什么。同样,在系统开发、排查
和运行中日志也是重要的。当你编写了一个服务,日志就是必须品。如果没有日志,我几乎不可能发现
错误如果服务已经挂掉。不仅仅是服务,日志对于桌面应用也是重要的。举个例子,当你的程序在你
客户电脑崩溃了,你可以让他们发送日志文件给你,你才有可能去查找为什么崩溃。相信我,你永远
不知道在不同的电脑环境中会发生什么奇怪的问题.
Print 不是个好主意
尽管日志很重要,但是不是所有的开发者都知道如何正确的使用。我见过一些开发者在开发过程中插入
一些 print ,在开发结束时删除这些 print。向下面这样:
1 | print 'Start reading database' |
当程序是一个简单的脚本时这是工作的,但是对于一个复杂的系统,你最好不要通过这种途径记录日志。
第一,你在日志中无法记录重要的信息,你可能看到很多垃圾信息在日志中,但是没有找到任何有用的。
你也不能通过不修改代码的方式控制日志的输出,还有可能在不使用这些 print 的时候忘记删除掉。
所有的 print 信息都将输出到标准输出中,这个是要禁止的。当然你可以输出到标准错误输出中,但是
这仍不是记录日志的最佳实践。
使用 Python 标准库 Logging
所以,如何正确的记录日志? 很简单,使用标准库中的 logging 模块。感谢 Python 社区,logging
在标准库中,它被设计的灵活且易用。你可以使用 logging 像下面这样:
1 | import logging |
你可以运行得到下面输出:
1 | INFO:__main__:Start reading database |
你可能会问这与 print 有什么区别。这样做当然是有好处的:
- 你可以控制信息级别,过滤掉不重要的信息
- 你可以决定之后输出的位置和方式
有不同的日志级别你可以使用:debug,info,warning,error 和 critical。
通过设置不同的日志级别,你可以只输出错误信息到指定的日志文件中,或者在 debug
时输出调试的详细信息。尝试改变日志级别为 debug 再观察下输出结果:
logging.basicConfig(level=logging.DEBUG)
输出结果:
1 | INFO:__main__:Start reading database |
像你所看到的那样,我们调整日志级别为 DEBUG,调试记录输出在日志中。你也可以决定
如何处理这些信息.举个例子你可以使用 FileHandler 让日志输出到一个文件中:
1 | import logging |
有很多种不同的处理方式,你可以发送日志到你的消息中心,甚至是一个远端的服务器。
你也可以自定义日志处理方式,更多信息可以参考: Basic Tutorial, Advanced Tutorial and Logging Cookbook.
设置正确的日志级别
由于 logging 的灵活性,你可以在任何地方配置适当的日志级别去记录日志,可以再之后配置他们。
你可能会问,什么是正确的日志级别,这里我将分享我的经验。
在大多数场景下,在日志中不希望去看到大量的详细信息。因此,debug 级别只是在你
调试过程中开启。我只使用调试级别来获取详细的调试信息,特别是当数据量很大或者频率
很高的时候,比如在一个 for 循环中记录某些内部的状态变化。
1 | def complex_algorithm(items): |
对于一般日志我使用 info 级别,比如,处理请求或者服务状态变化:
1 | def handle_request(request): |
当一个状态比较重要,但是又没有处于 error 状态时,我会使用 warning ,比如
当一个用户登录时使用了错误的密码或者连接很慢时:
1 | def authenticate(user_name, password, ip_address): |
当某些事情出现了错误,我使用 error 级别,比如抛出异常,I/O 操作失败或者连接失败:
1 | def get_user_by_id(user_id): |
我很少使用严重级别,你可以在某些事情真的发生时,你可以使用它。比如内存泄露,
磁盘空间不足.
使用 name 作为 logger 名称
你不需要讲 logger 名称设置为 name ,但是如果这样做,会带来一些好处。
name 变量在 Python 中是当前模块名称。举个例子,你可以调用
logger.getLogger(name) 在模块 “foo.bar.my_module”中,这相当于
logger.getLogger(foo.bar.my_module)。当你需要配置 logger,你可以配置为 “foo”,
这样所有在 foo 包中的模块都共享同样的配置。你可以在阅读日志的时候了解模块信息。
捕获异常并使用 traceback 记录
记录哪些地方出错总是一个好的做法,但是如果没有 traceback 这用处很小。你应该捕获
异常并与 traceback 一起记录他们。像这个例子:
1 | try: |
通过 exc_info=True 参数调用 logger 方法,traceback 会被记录到日志中,你可以
看到这样的结果:
1 | ERROR:__main__:Failed to open file |
你也可以调用 logger.Exception(msg, args),这相当于 logger.error(msg,exc_info=True,args)。
除非 disable_existing_loggers == False,否则不要在模块级别获取 logger
你可以看到很多的例子(包括这篇文章)在模块级别获取 logger。这看上去是无害的,
但是实际上,这是一个陷阱 - 在你从一个文件加载配置前,logging 模块会遵守所有
已经创建的 logger,如果你在模块级别获取 logger,像这种:
my_module.py1
2
3
4
5
6
7
8
9
10import logging
logger = logging.getLogger(__name__)
def foo():
logger.info('Hi, foo')
class Bar(object):
def bar(self):
logger.info('Hi, bar')
main.py
1 | import logging |
logging.ini
1 | [loggers] |
你期望看到记录显示在日志中,但是日志中是空的。为什么? 因为你在模块级别创建 logger ,
所以在从文件加载 logging 配置之前导入该模块。 logging.fileConfig 和 logging.dictConfig
默认禁用已经存在的 logger。 所以配置文件中的配置不会在你的 logger 中生效。
这适合在你需要的时候获取 logger。 你可以像下面这样编写:
1 | import logging |
通过这样做, logger 将会在你加载配置之后创建,配置会正确生效。
从 Python 2.7 开始,添加一个新的参数名为 “disable_existing_loggers” 到 fileConfig 和
dictConfig,通过将其设置为 False,可以解决上面提到的问题。比如:
1 | import logging |
使用 JSON 和 YAML 配置 logging
你可以再 Python 代码中配置 logging 参数,但是这不够灵活。更好的方式
是使用 logging 配置文件。在 Python2.7 之后,你可以加载 logging 配置
从一个字典,这意味着你可以从 JSON 或 YAML 文件中加载 logging 配置。
虽然你可以通过 old.ini 方式加载 logging 配置,但是这对于可读性和可编辑
来说是困难的。这里我将展示通过 JSON 或者 YAML 的配置文件示例。
logging.json
1 | { |
logging.yaml
1 |
|
以下配置展示了如何从 json 文件中读取 logging 配置:
1 | import os |
使用 JSON 配置的一个优点是 json 是标准库中的模块,你不需要安装。但就个人来说,
我更喜欢 YAML。这有非常好的可读性和可编辑性。你可以按照以下方式加载 YAML 配置:
1 | import os |
现在,要配置 logging,你可以在程序启动时调用 setup_logging。这将读取 logging.json 或者
logging.yaml。你也可以设置 LOG_CFG 环境变量去从指定的路径下加载配置文件。比如:
LOG_CFG=my_logging.json python my_server.py
或者
LOG_CFG=my_logging.yaml python my_server.py
配置文件轮询
如果你使用 FileHandler 去写日志,随着时间的增加日志文件大小也会增长,某一天,会撑爆你的磁盘。
为了避免这种情况,你应该在生产环境中使用 RotatingFileHandler 替换 FileHandler 。
配置日志中心
当你有多台服务器和不同的日志文件时,你可以配置一个日志中心去手机所有重要的文件。这样你可以
更方便的监控和通知你的系统发生了哪些报错。
结论
我很开心 Python logging 库设置的很好,最好的是它是标准库中的一员,你无须选择。
它非常灵活,你可以编辑自己的处理器和过滤器。还有第三方程序比如 pyzmq 提供的 ZeroMQ 日志处理,
它允许你通过 zmq socket 发送日志信息。
如果你不知道如何正确的使用 logging,那么本文对你会有所帮助。通过良好的日志记录练习,
你可以更轻松的找到系统中的问题。