python日志记录系列教程,内置logging模块(二)——logging日志进阶教程(五大核心组件)

前言:前面的一篇文章介绍了简单的日志记录的一些操作方法,一般都是直接通过使用 logging.xxxx() 的方式,这是最简单初步的日志记录,不涉及任何复杂的类和对象,但是logging日志记录本是一个非常复杂的东西,里面还会涉及到很多的概念与类,比如:记录器、处理程序、过滤器和格式化程序等概念,涉及到的类有Logger类、Handler类、Formatter类、Filter类、LogRecord类、LogAdapter类等,本文将会深入探讨它们的机制与使用方法。本篇为系列文章的第二篇。

一、日志记录的核心概念与基本类

对应关系如下:

  • 记录器——Logger类
  • 处理程序——Handler类
  • 过滤器——Filter类
  • 格式化器——Formatter类
  • 以及 LogRecord类、LogAdapter类等,

看了很多的文章,很少有专门介绍这几个概念与这几个类的,有的都是一些很书面化的东西,不够通俗易懂,决定自己写一篇,如果是有错误的地方,望有大神指正。

1.1 日志处理的一般流程

(1)logging日志模块四大组件

在介绍logging模块的日志流处理流程之前,我们先来介绍下logging模块的四大组件:

组件名称 对应类名 功能描述
日志器 Logger 提供了应用程序可一直使用的接口
处理器 Handler 将logger创建的日志记录发送到合适的目的输出
过滤器 Filter 提供了更细粒度的控制工具来决定输出哪条日志记录,丢弃哪条日志记录
格式器 Formatter 决定日志记录的最终输出格式

logging模块就是通过这些组件来完成日志处理的,上面所使用的logging模块级别的函数(即第一篇文章中的那些函数)也是通过这些组件对应的类来实现的。

这些组件之间的关系描述:

  • 日志器(logger)需要通过处理器(handler)将日志信息输出到目标位置,如:文件、sys.stdout、网络等;
  • 不同的处理器(handler)可以将日志输出到不同的位置;
  • 日志器(logger)可以设置多个处理器(handler)将同一条日志记录输出到不同的位置;
  • 每个处理器(handler)都可以设置自己的过滤器(filter)实现日志过滤,从而只保留感兴趣的日志;
  • 每个处理器(handler)都可以设置自己的格式器(formatter)实现同一条日志以不同的格式输出到不同的地方。

简单点说就是:日志器(logger)是入口,真正干活儿的是处理器(handler),处理器(handler)还可以通过过滤器(filter)和格式器(formatter)对要输出的日志内容做过滤和格式化等处理操作

所以一般的日志记录会经历一下一些步骤:

  1. 创建logger
  2. 创建handler
  3. 定义formatter和filter,formatter又是通过LogRecord来实现的
  4. 给handler添加formatter和filter
  5. 给logger添加handler和filter

这里需要注意的是,我们的记录器Logger和处理器Handler都可以设置和使用筛选器Filter,那区别到底是什么呢?

实际上对于Logger而言,是筛选哪些东西需要发送给Handler,哪些不需要发送给它;

对于Handler而言,是筛选哪些东西可以写入日志,哪些东西不需要写入日志;

  • logger:提供日志接口,供应用代码使用。logger最长用的操作有两类:配置和发送日志消息。可以通过logging.getLogger(name)获取logger对象,如果不指定name则返回root对象,多次使用相同的name调用getLogger方法返回同一个logger对象。
  • handler:将日志记录(log record)发送到合适的目的地(destination),比如文件,socket等。一个logger对象可以通过addHandler方法添加0到多个handler,每个handler又可以定义不同日志级别,以实现日志分级过滤显示。
  • filter:提供一种优雅的方式决定一个日志记录是否发送到handler。
  • formatter:指定日志记录输出的具体格式。formatter的构造方法需要两个参数:消息的格式字符串和日期字符串,这两个参数都是可选的。

