python的日志模块logging

Python Logging 指南

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

python logging日志模块以及多进程日志

Python配置日志的几种方式

logging 模块

在部署项目时,不可能直接将所有的信息都输出到控制台中,我们可以将这些信息记录到日志文件中

这样不仅方便我们查看程序运行时的情况,也可以在项目出现故障时根据运行时产生的日志快速定位问题出现的位置

img

logging框架组成:
  • Loggers: 日志,暴露函数给应用程序,基于日志记录器和过滤器级别决定哪些日志有效。
  • LogRecord :日志记录器,将日志传到相应的处理器处理。
  • Handlers: 处理器, 将(日志记录器产生的)日志记录发送至合适的目的地。
    • 常用类型有StreamHandler、FileHandler、NullHandler
  • Filters: 过滤器, 提供了更好的粒度控制,它可以决定输出哪些日志记录。
  • Formatters: 格式化器, 指明了最终输出中日志记录的布局。
logging日志级别:

每个logger都有一个日志的级别。logging中定义了如下级别

Level Numeric value 解释
NOTSET 0 意指不设置 所以按照父logger级别来过滤日志
DEBUG 10 详细信息,通常仅在诊断问题时才有意义
INFO 20 确认事情按预期工作
WARNING 30 表明发生了意外情况,或表明在不久的将来出现了一些问题
(例如 “磁盘空间不足”)。但是该软件仍在按预期工作
ERROR 40 由于更严重的问题,该软件无法执行某些功能
CRITICAL 50 严重错误,表明程序本身可能无法继续运行
注意事项:

但是当发生异常时,直接使用无参数的 debug()、info()、warning()、error()、critical() 方法并不能记录异常信息

需要设置 exc_info 参数为 True 才可以,或者使用 exception() 方法,还可以使用 log() 方法,但还要设置日志级别和 exc_info 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

logging 流程

  1. 判断 Logger 对象对于设置的级别是否可用,如果可用,则往下执行,否则,流程结束。

  2. 创建 LogRecord 对象,如果注册到 Logger 对象中的 Filter 对象过滤后返回 False,则不记录日志,流程结束,否则,则向下执行。

  3. LogRecord 对象将 Handler 对象传入当前的 Logger 对象,(图中的子流程)如果 Handler 对象的日志级别大于设置的日志级别,再判断注册到 Handler 对象中的 Filter 对象过滤后是否返回 True 而放行输出日志信息,否则不放行,流程结束。

  4. 如果传入的 Handler 大于 Logger 中设置的级别,也即 Handler 有效,则往下执行,否则,流程结束。

  5. 判断这个 Logger 对象是否还有父 Logger 对象,如果没有(代表当前 Logger 对象是最顶层的 Logger 对象 root Logger),流程结束。否则将 Logger 对象设置为它的父 Logger 对象,重复上面的 3、4 两步,输出父类 Logger 对象中的日志输出,直到是 root Logger 为止。

img

Logger 使用

logging 配置

python代码

使用Python代码显式的创建loggers,handlers和formatters并分别调用它们的配置函数

通过简单方式进行配置,使用 basicConfig() 函数直接进行配置;

代码

Logger 对象和 Handler 对象都可以设置级别,而默认 Logger 对象级别为 30 ,也即 WARNING,默认 Handler 对象级别为 0,也即 NOTSET。

logging 模块这样设计是为了更好的灵活性,比如有时候我们既想在控制台中输出DEBUG 级别的日志,又想在文件中输出WARNING级别的日志。

可以只设置一个最低级别的 Logger 对象,两个不同级别的 Handler 对象,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import logging
import logging.handlers

logger = logging.getLogger("logger")

handler1 = logging.StreamHandler()
handler2 = logging.FileHandler(filename="test.log", encoding="utf-8")

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

或者
1
2
3
4
5
6
7
8
9
logging.basicConfig(filename="config.log",
filemode="w",
format="%(asctime)s-%(name)s-%(levelname)s-%(message)s",
level=logging.INFO)
logging.basicConfig(level=log_level,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename='parser_result.log',
filemode='w')

参数说明

