python logging 日志模块入门笔记

发布时间:2024-08-29 09:01

在项目的开发和调试中,我们常常会用print打印中间信息,来查看程序是否运行正常。但是采用print有以下缺点:
(1)print 的信息只能在控制台输出,而无法保存
(2)假如程序中到处都是print,会影响程序的可读性,并且在程序发布的时候需要删除这些print。等你再次想调试的时候又要增加print,这是一个很麻烦的事情。
(3)print 打印的信息都会输出到控制台,但是实际上程序print 的信息重要程度是不同的,在开发的不同阶段,你可能希望打印不同的信息。
(4)当程序上线之后,就无法再通过print查看程序的运行信息,无发捕捉到程序在部署之后的异常信息。

由此可见,在我们简单的程序调试中print虽然很好用,但是有没有更好的方法,可以解决这些问题呢。有,那就是日志。这篇文章主要记录python自带日志模块 logging的学习,实际上第三方日志模块甚至是java的日志模块的使用都是大同小异的。

1、日志模块 Hello world

我们上面说到,日志模块是用来代替简单粗暴的print,那么很显然,日志模块可以很简单地实现之前 print 的功能,请看以下代码:

import logging

logging.basicConfig() #加载默认配置,后面会详述如何自定义格式等
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

其中logging的 debug()、info()、warning()、error()、critical() 函数就代替了以前的print(),用来记录和输出信息。这里不同的函数是指记录的信息的级别不一样,从函数名可以很容易得到相应的级别信息。后面会详述。

上段代码的输出为:

WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message

看到这你可能觉得很奇怪,上一段代码明明记录了5行信息,怎么只输出了3行,这里就要讲到日志级别的问题,还有默认的输出级别。其实很简单,比如程序上线之后,你就不希望日志里有debug的信息了,你可能关注的是error以上的信息,你就可以设置只记录和输出error级别以上的信息,这也是日志的好处。

2、日志级别

logging日志的级别包含以下六级,从上到下重要性依次提高,其实从日志的级别名称就可以大致看出相应的意思,不同的输出使用什么级别的日志输出需要自己根据需求来确定:

NOTSET

DEBUG

INFO

WARNING

ERROR

CRITICAL

在默认的配置下,日志只记录和输出WARNING和以上级别的日志。而且日志的格式也是设置好的。那么我们是否可以修改logging的默认配置呢,当然可以!

3、修改日志的默认配置

basicConfig()默认的日志输出格式如下:
python logging 日志模块入门笔记_第1张图片
其中 root 为根 logger名称,后面会讲到自己定义logger,实际上自己定义的logger是基于根logger(默认名称为root) ,这个放到后面再讲。

假如你想定义自己输出日志的格式,你可以在基本配置的基础上进行修改,即可以修改basicConfig()方法中的参数,它的参数如下表:

参数名称 参数描述
filename 日志输出到文件的文件名
filemode 文件模式,r[+]、w[+]、a[+]
format 日志输出的格式
datefat 日志附带日期时间的格式
style 格式占位符,默认为 “%” 和 “{}”
level 设置日志输出级别
stream 定义输出流,用来初始化 StreamHandler 对象,不能 filename 参数一起使用,否则会ValueError 异常
handles 定义处理器,用来创建 Handler 对象,不能和 filename 、stream 参数一起使用,否则也会抛出 ValueError 异常

以下举例说明:

import logging

logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

这里:
filemode表示写log文件的方式,“w”代表重写文件,会覆盖掉文件已有的内容。显然还有一种模式 “a”,是在log文件后面追加日志,不会覆盖原文件。
format指的是日志输出的格式,dateformt指的是日志的时间格式。

这样设置之后,日志不会在控制台输出,而会写入到日志文件(test.log)里。能不能同时两者都输出呢?这个涉及到Handler的概念,后面再讲。