二、日志记录四大核心组件介绍(Logger、Handler、Formatter、Filter以及LogRecord和LogAdapter)

2.1 记录器——Logger类

(1)Logger类的实例的创建

 永远 不要直接实例化 Loggers,应当通过模块级别的函数 logging.getLogger(name) 。多次使用相同的名字调用 getLogger() 会一直返回相同的 Logger 对象的引用。

这里需要注意的是参数“name”到底是什么含义,下面会说明的,先记住,logger对象

logger = logging.getLogger(name)

Logger是Logging模块的主体,进行以下三项工作:

  • 1. 为程序提供记录日志的接口
  • 2. 判断日志所处级别,并判断是否要过滤
  • 3. 根据其日志级别将该条日志分发给不同handler

Logger类的方法分为两类,分别是,日志配置消息发送

(2)Logger常见的配置方法

  • Logger.setLevel():指定记录器将处理的最低严重性日志消息,其中 debug 是最低内置严重性级别, critical 是最高内置严重性级别。 例如,如果严重性级别为 INFO ,则记录器将仅处理 INFO 、 WARNING 、 ERROR 和 CRITICAL 消息,并将忽略 DEBUG 消息。

  • Logger.addHandler()和 Logger.removeHandler():从记录器对象中添加和删除处理程序对象。处理程序在以下内容中有更详细的介绍Handler类 。

  • Logger.addFilter()和Logger.removeFilter():可以添加或移除记录器对象中的过滤器。参考后面的Filter类。

(3)Logger常见的消息发送方法

  • Logger.debug()、Logger.info()、Logger.warning()、Logger.error()、Logger.critical() 都创建日志记录,包含消息和与其各自方法名称对应的级别。该消息实际上是一个格式化字符串,它可能包含标题字符串替换语法 %s 、 %d 、 %f 等等。其余参数是与消息中的替换字段对应的对象列表。关于 **kwargs ,日志记录方法只关注 exc_info 的关键字,并用它来确定是否记录异常信息。

  • Logger.exception() 创建与Logger.error() 相似的日志信息。 不同之处是,Logger.exception()同时还记录当前的堆栈追踪。仅从异常处理程序调用此方法。

  • Logger.log() 将日志级别作为显式参数。对于记录消息而言,这比使用上面列出的日志级别方便方法更加冗长,但这是自定义日志级别的方法。

(4)其他方法

  • isEnabledFor(level):指示此记录器是否将处理级别为 level 的消息。此方法首先检查由 logging.disable(level) 设置的模块级的级别,然后检查由 getEffectiveLevel() 确定的记录器的有效级别。
  • getEffectiveLevel():指示此记录器的有效级别。如果通过 setLevel() 设置了除 NOTSET 以外的值,则返回该值。否则,将层次结构遍历到根,直到找到除 NOTSET 以外的其他值,然后返回该值。返回的值是一个整数,通常为 logging.DEBUG、 logging.INFO 等等。
  • getChild(suffix):返回由后缀确定的,是该记录器的后代的记录器。 因此,logging.getLogger('abc').getChild('def.ghi') 与 logging.getLogger('abc.def.ghi') 将返回相同的记录器。 这是一个便捷方法,当使用如 __name__ 而不是字符串字面值命名父记录器时很有用。
  • filter(record):将此记录器的过滤器应用于记录,如果记录能被处理则返回 True。过滤器会被依次使用,直到其中一个返回假值为止。如果它们都不返回假值,则记录将被处理(传递给处理器)。如果返回任一为假值,则不会对该记录做进一步处理。
  • findCaller(stack_info=False):查找调用源的文件名和行号,以 文件名,行号,函数名称和堆栈信息 4元素元组的形式返回。堆栈信息将返回 None``除非 *stack_info* 为 ``True
  • handle(record):通过将记录传递给与此记录器及其祖先关联的所有处理器来处理(直到某个 propagate 值为 false)。此方法用于从套接字接收的未序列化的以及在本地创建的记录。使用 filter() 进行记录程序级别过滤。
  • hasHandlers()