参数名称 参数描述
filename 日志输出到文件的文件名
filemode 文件模式,r[+]、w[+]、a[+]
format 日志输出的格式
datefat 日志附带日期时间的格式
style 格式占位符,默认为 “%” 和 “{}”
level 设置日志输出级别
stream 定义输出流,用来初始化 StreamHandler 对象
不能 filename 参数一起使用,否则会ValueError 异常
handles 定义处理器,用来创建 Handler 对象,不能和 filename 、stream 参数
一起使用,否则也会抛出 ValueError 异常
输出
1
2
3
4
5
2018-05-06 11:05:34,486 - simple_logger - DEBUG - debug message.
2018-05-06 11:05:34,487 - simple_logger - INFO - info message.
2018-05-06 11:05:34,487 - simple_logger - WARNING - warning message.
2018-05-06 11:05:34,487 - simple_logger - ERROR - error message.
2018-05-06 11:05:34,487 - simple_logger - CRITICAL - critical message.

配置文件

创建一个日志配置文件,然后使用fileConfig()函数来读取该文件的内容

相对于第一种配置方式的优点在于,它将配置信息和代码分离了,这一方面降低了日志的维护成本,同时还使得非开放人员也能够很容易的修改日志配置

通过配置文件进行配置,使用fileConfig()函数读取配置文件

常见的配置文件有 ini 格式、yaml 格式、JSON 格式,或者从网络中获取都是可以的

配置文件logging.conf
  • 配置文件中一定要包含loggers、handlers、formatters这些section,它们通过keys这个option来指定该配置文件中已经定义好的loggers、handlers和formatters,多个值之间用逗号分隔;另外loggers这个section中的keys一定要包含root这个值;
  • loggers、handlers、formatters中所所指定的日志器、处理器和格式器都需要在下面单独的section中进行定义。section的命名规则为[logger_loggerName]、[handler_handlerName]、[formatter_formatterName];
  • 定义logger的section必须指定level和handlers这两个option,level的可取值为DEBUG、INFO、WARNING、ERROR、CRITICAL、NOTSET,其中NOTSET表示所有级别的日志消息都要记录,包括用户定义级别;handlers的值是以逗号分隔的handler名字列表,这里出现的handler必须出现在[handlers]这个section中,并且相应的handler必须在配置文件中有对应的section定义;
  • 对于非root logger来说,除了level和handlers这两个option之外,还需要一些额外的option,其中qualname是必须提供的option,它表示在logger层级中的名字,在应用代码中通过这个名字得到logger;propagate是可选的,其默认值为1,表示消息将会传递给高层次logger的handler,通常我们需要指定其值为0,这个可以看下面的例子;另外,对于非root logger的level如果设置为NOTSET,系统将会查找高层次的logger来决定此logger的有效level;
  • 定义handler的section中必须指定class和args这两个option,level和formatter为可选option;class表示用于创建handler的类名,args表示传递给class所指定的handler类初始化方法参数,它必须是一个元组(tuple)的形式,即便只有一个参数值也需要是一个元组的形式;level与logger中的level一样,而formatter指定的是该处理器所使用的格式器,这里指定的格式器名称必须出现在formatters这个section总,且在配置文件中必须要有这个formatter的section定义;如果不指定formatter则该handler将会以消息本身作为日志消息进行记录,而不添加额外的时间、日志器名称等信息;
  • 定义formatter的section中的option都是可选的,其中包括format用于指定格式字符串,默认为消息字符串本身;datefmt用于指定asctime的时间格式,默认为”%Y-%m-%d %H:%M:%S”;class用于指定格式器类名,默认为logging.Formatter;

每一个logger或者handler或者formatter都有一个key名字,以logger为例,首先需要在[loggers]配置中加上key名字代表了这个logger

然后用[loggers_xxxx]其中xxxx为key名来具体配置这个logger,在log02中我配置了level和一个handler名,当然你可以配置多个hander

根据这个handler名再去 [handlers]里面去找具体handler的配置,以此类推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
##############################################
[loggers]
keys=root, log02

[logger_root]
level=INFO
handlers=handler01

