【logging】Python多层级日志输出

本文地址:https://www.jianshu.com/p/3be28b5d2ff8

1. 简介

在应用的开发过程中,我们常常需要去记录应用的状态,事件,结果。而Python最基础的Print很难满足我们的需求,这种情况下我们就需要使用python的另一个标准库:logging

这是一个专门用于记录日志的模块。相对于Print来说,logging提供了日志信息的分级,格式化,过滤等功能。如果在程序中定义了丰富而有条理的log信息,那么可以非常方便的去分析程序的运行状况,在有问题时也能够方便的去定位问题,分析问题。

以下是具体的一些应用场景。

执行的任务 这项任务的最佳工具
显示控制台输出 print()
报告在程序正常运行期间发生的事件 logging.info()或 logging.debug()
发出有关特定运行时事件的警告 logging.warning()
报告有关特定运行时事件的错误 抛出异常
报告错误但不抛出异常 logging.error(), logging.exception()或 logging.critical()

2. 基础用法

以下是一些logging最基础的使用方法,如果不需要深入的去定制log的话,那么只需要使用最基础的部分即可。

In [1]: import logging

In [2]: logging.info('hello world')

In [3]: logging.warning('good luck')
WARNING:root:good luck

可以看到,logging.info()的日志信息没有被输出,而logging.warning()的日志信息被输出了,这就是因为logging的日志信息分为几个不同的重要性级别,而默认输出的级别则是warning,也就是说,重要性大于等于warning的信息才会被输出。

以下是logging模块中信息的五个级别,重要性从上往下递增。

等级 什么时候使用
DEBUG 详细信息,通常仅在Debug时使用。
INFO 程序正常运行时输出的信息。
WARNING 表示有些预期之外的情况发生,或者在将来可能发生什么情况。程序依然能按照预期运行。
ERROR 因为一些严重的问题,程序的某些功能无法使用了。
CRITICAL 发生了严重的错误,程序已经无法运行。

我们也可以通过设置来设定输出日志的级别:

In [1]: import logging

In [2]: logging.basicConfig(level=logging.DEBUG)

In [3]: logging.info('hello world')
INFO:root:hello world

可以看到,在设定了level参数为logging.DEBUG后,logging.info()的日志信息就正常输出了。


2.1. basicConfig

logging.basicConfig(**kwargs)

通过basicConfig()方法可以为logging做一些简单的配置。此方法可以传递一些关键字参数。

  • filename

    文件名参数,如果指定了这个参数,那么logging会把日志信息输入到指定的文件之中。

    import logging
    logging.basicConfig(filename='example.log')
    logging.warning('Hello world')
    
  • filemode

    如果指定了filename来输出日志到文件,那么filemode就是打开文件的模式,默认为'a',追加模式。当然也可以设置为'w',则每一次输入都会丢弃掉之前日志文件中的内容。

  • format

    指定输出的log信息的格式。

    In [1]: import logging
    
    In [2]: logging.basicConfig(format='%(asctime)s %(message)s')
    
    In [3]: logging.warning('hello world')
    2018-07-06 16:28:12,074 hello world
    
  • datefmt

    如果在format中使用了asctime输出时间,那么可以使用此参数控制输出日期的格式,使用方式与time.strftime()相同。

  • level

    设置输出的日志的级别,只有高出此级别的日志信息才会被输出。

    In [1]: import logging
    
    In [2]: logging.basicConfig(level=logging.INFO)
    
    In [3]: logging.info('hi')
    INFO:root:hi
    
    In [4]: logging.debug('byebye')
    

注:需要注意的是,basicConfig()方法是一个一次性的方法,只能用来做简单的配置,多次的调用basicConfig()是无效的。


3. 模块化定制logging

在深度使用logging来定制日志信息之前,我们需要先来了解一下logging的结构。logging的主要逻辑结构主要由以下几个组件构成:

  • Logger:提供应用程序直接使用的接口。

  • Handler:将log信息发送到目标位置。

  • Filter:提供更加细粒度的log信息过滤。

  • Formatter:格式化log信息。

这四个组件是logging模块的基础,在基础用法中的使用方式,其实也是这四大组件的封装结果。

这四个组件的关系如下所示:

image.png

logger主要为外部提供使用的api接口,而每个logger下可以设置多个Handler,来将log信息输出到多个位置,而每一个Handler下又可以设置一个Formatter和多个Filter来定制输出的信息。


3.1. Logger

Logger这个对象主要有三个任务要做:

  • 向外部提供使用接口。
  • 基于日志严重等级(默认的过滤组件)或filter对象来决定要对哪些日志进行后续处理。
  • 将日志消息传送给所有符合输出级别的Handlers

logging.getLogger(name=None)

首先,我们需要通过getLogger()方法来生成一个Logger,这个方法中有一个参数name,则是生成的Logger的名称,如果不传或者传入一个空值的话,Logger的名称默认为root。

In [1]: import logging

In [2]: logger = logging.getLogger('nanbei')

需要注意的是,只要在同一个解释器的进程中,那么相同的Logger名称,使用getLogger()方法将会指向同一个Logger对象。

而使用logger的一个好习惯,是生成一个模块级别的Logger对象:

In [1]: logger = logging.getLogger(__name__)

通过这种方式,我们可以让logger清楚的记录下事件发生的模块位置。

除此之外,logger对象是有层级结构的:

  • Logger的名称可以是一个以.分割的层级结构,每个.后面的Logger都是.前面的logger的子辈。

    例如,有一个名称为nanbeilogger,其它名称分别为nanbei.ananbei.bnanbei.a.c都是nanbei的后代。

  • Logger在完成对日志消息的处理后,默认会将log日志消息传递给它们的父辈Logger相关的Handler

    因此,我们不不需要去配置每一个的Logger,只需要将程序中一个顶层的Logger配置好,然后按照需要创建子Logger就好了。也可以通过将一个loggerpropagate属性设置为False来关闭这种传递机制。

例如:

In [1]: import logging
# 生成一个名称为nanbei的Logger
In [2]: logger = logging.getLogger('nanbei')
# 生成一个StreamHandler,这个Handler可以将日志输出到console中
In [3]: sh = logging.StreamHandler()
# 生成一个Formatter对象,使输出日志时只显示Logger名称和日志信息
In [4]: fmt = logging.Formatter(fmt='%(name)s - %(message)s')
# 设置Formatter到StreamHandler中
In [5]: sh.setFormatter(fmt)
# 将Handler添加到Logger中
In [6]: logger.addHandler(sh)
# 生成一个nanbei的子Logger:nanbei.child
In [7]: child_logger = logging.getLogger('nanbei.child')
# 可以看到两个Logger输出的日志信息都使用了相同的日志格式
In [8]: logger.warning('hello')
nanbei - hello

In [9]: child_logger.warning('hello')
nanbei.child - hello

Logger对象中,主要提供了以下方法:

方法 描述
Logger.setLevel() 设置日志器将会处理的日志消息的最低输出级别
Logger.addHandler() 和 Logger.removeHandler() 为该logger对象添加、移除一个handler对象
Logger.addFilter() 和 Logger.removeFilter() 为该logger对象添加、移除一个filter对象
Logger.debug(),Logger.info(),Logger.warning(),Logger.error(),Logger.critical() 输出一条与方法名对应等级的日志
Logger.exception() 输出一条与Logger.error()类似的日志,包含异常信息
Logger.log() 可以传入一个明确的日志level参数来输出一条日志

3.2. Handler

Handler的作用主要是把log信息输出到我们希望的目标位置,其提供了如下的方法以供使用:

方法 描述
Handler.setLevel() 设置handler处理日志消息的最低级别
Handler.setFormatter() 为handler设置一个格式器对象
Handler.addFilter() 和 Handler.removeFilter() 为handler添加、删除一个过滤器对象

我们可以通过这几个方法,给每一个Handler设置一个Formatter和多个Filter,来定制不同的输出log信息的策略。

Handler本身是一个基类,不应该直接实例化使用,我们应该使用的是其多种多样的子类,每一个不同的子类可以将日志信息输出到不同的目标位置,以下是一些常用的Handler