2.2 处理器——Handler类

注意不要直接实例化 Handler ;这个类用来派生其他更有用的子类。但是,子类的 __init__() 方法需要调用 Handler.__init__() 。

Handler 对象负责将适当的日志消息(基于日志消息的严重性)分派给处理程序的指定目标。 Logger 对象可以使用 addHandler() 方法向自己添加零个或多个处理程序对象。作为示例场景,应用程序可能希望将所有日志消息发送到日志文件,将错误或更高的所有日志消息发送到标准输出,以及将所有关键消息发送至一个邮件地址。 此方案需要三个单独的处理程序,其中每个处理程序负责将特定严重性的消息发送到特定位置。

处理程序中很少有方法可供应用程序开发人员使用。与使用内置处理程序对象(即不创建自定义处理程序)的应用程序开发人员相关的唯一处理程序方法是以下配置方法:

  • setLevel() 方法,就像在记录器对象中一样,指定将被分派到适当目标的最低严重性。为什么有两个 setLevel() 方法?记录器中设置的级别确定将传递给其处理程序的消息的严重性。每个处理程序中设置的级别确定处理程序将发送哪些消息

  • setFormatter() 选择一个该处理程序使用的 Formatter 对象。

  • addFilter() 和 removeFilter() 分别在处理程序上配置和取消配置过滤器对象。

应用程序代码不应直接实例化并使用 Handler 的实例。 相反, Handler 类是一个基类,它定义了所有处理程序应该具有的接口,并建立了子类可以使用(或覆盖)的一些默认行为。

标准库包含很多处理程序类型(参见 有用的处理程序 );教程主要使用 StreamHandler 和 FileHandler 。

当然还有很多其他的Handler,参考下面的表格,我们常用的就是StreamHandler和FileHandler

  • StreamHandler 实例发送消息到流(类似文件对象)。

  • FileHandler 实例将消息发送到硬盘文件。

  • BaseRotatingHandler 是轮换日志文件的处理程序的基类。它并不应该直接实例化。而应该使用 RotatingFileHandler 或 TimedRotatingFileHandler 代替它。

  • RotatingFileHandler 实例将消息发送到硬盘文件,支持最大日志文件大小和日志文件轮换。

  • TimedRotatingFileHandler 实例将消息发送到硬盘文件,以特定的时间间隔轮换日志文件。

  • SocketHandler 实例将消息发送到 TCP/IP 套接字。从 3.4 开始,也支持 Unix 域套接字。

  • DatagramHandler 实例将消息发送到 UDP 套接字。从 3.4 开始,也支持 Unix 域套接字。

  • SMTPHandler 实例将消息发送到指定的电子邮件地址。

  • SysLogHandler 实例将消息发送到 Unix syslog 守护程序,可能在远程计算机上。

  • NTEventLogHandler 实例将消息发送到 Windows NT/2000/XP 事件日志。

  • MemoryHandler 实例将消息发送到内存中的缓冲区,只要满足特定条件,缓冲区就会刷新。

  • HTTPHandler 实例使用 GET 或 POST 方法将消息发送到 HTTP 服务器。

  • WatchedFileHandler 实例会监视他们要写入日志的文件。如果文件发生更改,则会关闭该文件并使用文件名重新打开。此处理程序仅在类 Unix 系统上有用; Windows 不支持依赖的基础机制。

  • QueueHandler 实例将消息发送到队列,例如在 queue 或 multiprocessing 模块中实现的队列。

  • NullHandler 实例对错误消息不执行任何操作。它们由想要使用日志记录的库开发人员使用,但是想要避免如果库用户没有配置日志记录,则显示 "无法找到记录器XXX的消息处理器" 消息的情况。有关更多信息,请参阅 配置库的日志记录 。

常用的方法如下:

这里就不一一列出来了,参考如下网址:https://docs.python.org/zh-cn/3.7/library/logging.html

