如何优雅地调试你的python程序之logging库使用

本文详细介绍了Python内置的logging库,包括为何需要日志记录、为何使用logging模块、何时选择不同日志级别。内容涵盖如何记录日志、组织多个模块日志、自定义logger以及日志格式的调整。此外,还讨论了在实际项目中遇到的问题,如日志文件的管理和中文乱码处理等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在开始:由于在学校没有编程规范要求,从而养成了使用print直接打屏的恶习。

1. python的日志记录工具logging

1.1 为什么需要日志记录

日志记录是一种跟踪某些软件运行时发生的事件的方法。该软件的开发人员将日志记录调用添加到其代码中,以指示已发生某些事件。事件由描述性消息描述,该消息可以可选地包含可变数据(即,对于事件的每次出现可能不同的数据)。事件也具有开发人员对事件的重要性; 重要性也可以称为水平 或严重程度

1.2 为什么使用python自带的日志记录工具

使用标准库模块提供的日志记录API的主要好处是所有Python模块都可以参与日志记录,因此您的应用程序日志可以包含您自己的消息,这些消息与来自第三方模块的消息集成在一起。

1.3 何时使用哪种日志

Python 标准库 logging 用作记录日志,默认分为六种日志级别(括号为级别对应的数值)分别对应于不同的日志的重要程度:

NOTSET(0)

DEBUG(10)

INFO(20)

WARNING(30)

ERROR(40)

CRITICAL(50)

我们自定义日志级别时注意不要和默认的日志级别数值相同,logging 执行时输出大于等于设置的日志级别的日志信息,如设置日志级别是 INFO,则 INFO、WARNING、ERROR、CRITICAL 级别的日志都会输出。默认级别为WARNING,这意味着将仅跟踪此级别及更高级别的事件,除非日志包已配置为执行其他操作。

要确定何时使用日志记录,请参阅下表,其中列出了针对一组常见任务中的每个任务的最佳工具。(双语对照,极致阅读体验)

日志记录功能以它们用于跟踪的事件的级别或严重性命名。标准水平及其适用性如下所述(按严重程度递增顺序):

2. 如何记录日志

两种方法:

1. 打印到控制台。

import logging
logging.warning('Watch out!')  # will print a message to the console
logging.info('I told you so')  # will not print anything

因为默认日志级别为warning所以下面info的日志不会打印在屏幕上

2.将它们写入磁盘文件。

import logging
logging.basicConfig(filename='example.log',level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')

上面的代码会在文件路径上生成一个example.log文件,现在,如果我们打开文件并查看我们的内容,我们应该找到日志消息:

DEBUG:root:This message should go to the log file
INFO:root:So should this
WARNING:root:And this, too

此示例还说明了如何设置作为跟踪阈值的日志记录级别。在这种情况下,因为我们将阈值设置为 DEBUG,所以打印了所有消息。注意:logging.basicConfig(filename='example.log',level=logging.DEBUG)此行应在所有的日志前

如果多次运行上述脚本,则连续运行的消息将附加到文件example.log中。如果您希望每次运行重新开始,而不记住先前运行的消息,则可以 通过将上例中的调用更改为:来指定filemode参数:

logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)

3. 如何组织多个模型的日志

如果您的程序包含多个模块,这里有一个如何组织日志记录的示例:

# myapp.py
import logging
import mylib

def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    logging.info('Started')
    mylib.do_something()
    logging.info('Finished')

if __name__ == '__main__':
    main()
# mylib.py
import logging

def do_something():
    logging.info('Doing something')

实际的日志输出应该为下面这种:

INFO:root:Started
INFO:root:Doing something
INFO:root:Finished

这种日志无法定位到日志产生的位置,程序出错了更无法定位错误,那么如何对上述两个python文件做有效和有意义的日志记录呢?

详情请见自定义logger

4. 对日志格式的更改

要更改用于显示消息的格式,您需要指定要使用的格式:

import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
logging.info('So should this')
logging.warning('And this, too')

会输出下面的日志结果: 

DEBUG:This message should appear on the console
INFO:So should this
WARNING:And this, too

发现没有,这样的日志会显得更加简洁明了