[logger_log02]
level=DEBUG
handler=handler02
qualname=log02
##############################################
[handlers]
keys=handler01,handler02

[handler_handler01]
class=FileHandler
level=INFO
formatter=form01
args=('../log/cv_parser_gm_server.log',"a")

[handler_handler02]
class=StreamHandler
level=NOTSET
formatter=form01
args=(sys.stdout,)
##############################################
[formatters]
keys=form01,form02

[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(process)d %(message)s
datefmt=[%Y-%m-%d %H:%M:%S]

[formatter_form02]
format=(message)s
##############################################
代码

该函数实际上是对configparser模块的封装,函数定义:该函数定义在logging.config模块下

1
logging.config.fileConfig(fname, defaults=None, disable_existing_loggers=True)

参数说明:

  • fname:表示配置文件的文件名或文件对象;
  • defaults:指定传给ConfigParser的默认值;
  • disable_existing_loggers:这是一个布尔值,默认值为True(为了向后兼容)表示禁用已经存在的logger,除非它们或它们的祖先明确的出现在日志配置中;如果该值为False,则对已存在的loggers保持启动状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import logging.config

# 读取日志配置文件内容
logging.config.fileConfig("logging.conf")

# 创建一个日志器logger
logger = logging.getLogger("simpleExample")

# 日志输出
logger.debug("debug message.")
logger.info("info message.")
logger.warning("warning message.")
logger.error("error message.")
logger.critical("critical message.")
输出
1
2
3
4
5
2018-05-06 12:29:24,849 - simpleExample - DEBUG - debug message.
2018-05-06 12:29:24,849 - simpleExample - INFO - info message.
2018-05-06 12:29:24,849 - simpleExample - WARNING - warning message.
2018-05-06 12:29:24,849 - simpleExample - ERROR - error message.
2018-05-06 12:29:24,849 - simpleExample - CRITICAL - critical message.

配置文件test.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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]
代码
1
2
3
4
5
6
7
8
9
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")

字典配置(推荐)

创建一个包含配置信息的dict,然后把它传递给dictConfig()函数

通过配置字典进行配置,使用 dictConfig() 函数读取配置信息

  • logging.FileHandler: 文件handle, 多线程下安全

  • logging.handlers.RotatingFileHandler: 轮循文件handle, 多线程下安全, 可以限制文件大小, 设置历史日志数量

  • concurrent_log_handler.ConcurrentRotatingFileHandler: 多进程多线程下安全, 可以限制文件大小, 设置历史日志数量

    1
    2
    pip install redis==4.2.2
    pip install concurrent-log-handler==0.9.20
logging_config.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env Python
# -- coding: utf-8 --

"""
@version: v0.1
@author: narutohyc
@file: logger_config.py
@Description: 日志配置字典 + 定义logger句柄供项目使用
Python日志库logging总结-可能是目前为止将logging库总结的最好的一篇文章:
https://www.jianshu.com/p/7b5e4752932e
@time: 2020/5/23 21:34
"""
import logging
import os
from enum import Enum, unique
from logging import config as logging_config
from os.path import join

# 获取日志目录
from basic_support.data_access.config.project_config import project_config

base_path = project_config.PROJECT_PATH
base_log_path = join(base_path, 'logs')

# 文件目录不存在时, 创建该目录
if not os.path.exists(base_log_path):
os.makedirs(base_log_path)


@unique
class LogLevel(Enum):
""" 日志等级枚举类 """
CRITICAL = '致命' # 严重错误,表明程序本身可能无法继续运行
ERROR = '错误' # 由于更严重的问题,该软件无法执行某些功能
WARNING = '警告' # 表明发生了意外情况,或表明在不久的将来出现了一些问题 (例如 '磁盘空间不足')。但是该软件仍在按预期工作
INFO = '普通' # 确认事情按预期工作
DEBUG = '详细' # 详细信息,通常仅在诊断问题时才有意义
NOTSET = '不设置' # 意指不设置,所以按照父logger级别来过滤日志