2.3 格式化器——Formatter类

格式化程序对象配置日志消息的最终顺序、结构和内容。

Handler类和Logger类不同,应用程序代码可以直接实例化格式化程序类,不仅如此,如果应用程序需要特殊行为,则可能会对格式化程序进行子类化。构造函数有三个可选参数 —— 消息格式字符串、日期格式字符串和样式指示符。

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

如果没有消息格式字符串,则默认使用原始消息。如果没有日期格式字符串,则默认日期格式为:

%Y-%m-%d %H:%M:%S

最后加上毫秒数。 style 是 %,'{ ' , '$'  三者之一。 如果未指定其中一个,则将使用 '%'。具体什么含义,可以参考系列文章的第一篇中关于logging.basicConfig()中的style参数的解释。

下面是Formatter类的常用函数:

  • class logging.Formatter(fmt=Nonedatefmt=Nonestyle='%')   即构造函数
  • format(record)
  • formatTime(recorddatefmt=None)
  • formatException(exc_info)
  • formatStack(stack_info)

注意:

看到这里的一些参数,什么fmt\、datefmt、style、exc_info、stack_info,我们发现这个和系列文章里面的前面一篇文章中的。basicConfig()函数以及logging.xxx()函数中的参数是相同的,其实他们也是具有相同的含义,模块级别的函数都是通过这些低层的实现来实现的。

2.4 过滤筛选器——Filter类

Filters类可以被Handler类和Logger类使用,用于筛选过滤哪些日志级别记录,哪些日志级别不记录,前面说了决定日志纪录级别的是通过指定参数level来设置,现在还可以通过Filter类来实现。

常见的函数如下:

  • class logging.Filter(name='')  构造函数,返回一个Filter类的实例,如果参数name指定了某一个确定的logger对象,则这个logger的子对象也会拥有同样的Filter,如果不指定name,则每一个event都是一样的filter。
  • filter(record)。是否要记录指定的记录?返回零表示否,非零表示是。如果认为合适,则可以通过此方法就地修改记录。

 

2.5 LogRecord类

实例的创建,每次在创建Logger实例的时候,就会自动创建一个LogRecord实例对象;或者我们也可以通过它的工厂函数来创建LogRecord对象,它的构造函数如下:

class logging.LogRecord(name, level, pathname, lineno, msg, args, exc_info, func=None, sinfo=None)

getMessage()

2.6 LogRecord 属性

前面在设置日志输出的格式的时候,使用了一些默认名称的占位符,如下面所示:

%(asctime)s:
%(filename)s:文件名;
%(levelname)s:日志消息的文本级别;
%(message)s:日志消息;

等等,那么这些名称像asctime、filename、levelname、message到底是哪里来的呢?实际上就是默认定义在LogRecord类里面的属性,

前面说了,logging日志记录的格式化有三种不同的格式化方法可以提供选择,即“%”、“{”、“$”,那分别怎么使用呢?如下所示

(1)使用默认的百分号%来设置格式——%(attriname)s 作为占位符

format="%(levelname)s:%(asctime)s:%(message)s"

(2)使用"{" 即str.format()来设置格式——{attrname} 作为占位符

format="{levelname}:{asctime}:{message}"

(3)使用"$" 即string.Template来设置格式——${attriname}

format="${levelname}:${asctime}:${message}"

我么还可以设置更加复杂的格式显示,就像格式化字符串那样,如下:

{msecs:03d}  # 这样的格式化设置

LogRecord常见的属性有:

属性名称

格式

描述

args

不需要格式化。

The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when there is only one argument, and it is a dictionary).

asctime

%(asctime)s

Human-readable time when the LogRecord was created. By default this is of the form '2003-07-08 16:49:45,896' (the numbers after the comma are millisecond portion of the time).

created

%(created)f

Time when the LogRecord was created (as returned by time.time()).

exc_info

不需要格式化。

Exception tuple (à la sys.exc_info) or, if no exception has occurred, None.

