Python logging 日志模块配置

重要性

在代码数量少的情况下用 print() 来进行调试输出确实无伤大雅,但是当代码数量暴增,各种依赖嵌套错综复杂的情形下还用 print() 来调试那么纯粹是在折磨自己。这种情形下使用日志来进行记录就显得非常有必要。

基本使用

logging 日志功能输出分为几个等级:

  • NOTSET(0):不设置,会按照 parent logger 的级别来设置日志
  • DEBUG(10):细节信息,仅当诊断问题时适用。
  • INFO(20):确认程序按预期运行。
  • WARN/WARNING(30):表明有已经或即将发生的意外(例如:磁盘空间不足)。程序仍按预期进行。
  • ERROR(40):由于严重的问题,程序的某些功能已经不能正常执行
  • FATAL/CRITICAL(50):严重的错误,表明程序已不能继续执行

默认级别是 WARNING,意味着只会追踪该级别及以上的事件,除非更改日志配置。

一个简单的例子

1
2
3
4
5
6
7
import logging

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

# console:
# WARNING:root:Watch out!

更改日志等级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.WARNING)

# Log
logger.debug('Debugging')
logger.critical('Critical Something')
logger.error('Error Occurred')
logger.warning('Warning exists')
logger.info('Finished')

# console:
# Critical Something
# Error Occurred
# Warning exists

格式化输出信息

1
2
3
4
5
6
7
8
9
10
11
12
# ...

logging.basicConfig(level=logging.DEBUG,
filename='output.log',
datefmt='%Y/%m/%d %H:%M:%S',
format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s')
logger = logging.getLogger(__name__)

logger.info('This is a log info')

# console:
# 2018/06/03 14:43:26 - __main__ - INFO - 9 - demo3 - This is a log info

format= 参数

  • %(levelno)s:打印日志级别的数值。
  • %(levelname)s:打印日志级别的名称。
  • %(pathname)s:打印当前执行程序的路径。
  • %(filename)s:打印当前执行程序名。
  • %(funcName)s:打印日志的当前函数。
  • %(lineno)d:打印日志的当前行号。
  • %(asctime)s:打印日志的时间。
  • %(thread)d:打印线程 ID。
  • %(threadName)s:打印线程名称。
  • %(process)d:打印进程 ID。
  • %(processName)s:打印线程名称。
  • %(module)s:打印模块名称。
  • %(message)s:打印日志信息。

更多参数可以参考官方文档

字符串拼接

在日志输出的时候不建议使用字符串的 format()f-string 等形式进行拼接。

1
2
3
4
5
6
# ...

# bad
logging.debug('Hello {0}, {1}!'.format('World', 'Congratulations'))
# good
logging.debug('Hello %s, %s!', 'World', 'Congratulations')

异常处理时,不需要格式化字符串,可以直接打印。因为直接使用字符串格式化的方法将错误输出的话,是不会包含 Traceback 信息的,但如果我们加上 exc_info 参数或者直接使用 exception () 方法打印的话,那就会输出 Traceback 信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ...

try:
result = 5 / 0
except Exception as e:
# bad
logging.error('Error: %s', e)
# good
logging.error('Error', exc_info=True)
# good
logging.exception('Error')

# console:
# 2018-06-03 22:24:31,927 - root - ERROR - Error: division by zero
# 2018-06-03 22:24:31,927 - root - ERROR - Error
# Traceback (most recent call last):
# File "/private/var/books/aicodes/loggingtest/demo9.py", line 6, in <module>
# result = 5 / 0
# ZeroDivisionError: division by zero
# 2018-06-03 22:24:31,928 - root - ERROR - Error
# Traceback (most recent call last):
# File "/private/var/books/aicodes/loggingtest/demo9.py", line 6, in <module>
# result = 5 / 0
# ZeroDivisionError: division by zero

输出到外部文件

1
2
3
4
import logging

logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logging.debug('This message should go to the log file')

处理器(Handler

Handler 对象负责将适当的日志消息(基于日志消息的严重性)分派给处理器的指定目标。

1
2
3
4
5
6
7
8
9
10
import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)
handler = logging.FileHandler('output.log')
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.info('This is a log info')

这里没有再使用 basicConfig 全局配置,而是先声明了一个 Logger 对象,然后指定了其对应的 Handler 为 FileHandler 对象,然后 Handler 对象还单独指定了 Formatter 对象单独配置输出格式,最后给 Logger 对象添加对应的 Handler 即可。

更多其他的处理器可以参考官方文档

配置共享

logging 模块提供了父子模块共享配置的机制,会根据 Logger 的名称来自动加载父模块的配置,这样可以在不同模块之间共享相同的配置。

1
2
3
4
5
import logging

logger = logging.getLogger('main')

# ...
1
2
3
4
5
import logging

logger = logging.getLogger('main.core')

# ...

使用独立的配置文件

把配置写在单独的文件中是一种更加常见的方式,当运行程序时读取配置文件,这样更加灵活、方便管理。配置文件可以使用 JSONYAMLconf 等格式。

1
2
3
4
5
6
7
8
9
10
11
12
# ...

def setup_logging(default_path='config.yaml', default_level=logging.INFO):
path = default_path
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
config = yaml.load(f)
logging.config.dictConfig(config)
else:
logging.basicConfig(level=default_level)

# ...

一个比较不错的配置模板:

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
version: 1
disable_existing_loggers: true

formatters:
standard:
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
error:
format: "%(levelname)s <PID %(process)d:%(processName)s> %(name)s.%(funcName)s(): %(message)s"

handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: standard
stream: ext://sys.stdout

info_file_handler:
class: logging.handlers.RotatingFileHandler
level: INFO
formatter: standard
filename: /tmp/info.log
maxBytes: 10485760 # 10MB
backupCount: 20
encoding: utf8

error_file_handler:
class: logging.handlers.RotatingFileHandler
level: ERROR
formatter: error
filename: /tmp/errors.log
maxBytes: 10485760 # 10MB
backupCount: 20
encoding: utf8

debug_file_handler:
class: logging.handlers.RotatingFileHandler
level: DEBUG
formatter: standard
filename: /tmp/debug.log
maxBytes: 10485760 # 10MB
backupCount: 20
encoding: utf8

critical_file_handler:
class: logging.handlers.RotatingFileHandler
level: CRITICAL
formatter: standard
filename: /tmp/critical.log
maxBytes: 10485760 # 10MB
backupCount: 20
encoding: utf8

warn_file_handler:
class: logging.handlers.RotatingFileHandler
level: WARN
formatter: standard
filename: /tmp/warn.log
maxBytes: 10485760 # 10MB
backupCount: 20
encoding: utf8

root:
level: NOTSET
handlers: [console]
propogate: yes

loggers:
<module>:
level: INFO
handlers: [console, info_file_handler, error_file_handler, critical_file_handler, debug_file_handler, warn_file_handler]
propogate: no

<module.x>:
level: DEBUG
handlers: [info_file_handler, error_file_handler, critical_file_handler, debug_file_handler, warn_file_handler]
propogate: yes

参考