Handler 描述
logging.StreamHandler 将日志消息发送到输出到Stream,如std.out, std.err或任何file-like对象。
logging.FileHandler 将日志消息发送到磁盘文件,默认情况下文件大小会无限增长
logging.handlers.RotatingFileHandler 将日志消息发送到磁盘文件,并支持日志文件按大小切割
logging.hanlders.TimedRotatingFileHandler 将日志消息发送到磁盘文件,并支持日志文件按时间切割
logging.handlers.HTTPHandler 将日志消息以GET或POST的方式发送给一个HTTP服务器
logging.handlers.SMTPHandler 将日志消息发送给一个指定的email地址
logging.NullHandler 该Handler实例会忽略error messages,通常被想使用logging的library开发者使用来避免'No handlers could be found for logger XXX'信息的出现。

3.3. Filter

Filter可以被HandlerLogger用来做比level分级更细粒度的、更复杂的过滤功能。

Filter是一个过滤器基类,它可以通过name参数,来使这个logger下的日志通过过滤。

class logging.Filter(name='')

比如,一个Filter实例化时传递的name参数值为A.B,那么该Filter实例将只允许名称为类似如下规则的Loggers产生的日志通过过滤:A.BA.B.CA.B.C.DA.B.D

而名称为A.BBB.A.BLoggers产生的日志则会被过滤掉。如果name的值为空字符串,则允许所有的日志事件通过过滤。

In [1]: import logging

In [2]: logger = logging.getLogger('nanbei')

In [3]: filt = logging.Filter(name='nanbei.a')

In [4]: sh = logging.StreamHandler()

In [5]: sh.setLevel(logging.DEBUG)

In [6]: sh.addFilter(filt)

In [7]: logger.addHandler(sh)

In [8]: logging.getLogger('nanbei.a.b').warning('i am nanbei.a.b')
i am nanbei.a.b

In [9]: logging.getLogger('nanbei.b.b').warning('i am nanbei.a.b')

可以看到,名称为nanbei.b.bLogger的日志没有被输出。


3.4. Formatter

Formater对象用于配置日志信息的最终顺序、结构和内容。

Formatter类的构造方法定义如下:

logging.Formatter.__init__(fmt=None, datefmt=None, style='%')
  • fmt

    这个参数主要用于格式化log信息整体的输出。

    以下是可以用来格式化的字段:

    字段/属性名称 使用格式 描述
    asctime %(asctime)s 日志事件发生的时间--人类可读时间,如:2003-07-08 16:49:45,896
    created %(created)f 日志事件发生的时间--时间戳,就是当时调用time.time()函数返回的值
    relativeCreated %(relativeCreated)d 日志事件发生的时间相对于logging模块加载时间的相对毫秒数(目前还不知道干嘛用的)
    msecs %(msecs)d 日志事件发生事件的毫秒部分
    levelname %(levelname)s 该日志记录的文字形式的日志级别('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
    levelno %(levelno)s 该日志记录的数字形式的日志级别(10, 20, 30, 40, 50)
    name %(name)s 所使用的日志器名称,默认是'root',因为默认使用的是 rootLogger
    message %(message)s 日志记录的文本内容,通过 msg % args计算得到的
    pathname %(pathname)s 调用日志记录函数的源码文件的全路径
    filename %(filename)s pathname的文件名部分,包含文件后缀
    module %(module)s filename的名称部分,不包含后缀
    lineno %(lineno)d 调用日志记录函数的源代码所在的行号
    funcName %(funcName)s 调用日志记录函数的函数名
    process %(process)d 进程ID
    processName %(processName)s 进程名称,Python 3.1新增
    thread %(thread)d 线程ID
    threadName %(thread)s 线程名称
  • datefmt

    如果在dmt中指定了asctime,那么这个参数可以用来格式化asctime的输出,使用方式与time.strftime()相同。

  • style

    Python 3.2新增的参数,可取值为 '%', '{'和 '$',如果不指定该参数则默认使用'%'。


4. 使用字典配置Logger

可以看到使用logging内置的方法去配置Logger的话,会比较繁琐,特别是配置多个Logger的时候,写的代码会很多很杂乱。logging还提供了文件配置和字典配置两种方式,可以使代码更有条理,但由于文件配置的API比较老旧,有一些功能不能使用,所以这里我们只介绍字典配置方式。

从字典配置主要使用以下方法:

logging.config.dictConfig(config)

此方法通过传入一个字典来进行配置,字典中可包含的key如以下所示:

  • version - 必选项,其值是一个整数值,表示配置格式的版本,当前唯一可用的值是1。
  • disable_existing_loggers - 可选项,默认值为True。该选项用于指定是否禁用已存在的日志器loggers,如果incremental的值为True则该选项将会被忽略。
  • incremental - 可选项,默认值为False。该选项的意义在于,如果这里定义的对象已经存在,那么这里对这些对象的定义是否应用到已存在的对象上。值为False表示,已存在的对象将会被重新定义。
  • root - 可选项,这是root logger的配置信息,其值也是一个字典对象。除非在定义其它logger时明确指定propagate值为no,否则root logger定义的handlers都会被作用到其它logger上。
  • loggers - 可选项,其值是一个字典对象,该字典对象每个元素的key为要定义的日志器名称,value为日志器的配置信息组成的字典,其中包含的选项有:
    • level (optional). logger的level。
    • propagate (optional). 是否传播给父记录器。
    • filters (optional). 包含的filters列表。
    • handlers (optional). 包含的handlers列表。
  • handlers - 可选项,其值是一个字典对象,该字典对象每个元素的key为要定义的处理器名称,value为处理器的配置信息组成的字典,包含的选项有:
    • class (mandatory) - handler的类型。
    • level (optional) - handler的level。
    • formatter (optional) - handler使用的formatter。
    • filters (optional) - 包含的filters列表。
  • formatters - 可选项,其值是一个字典对象,该字典对象每个元素的key为要定义的格式器名称,value为格式器的配置信息组成的dict,如format和datefmt。
  • fittlers - 可选项,其值是一个字典对象,该字典对象每个元素的key为要定义的过滤器名称,value为过滤器的配置信息组成的dict,如name。

在这里并没有完全列出每一个对象所需的key,但熟悉模块化定制logger之后,其构造所需的参数与字典构造基本是一致的,以下有一个使用简单的例子:

import logging
import logging.config
import os

path = os.path.abspath(__file__)
BASE_DIR = os.path.dirname(os.path.dirname(path))

debug_flag = True

# 给过滤器使用的判断
class RequireDebugTrue(logging.Filter):
    # 实现filter方法
    def filter(self, record):
        return debug_flag

logging_config = {
    #必选项,其值是一个整数值,表示配置格式的版本,当前唯一可用的值就是1
    'version': 1,
    # 是否禁用现有的记录器
    'disable_existing_loggers': False,

    # 过滤器
    'filters': {
        'require_debug_true': {
            '()': RequireDebugTrue,   #在开发环境,我设置DEBUG为True;在客户端,我设置DEBUG为False。从而控制是否需要使用某些处理器。
        }
    },

    #日志格式集合
    'formatters': {
        'simple': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        },
    },

    # 处理器集合
    'handlers': {
        # 输出到控制台
        'console': {
            'level': 'DEBUG',  # 输出信息的最低级别
            'class': 'logging.StreamHandler',
            'formatter': 'simple',  # 使用standard格式
            'filters': ['require_debug_true', ]
        },
        # 输出到文件
        'log': {
            'level': 'DEBUG',
            'class': 'logging.handlers.RotatingFileHandler',
            'formatter': 'simple',
            'filename': os.path.join(BASE_DIR, 'debug.log'),  # 输出位置
            'maxBytes': 1024 * 1024 * 5,  # 文件大小 5M
            'backupCount': 5,  # 备份份数
            'encoding': 'utf8',  # 文件编码
        },
    },

    # 日志管理器集合
    'loggers':{
        'root': {
            'handlers': ['console','log'],
            'level': 'DEBUG',
            'propagate': True,  # 是否传递给父记录器
        },
        'simple': {
            'handlers': ['console','log'],
            'level': 'WARN',
            'propagate': True,  # 是否传递给父记录器,
        }
    }
}

logging.config.dictConfig(logging_config)
logger = logging.getLogger('root')

# 尝试写入不同消息级别的日志信息
logger.debug("debug message")
logger.info("info message")
logger.warn("warn message")
logger.error("error message")
logger.critical("critical message")
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342