filename

%(filename)s

Filename portion of pathname.

funcName

%(funcName)s

Name of function containing the logging call.

levelname

%(levelname)s

Text logging level for the message ('DEBUG''INFO''WARNING''ERROR''CRITICAL').

levelno

%(levelno)s

Numeric logging level for the message (DEBUGINFOWARNINGERRORCRITICAL).

lineno

%(lineno)d

Source line number where the logging call was issued (if available).

message

%(message)s

The logged message, computed as msg % args. This is set when Formatter.format() is invoked.

module 模块

%(module)s

模块 (filename 的名称部分)。

msecs

%(msecs)d

Millisecond portion of the time when the LogRecord was created.

msg

不需要格式化。

The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object (see 使用任意对象作为消息).

名称

%(name)s

Name of the logger used to log the call.

pathname

%(pathname)s

Full pathname of the source file where the logging call was issued (if available).

process

%(process)d

进程ID(如果可用)

processName

%(processName)s

进程名(如果可用)

relativeCreated

%(relativeCreated)d

Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.

stack_info

不需要格式化。

Stack frame information (where available) from the bottom of the stack in the current thread, up to and including the stack frame of the logging call which resulted in the creation of this record.

thread

%(thread)d

线程ID(如果可用)

threadName

%(threadName)s

线程名(如果可用)

2.7 LogAdapter类

这个后面再补充吧!

 

三、日志记录的一般操作步骤——五步走

  1. 第一步:创建logger,并且进行相关的设置操作
  2. 第二步:创建Formatter以及Filter对象,来设置格式以及筛选过滤设置
  3. 第三步:定义Handler,给Handler和Logger设置定义formatter和filter,formatter又是通过LogRecord来实现的
  4. 第四步:将设置好的Handler绑定到对应的Logger上面
  5. 第五步:使用logger对象来进行日志记录

下面是一个简单的代码案例

import logging
import sys

# 第一步:获取Logger实例,如果参数为空则返回root logger,并设置日志的级别等等
logger = logging.getLogger("AppName")
logger.setLevel(logging.INFO)

# 第二步:指定日志输出格式,即设置Formatter,此处是文件日志
formatter = logging.Formatter('%(asctime)s %(levelname)-8s: %(message)s')

# 第三步:创建Handler,然后通过Formatter对象来为Handler设置格式
file_handler = logging.FileHandler("test.log")
file_handler.setFormatter(formatter) # 可以通过setFormatter指定输出格式,也可以通过formatter属性直接复制

# 如果是控制台日志,则为
console_handler = logging.StreamHandler(sys.stdout)
console_handler.formatter = formatter # 可以通过setFormatter指定输出格式,也可以直接给formatter赋值


# 第四步:为logger添加的日志处理器Handler,可以自定义日志处理器让其输出到其他地方
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# 第五步:输出不同级别的日志到指定的地方
logger.debug('this is debug info')
logger.info('this is information')  # info以及以上的日志级别才会显示
logger.warning('this is warning message')
logger.error('this is error message')
logger.fatal('this is fatal message, it is same as logger.critical')  # 现在统一使用critical,不再有fatal,所以显示的也是critical
logger.critical('this is critical message')

# 移除Logger的Handler对象
# logger.removeHandler(file_handler)
'''
2020-06-08 10:56:05,078 INFO    : this is information
2020-06-08 10:56:05,078 WARNING : this is warning message
2020-06-08 10:56:05,079 ERROR   : this is error message
2020-06-08 10:56:05,079 CRITICAL: this is fatal message, it is same as logger.critical
2020-06-08 10:56:05,079 CRITICAL: this is critical message
'''
'''

运行结束之后,在日志文件test.log以及控制台都会出现上面的结果。

下一篇文章将会介绍日志的简单配置方式,参考系列文章之三。

更多关于logging的复杂以及实用使用方法,可以参考官方文档的 《logging cookbook》

 

 

 

你可能感兴趣的:(python进阶,python)