# 日志相关配置
LOGGING_CONFIG = {
'version': 1,
'loggers': { # 日志,暴露函数给应用程序,基于日志记录器和过滤器级别决定哪些日志有效
'': { # root logger
'level': LogLevel.INFO.name, # 日志等级
'handlers': ['console_handler', 'info_file_handler', 'error_file_handler'],
}
},
'handlers': { # 处理器, 将(日志记录器产生的)日志记录发送至合适的目的地
'console_handler': {
'level': LogLevel.INFO.name, # 控制台日志等级 和 最终等级=max(当前等级,loggers)
'formatter': 'info',
'class': 'logging.StreamHandler', # 日志类
'stream': 'ext://sys.stdout', # 日志流
},
'info_file_handler': {
'level': LogLevel.INFO.name, # 信息日志等级
'formatter': 'info',
'class': "concurrent_log_handler.ConcurrentRotatingFileHandler", # 多进程下多线程安全
'filename': os.path.join(base_log_path, 'info.log'), # 信息日志文件输出目录
'mode': 'a+', # 日志文件模型 a表示追加 w是覆盖写
'encoding': 'utf-8',
'backupCount': 4, # 4 = 自己+历史的3个
'maxBytes': 1024 * 1024 * 50, # 单个日志文件大小限制在 50MB内
'use_gzip': False,
},
'error_file_handler': {
'level': LogLevel.WARNING.name, # 错误日志等级
'formatter': 'error',
'class': 'logging.FileHandler',
# 'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(base_log_path, 'error.log'), # 错误日志文件输出目录
'mode': 'a+',
'encoding': 'utf-8'
}
},
'formatters': { # 格式化器, 指明了最终输出中日志记录的布局
'info': {
'format': '%(asctime)s %(module)s:%(lineno)d %(levelname)s: %(message)s', # 日志输出格式化
'datefmt': '%Y-%m-%d %H:%M:%S' # 日期格式化
},
'error': {
# 'format': '%(asctime)s-%(levelname)s-%(name)s-%(process)d::%(module)s|%(lineno)s:: %(message)s',
'format': '%(asctime)s %(module)s|%(lineno)s %(levelname)s|%(process)d: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'
},
},
}

# 获取日志实例
logging_config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)
使用
1
2
3
4
5
6
7
8
from comm.config.logger_config import logger

# 日志输出
logger.debug("debug message.")
logger.info("info message.")
logger.warning("warning message.")
logger.error("error message.")
logger.critical("critical message.")
输出
1
2
3
4
5
2020-05-24 10:29:30 text_augmentation_script DEBUG:  debug message.
2020-05-24 10:29:30 text_augmentation_script INFO: info message.
2020-05-24 10:29:30 text_augmentation_script WARNING: warning message.
2020-05-24 10:29:30 text_augmentation_script ERROR: error message.
2020-05-24 10:29:30 text_augmentation_script CRITICAL: critical message.

Handler 子类

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 实例将消息发送到队列,例如队列或多处理模块中实现的队列。

NullHandler 实例不会对错误消息执行任何操作。

NullHandlerStreamHandlerFileHandler 类在核心日志包中定义。其他处理程序在子模块 logging.handlers 中定义。(还有另一个子模块 logging.config,用于配置功能。)

日志文件按照时间划分或者按照大小划分

如果将日志保存在一个文件中,那么时间一长,或者日志一多,单个日志文件就会很大,既不利于备份,也不利于查看。我们会想到能不能按照时间或者大小对日志文件进行划分呢?答案肯定是可以的,并且还很简单,logging 考虑到了我们这个需求。

logging.handlers 文件中提供了 TimedRotatingFileHandlerRotatingFileHandler 类分别可以实现按时间和大小划分。打开这个 handles 文件,可以看到还有其他功能的 Handler 类,它们都继承自基类 BaseRotatingHandler

自定义 Logger

可能的问题

logging 库是线程安全的,但在多进程、多线程、多进程多线程环境中仍然还有值得考虑的问题,比如,如何将日志按照进程(或线程)划分为不同的日志文件,也即一个进程(或线程)对应一个文件。

可以使用多进程安全的日志类concurrent-log-handler