要显示事件的日期和时间,您可以在格式字符串中放置'%(asctime)s':

import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.warning('is when this event was logged.')

日志结果如下: 

12/12/2010 11:46:36 AM is when this event was logged.

到此为止,对于基础的日志使用已经完全够用了,如果你想更进一步,那么你可以继续阅读:

自定义logger

日志库采用模块化方法,并提供几类组件:记录器Logger,处理程序Handlers,过滤器Filter和格式化Formatters程序。

  • 记录器公开应用程序代码直接使用的接口(Logger用于输出的日志的总对象)。

  • 处理程序Handlers将日志记录(由记录器创建)发送到适当的目标。

  • 过滤器Filter提供了更精细的设施,用于确定要输出的日志记录(如:只输出debug以上的内容)。

  • Formatters指定最终输出中日志记录的结构和内容格式,默认的时间格式为%Y-%m-%d %H:%M:%S。

日志事件信息在LogRecord实例中的记录器,处理程序,过滤器和格式化程序之间传递。

一个系统只有一个 Logger 对象,并且该对象不能被直接实例化,没错,这里用到了单例模式,获取 Logger 对象的方法为 getLogger。注意:这里的单例模式并不是说只有一个 Logger 对象,而是指整个系统只有一个根 Logger 对象,Logger 对象在执行 info()、error() 等方法时实际上调用都是根 Logger 对象对应的 info()、error() 等方法。

我们可以创造多个 Logger 对象,但是真正输出日志的是根 Logger 对象。每个 Logger 对象都可以设置一个名字,如果设置logger = logging.getLogger(__name__),__name__ 是 Python 中的一个特殊内置变量,他代表当前模块的名称(默认为 __main__)。则 Logger 对象的 name 为建议使用使用以点号作为分隔符的命名空间等级制度。

Logger 对象可以设置多个 Handler 对象和 Filter 对象,Handler 对象又可以设置 Formatter 对象。Formatter 对象用来设置具体的输出格式,常用变量格式如下表所示,所有参数见 Python(3.7)官方文档

Logger 对象和 Handler 对象都可以设置级别,而默认 Logger 对象级别为 30 ,也即 WARNING,默认 Handler 对象级别为 0,也即 NOTSET。logging 模块这样设计是为了更好的灵活性,比如有时候我们既想在控制台中输出DEBUG 级别的日志,又想在文件中输出WARNING级别的日志。可以只设置一个最低级别的 Logger 对象,两个不同级别的 Handler 对象,示例代码如下:

import logging
import logging.handlers

logger = logging.getLogger("logger")

handler1 = logging.StreamHandler()
handler2 = logging.FileHandler(filename="test.log")

logger.setLevel(logging.DEBUG)
handler1.setLevel(logging.WARNING)
handler2.setLevel(logging.DEBUG)

formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
handler1.setFormatter(formatter)
handler2.setFormatter(formatter)

logger.addHandler(handler1)
logger.addHandler(handler2)

# 分别为 10、30、30
# print(handler1.level)
# print(handler2.level)
# print(logger.level)

logger.debug('This is a customer debug message')
logger.info('This is an customer info message')
logger.warning('This is a customer warning message')
logger.error('This is an customer error message')
logger.critical('This is a customer critical message')

控制台输出为:

2018-10-13 23:24:57,832 logger WARNING This is a customer warning message
2018-10-13 23:24:57,832 logger ERROR This is an customer error message
2018-10-13 23:24:57,832 logger CRITICAL This is a customer critical message

test.log文件输出:

2018-10-13 23:44:59,817 logger DEBUG This is a customer debug message
2018-10-13 23:44:59,817 logger INFO This is an customer info message
2018-10-13 23:44:59,817 logger WARNING This is a customer warning message
2018-10-13 23:44:59,817 logger ERROR This is an customer error message
2018-10-13 23:44:59,817 logger CRITICAL This is a customer critical message

创建了自定义的 Logger 对象,就不要在用 logging 中的日志输出方法了,这些方法使用的是默认配置的 Logger 对象,否则会输出的日志信息会重复。