07-55-2019 20:55:57 root:DEBUG:This is a debug message
07-55-2019 20:55:57 root:INFO:This is an info message
07-55-2019 20:55:57 root:WARNING:This is a warning message
07-55-2019 20:55:57 root:ERROR:This is an error message
07-55-2019 20:55:57 root:CRITICAL:This is a critical message

可以看到,日志按照我们设置的格式输出了。

另外值得注意的一点是,当我们程序出现异常时(exception),我们也希望日志能够记录下来,但是通过logging.info(),…,logging.critical()这些无参的方法无法记录这些信息,具体的解决方法是,需要在方法里,将exc_info参数设置为True,表示捕捉异常信息。

import logging

logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
a = 5
b = 0
try:
    c = a / b
except Exception as e:
    # 下面三种方式三选一,推荐使用第一种
    logging.exception("Exception occurred")
    logging.error("Exception occurred", exc_info=True)
    logging.log(level=logging.DEBUG, msg="Exception occurred", exc_info=True)

这样做之后,日志的文件里面就会出现以下信息:

07-03-2019 21:03:53 root:ERROR:Exception occurred
Traceback (most recent call last):
  File "C:/Users/liuzard/Desktop/rnn_text_classification/logging_test.py", line 7, in 
    c = a / b
ZeroDivisionError: division by zero

我们上面介绍了基于基本的日志配置和根logger可以实现的功能,实际上,假如是简单的日志需求的话,通过这种基本配置可以解决大部分问题,同时写法也比较简单。但是假如你想要实现一些比较复杂的日志功能,那么就需要创建和定制自己的logger。

4、创建自己的logger

我们上面说到,假如要同时输出程序运行信息到日志文件还有控制台,并且,这里要加一个功能,日志和控制台记录的日志级别是不同的,那么就需要创建自己的logger了。

喜欢看代码的可以跳过下面这段文字直接先看代码。

上面其实已经提到,自己创建的logger对象实际上是基于根 logger对象,准确的说,其实日志系统里只有一个logger对象。logger类采用了单例模型,且不能直接实例化,需要通过getLogger的方式获取。假如这句话不理解的话没关系,你只要记住,你自己不管创建多少logger,实际上调用info()等方法的时候还是在用根logger的方法。

所以我们自己创建的logger可以理解为在根logger做一些设定得到的,比如将根logger的名称“root”改为我们自己想要的名字。

在介绍了logger对象之后,这里还要说一下 handler 和 formatter 的概念。

其实也很好理解,我们创建了logger对象之后,就可以用这个logger对象来记录不同级别的信息了。但是记录了这些信息之后,该如何处理这些信息呢,比如是记录为文件,还是输出到控制台呢,这里面就需要有一个东西来处理logger记录到的信息,这个东西就叫做handler,顾名思义,就是处理器。一个logger对象可以有多个handler,比如一个handler负责将日志信息记录到日志文件,另外一个handler负责将日志信息输出到控制台。

有了handler之后,formatter也就很好理解了,同样从名字就可以看出,叫格式器,负责控制输出格式的。比如上面的handler,记录到日志文件的handler我想要一种格式,输出到控制台的handler我想要另外一种格式,那么就可以给不同的handler赋予不同的formatter。

说了这么大段文字,估计各位都头疼了,上代码…

import logging
import logging.handlers

logger = logging.getLogger("logger") # 创建自己的logger对象,名字叫做“logger”
handler1 = logging.StreamHandler()   # 创建控制器对象,输出到控制台
handler2 = logging.FileHandler(filename="test.log") # 创建控制器对象,输出到日志文件

logger.setLevel(logging.DEBUG)  # 将 logger的级别设为DEBUG
handler1.setLevel(logging.WARNING)  # 将handler1 的级别设置为 Warning,也就是控制台只输出warning级别以上的信息
handler2.setLevel(logging.DEBUG) # 将handler2的级别设置为 debug,log文件记录debug级别以上的信息

# 创建一个formatter对象,并设置formatter对象的格式
formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
# 将formatter对象赋给两个handler,这样两个handler的输出格式就是formatter指定的格式了
handler1.setFormatter(formatter)
handler2.setFormatter(formatter)
#logger 对象添加两个处理器handler,这样logger记录下来信息就会通过两个handler分别处理,即输出到日志文件和控制台
logger.addHandler(handler1)
logger.addHandler(handler2)

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')

运行上述代码,我们可以就可以看到,控制台和日志文件分别输出了不同级别的日志信息。

值得注意的是,我们可以看到日志logger对象和handler对象都指定了日志级别。那么如何看最后输出的日志级别呢?基本原则是,先看logger对象的日志级别,再看handler对象的日志级别。其实很容易理解,就是日志记录到什么级别的信息是最重要的,这是logger做的事情。而基于记录到的日志信息,再决定输出什么,是后面的事,这个是handler做的事情。
举例来说,假如logger只记录到 error 级别以上的信息,而handler想输出info级别的信息,最终日志只会出现error级别的信息,因为error级别以下的信息压根没哟记录。

然后是formatter的一些参数还有写法,如下表所示,看着很多,其实只需要记得最常用的几个就行了,其他的用到的时候再查。

变量 格式 变量描述
asctime %(asctime)s 将日志的时间构造成可读的形式,默认情况下是精确到毫秒,如 2018-10-13 23:24:57,832,可以额外指定 datefmt 参数来指定该变量的格式
name %(name) 日志对象的名称
filename %(filename)s 不包含路径的文件名
pathname %(pathname)s 包含路径的文件名
funcName %(funcName)s 日志记录所在的函数名
levelname %(levelname)s 日志的级别名称
message %(message)s 具体的日志信息
lineno %(lineno)d 日志记录所在的行号
pathname %(pathname)s 完整路径
process %(process)d 当前进程ID
processName %(processName)s 当前进程名称
thread %(thread)d 当前线程ID
threadName %threadName)s 当前线程名称

最后需要注意的一点是,定义了logger之后,就不要再用logging了,因为用logging实际上是在用根logger,实际上和logger用的是同一个对象,如果同时用,相当于同时调用了两次logger对象。

5、 日志的配置

上面我们看到如何定制自己的logger对象,可以看到,可配置的参数还是挺多的。假如你想要将配置用到其他程序,那么这种复制代码的方式不是很方便,并且这些配置放在代码里也影响可读性。很显然我们可以用配置文件来解决这些问题。

(1)yml配置文件

yml格式是当前应用最为广泛的配置文件,强烈建议采用这个格式的配置文件。举例如下:
创建test.yml文件:

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
  
loggers:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

加载配置文件代码如下:

import logging.config
# 需要安装 pyymal 库
import yaml

with open('test.yaml', 'r') as f:
    config = yaml.safe_load(f.read())
    logging.config.dictConfig(config)

logger = logging.getLogger("sampleLogger")
# 省略日志输出

(2) .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")
# 省略日志输出

(3)从字典中获取配置

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")
# 省略日志输出

6、日志的划分

程序上线之后长期运行,会产生大量的日志记录,假如都记录在一个文件上的话,就会造成单个日志文件过大的问题。这里介绍两种日志的划分方法。分别是按日志的大小和时间划分。

# # 每隔 1000 Byte 划分一个日志文件,备份文件为 3 个
file_handler1 = logging.handlers.RotatingFileHandler("test.log", mode="w", maxBytes=1000, backupCount=3, encoding="utf-8")
# 每隔 1小时 划分一个日志文件,interval 是时间间隔,备份文件为 10 个
file_handler2 = logging.handlers.TimedRotatingFileHandler("test.log", when="H", interval=1, backupCount=10)

7、中文乱码问题

日志中写入中文会出现编码问题,这里可以将handler的处理编码格式设置为 utf-8。

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

ref : Python日志库logging总结-可能是目前为止将logging库总结的最好的一篇文章

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号