2018-10-13 22:21:35,873 logger WARNING This is a customer warning message
WARNING:logger:This is a customer warning message
2018-10-13 22:21:35,873 logger ERROR This is an customer error message
ERROR:logger:This is an customer error message
2018-10-13 22:21:35,873 logger CRITICAL This is a customer critical message
CRITICAL:logger:This is a customer critical message

注意上面的日志默认登记为warning所以前面的debug和info日志不会输出。

logger配置

通过上面的例子,我们知道创建一个 Logger 对象所需的配置了,上面直接硬编码在程序中配置对象,配置还可以从字典类型的对象和配置文件获取。

从字典获取配置:

import logging.config

config = {
    'version': 1,
    'formatters': {
        'simple': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        },
        # 其他的 formatter
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'DEBUG',
            'formatter': 'simple'
        },
        'file': {
            'class': 'logging.FileHandler',
            'filename': 'logging.log',
            'level': 'DEBUG',
            'formatter': 'simple'
        },
        # 其他的 handler
    },
    'loggers':{
        'StreamLogger': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
        'FileLogger': {
            # 既有 console Handler,还有 file Handler
            'handlers': ['console', 'file'],
            'level': 'DEBUG',
        },
        # 其他的 Logger
    }
}

logging.config.dictConfig(config)
StreamLogger = logging.getLogger("StreamLogger")
FileLogger = logging.getLogger("FileLogger")
# 省略日志输出

从配置文件中获取配置信息:

test.ini

[loggers]
keys=root,sampleLogger

[handlers]
keys=consoleHandler

[formatters]
keys=sampleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_sampleLogger]
level=DEBUG
handlers=consoleHandler
qualname=sampleLogger
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=sampleFormatter
args=(sys.stdout,)

[formatter_sampleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

import logging.config

logging.config.fileConfig(fname='test.ini', disable_existing_loggers=False)
logger = logging.getLogger("sampleLogger")
# 省略日志输出

上述代码直接从test.ini中获取配置信息

 

实战中的问题

1. 中文乱码时使用utf8的编码方式

handler = logging.FileHandler(filename="test.log", encoding="utf-8")  # 自定义logger

logging.basicConfig(handlers=[logging.FileHandler("test.log", encoding="utf-8")], level=logging.DEBUG)  # 使用默认的logger

2. 有时候我们又不想让日志输出,但在这后又想输出日志。如果我们打印信息用的是 print() 方法,那么就需要把所有的 print() 方法都注释掉,而使用了 logging 后,我们就有了一键开关闭日志的 "魔法"。一种方法是在使用默认配置时,给 logging.disabled() 方法传入禁用的日志级别,就可以禁止设置级别以下的日志输出了,另一种方法时在自定义 Logger 时,Logger 对象的 disable 属性设为 True,默认值是 False,也即不禁用。

logging.disable(logging.INFO)
logger.disabled = True

3. 如果将日志保存在一个文件中,那么时间一长,或者日志一多,单个日志文件就会很大,既不利于备份,也不利于查看。我们会想到能不能按照时间或者大小对日志文件进行划分呢?答案肯定是可以的,并且还很简单,logging 考虑到了我们这个需求。logging.handlers 文件中提供了 TimedRotatingFileHandlerRotatingFileHandler 类分别可以实现按时间和大小划分。打开这个 handles 文件,可以看到还有其他功能的 Handler 类,它们都继承自基类 BaseRotatingHandler

# TimedRotatingFileHandler 类构造函数
def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None):
# RotatingFileHandler 类的构造函数
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)

示例代码如下:

# 每隔 1000 Byte 划分一个日志文件,备份文件为 3 个
file_handler = logging.handlers.RotatingFileHandler("test.log", mode="w", maxBytes=1000, backupCount=3, encoding="utf-8")

# 每隔 1小时 划分一个日志文件,interval 是时间间隔,备份文件为 10 个
handler2 = logging.handlers.TimedRotatingFileHandler("test.log", when="H", interval=1, backupCount=10)

参考文献:

https://docs.python.org/3/library/logging.html

https://docs.python.org/3/howto/logging.html#logging-basic-tutorial

https://cuiqingcai.com/6080.html

https://juejin.im/post/5bc2bd3a5188255c94465d31

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值