Why: 为什么要使用 logging?
代码调试时常使用 print 函数输出一些中间变量的值或者相关信息,这样虽然简便,但是在程序调试完之后需要逐个删除或注释 print 语句,比较麻烦。
logging 模块能很好地解决这个问题,通过设置 severity level,容易控制在控制台打印的信息,也可以同时把日志信息输出到多个目的地,如控制台、日志文件、网络位置等。
How: 怎么使用 logging?
日志基本用法
默认
1 | #!/usr/local/bin/python |
默认情况下,日志将会被打印到屏幕上,日志级别为 WARNING (即只有日志级别等于或高于 WARNING 的日志信息才会输出),日志格式为 warning level:instance name:warning message
记录到文件
1 | import logging |
需要注意的是,配置应在代码文件开头设置,否则不会改变初始的设置 (即代码文件开头的原始设置)。
完善日志功能
想要更灵活地使用日志模块,就要了解它是如何工作的。
Logger, Handler, Formatter 和 Filter 是日志模块的几个基本概念,其工作原理也要从这四个基本概念说起。
- Logger 记录器,提供日志相关功能的调用接口
- Handler 处理器,将 (记录器产生的) 日志记录发送至合适的目的地。
- Filter 过滤器,提供更好的粒度控制,可决定输出哪些日志记录。
- Formatter 格式化器,指明最终输出中日志记录的格式
基本概念
Logger
Logger 记录器,其对象实例是日志记录功能的载体,如
1 | import logging |
注意:Logger 对象从不直接实例化,而是通过模块级的功能 logging.getLogger(name) 来创建 Logger 实例。调用 logging.getLogger(name) 功能时,如果传入的 name 参数值相同,则总是返回同一个 Logger 对象实例的引用。
如果没有显式进行创建,则默认创建一个 root logger,并应用默认的日志级别 (WARN)、默认的处理器 Handler (StreamHandler, 即将日志信息打印输出到标准输出上)、和默认的格式化器 Formatter (默认的格式即为第一个简单使用程序中输出的格式,即 warning level:instance name:warning message)
Handler
Handler 将日志信息发送到设定位置,可通过 Logger 对象的 addHandler() 方法为 Logger 对象添加 0 或多个 handler。日志的一种典型应用场景是:系统希望将所有的日志信息保存到 log 文件中,其中日志等级等于或高于 ERROR 的消息还要在屏幕标准输出上显示,日志等级为 CRITICAL 的还需要发送邮件通知;这种场景就需要 3 个独立的 handler 来实现需求,分别与指定的日志等级或日志位置做响应。
注意:为 Logger 配置的 handler 不能是 Handler 基类对象,而是 Handler 的子类对象。常用的 Handler 有 StreamHandler, FileHandler, 和 NullHandler。
Formatter
Formatter 用于设置日志输出的格式,与 Logger/Handler 不同,它可以直接初始化对象,即 *formatter=logging.Formatter(fmt=None, datefmt=None)*。创建时分别传入两个参数来修改日志格式和时间格式,默认的日志格式为 %(asctime)s - %(levelname)s - %(message)s, 默认的时间格式为 %Y-%m-%d %H:%M:%S.
Filter
Filter 可用于 Logger 对象或 Handler 对象,用于提供比日志等级更加复杂的日志过滤方式。默认的 filter 只允许在指定 logger 层级下的日志消息通过过滤。
举个栗子,若把 filter 设置为 filter=logging.Filter(‘A.B’), 则 logger ‘A.B’, ‘A.B.C’, ‘A.B.C.D’, ‘A.B.D’ 产生的日志信息可以通过过滤,但 ‘A.BB’, ‘B.A.B’ 均不行。若以空字符串初始化 filter ,则所有的日志消息都可以通过过滤。
Filter 在日志功能配置中是非必须的,只在对日志消息过滤需求比较复杂时配置使用即可。
日志产生流程
日志产生的流程逻辑参考图
日志模块的使用
日志模块使用的关键是“日志的配置”。配置好之后,只需调用 logger.INFO(), logger.ERROR() 等方法即可创建日志内容。
配置日志模块有三种方法:
- 在代码中显式创建 loggers, handlers, formatters 甚至 filters,并调用这几个对象中的各个配置函数来完成日志配置
- 将配置信息写到配置文件中,然后读取配置文件信息来完成日志配置
- 将配置信息写到一个字典 dict 中,然后读取这个配置字典来完成日志配置
代码配置和使用
通过代码配置日志模块胜在方便简单,但不推荐在大型项目中使用,因为修改配置就需要修改代码。
这种方法可帮助我们理解日志模块的工作原理,因此用作案例。
1 | import logging |
这个 logger.log 文件的写入是追加的,不是重新写入的形式。如这个例子中, logger.log 的文件内容是 (前三行是之前的测试内容):
1 | INFO:root:info |
文件配置和使用
通过配置文件配置日志模块时,配置文件通常使用 .ini 格式,日志模块需要调用 fileConfig ,即 logging.config.fileConfig(‘logging_config.ini’) ,然后 logger 的使用方法同上。
1 | import logging |
其中,logging_config.ini 文件内容为
1 | [loggers] |
通过配置文件配置日志模块,逻辑与代码中的配置一样,也是把 logger, handler 和 formatter 定义好,然后组装到一起。只是 ini 配置与代码配置的语法不通,可参考上例做相应修改。
字典配置和使用
基于 Dict 对象配置日志模块在 Python 中应用广泛,很多 Django 或 Flask 项目都采用这种方式。以下是一个使用样例,可参考用于修改。
1 | import logging |
*日志的严重等级
Log Level 如下,
1 | CRITICAL: 50 |
等级包括 NOTSET, DEBUG, INFO, WARNING, ERROR, CRITICAL, 严重程度依次递增。
Which: 查阅
示例
1 | # -*- coding: utf-8 -*- |
格式化输出日志
1 | # 格式化输出 |
记录异常信息
当使用logging模块记录异常信息时,不需要传入该异常对象,直接调用 logger.error() 或 logger.exception() 就可以将当前异常记录下来。
1 | # 记录异常信息 |
修改日志消息的格式
Formatter 日志格式
Formatter 对象定义了 log 信息的结构和内容,构造时需要带两个参数:
- 一个是格式化的模板 fmt ,默认会包含最基本的 level 和 message 信息
- 一个是格式化的时间样式 datefmt ,默认为 2003-07-08 16:49:45,896 (%Y-%m-%d %H:%M:%S)
fmt 中允许使用的变量可参考下表
| Value | Meaning |
|---|---|
| %(name)s | Logger 的名字 |
| %(levelno)s | 数字形式的日志级别 |
| %(levelname)s | 文本形式的日志级别 |
| %(message)s | 用户输出的消息 |
| %(created)f | 当前时间,用 UNIX 标准的表示时间的浮点数表示 |
| %(relativeCreated)d | 输出日志信息时的,自 Logger 创建以来的毫秒数 |
| %(asctime)s | 字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒 |
| %(pathname)s | 调用日志输出函数的模块的完整路径名,可能没有 |
| %(filename)s | 调用日志输出函数的模块的文件名 |
| %(module)s | 调用日志输出函数的模块名,filename 的名字部分 |
| %(funcName)s | 调用日志输出函数的函数名 |
| %(lineno)d | 调用日志输出函数的语句所在代码行 |
| %(thread)d | 线程 ID ,可能没有 |
| %(threadName)s | 线程名,可能没有 |
| %(process)d | 进程 ID ,可能没有 |
| %(processName)s | 进程名,可能没有 |
跌过的坑不要再爬一遍
logging 全局设一个就够了,否则会重复输出
使用同名 logger 会拿到同一实例,这样可以实现跨模块调用同样的 logger 来记录日志;另外也可以通过日志名称来区分同一程序的不同模块。
参考链接
logging - Logging facility for Python
time.strftime(format[, t])
Python 中 Logging 模块使用方法
Python日志输出——logging模块
Python之日志处理(logging模块)
Python logging 模块使用指南
* Examples
Only One
目标场景:比如说两个模块,在其中一个模块中引用另外一个模块,并存在某一日志文件中
test.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
29from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
logging.basicConfig(level=logging.INFO)
import os
if os.path.exists('logger.log'):
os.remove('logger.log')
logger = logging.getLogger('youruser')
ch2 = logging.FileHandler('logger.log')
ch2.setLevel(logging.NOTSET)
formatter = logging.Formatter('%(asctime)s %(name)s/%(levelname)s: %(message)s')
# formatter = logging.Formatter('%(created)f - %(name)s - %(levelno)s - %(message)s')
# formatter = logging.Formatter('%(relativeCreated)s - %(module)s/%(funcName)s | %(message)s')
ch2.setFormatter(formatter)
logger.addHandler(ch2)
logger.info('')
logger.info('2\n')
logger.info('\n3')
logger.info('\t4')
logger.warning('hello')
logger.warn('world')
logger.warn('main Test {}'.format(4))
from testa import funcA
funcA(logger)testa.py
1
2
3
4# import logging
def funcA(logger):
logger.warn('Test A')print
logger.log1
2
3
4
5
6
7
8
9
10
112020-04-15 04:53:45,247 youruser/INFO:
2020-04-15 04:53:45,247 youruser/INFO: 2
2020-04-15 04:53:45,248 youruser/INFO:
3
2020-04-15 04:53:45,248 youruser/INFO: 4
2020-04-15 04:53:45,248 youruser/WARNING: hello
2020-04-15 04:53:45,248 youruser/WARNING: world
2020-04-15 04:53:45,249 youruser/WARNING: main Test 4
2020-04-15 04:53:45,250 youruser/WARNING: Test A
Multiple Outputs
目标场景:想同时输出两个 logging 文件,一个存自定义设置的信息,一个用于输出 tensorflow 给出的日志信息
main
- testdouble.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
50from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
logging.basicConfig(level=logging.INFO)
import os
import shutil
for fn in ['tf1.log', 'tf2.log']:
if os.path.exists(fn):
os.remove(fn)
formatter = logging.Formatter('%(asctime)s %(name)s/%(levelname)s: %(message)s')
logger = logging.getLogger('double')
# ch2 = logging.FileHandler('tfdouble.log')
ch2 = logging.FileHandler('tf1.log')
ch2.setFormatter(formatter)
'''
# http://landcareweb.com/questions/26327/ru-he-jiang-tensorflowri-zhi-ji-lu-zhong-ding-xiang-dao-wen-jian
# get TF logger
log = logging.getLogger('tensorflow')
log.setLevel(logging.DEBUG)
# create formatter and add it to the handlers
#formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# create file handler which logs even debug messages
fh = logging.FileHandler('tensorflow.log')
fh.setLevel(logging.DEBUG)
#fh.setFormatter(formatter)
log.addHandler(fh)
'''
tflog = logging.getLogger('tensorflow')
tfh = logging.FileHandler('tf2.log')
tfh.setLevel(logging.DEBUG)
tfm = logging.Formatter(logging.BASIC_FORMAT, None)
tfh.setFormatter(tfm)
tflog.addHandler(tfh)
logger.addHandler(ch2)
logger.warn("hello")
logging.critical("critical")
from testa import funcA
from testb import funcB
funcA(logger)
funcB()
from testb import funcTF
funcTF()
import modules
- testa.py (same as prev)
1
2
3
4# import logging
def funcA(logger):
logger.warn('Test A') - testb.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14import logging
def funcB():
logging.warn("Warn, hello")
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
import tensorflow as tf
# 禁用 GPU
def funcTF():
# tf.logging.ERROR("tf._logging.ERROR") # 1.14.0
tf.compat.v1.logging.info("tf.logging.info")
tf.compat.v1.logging.error("tf.logging.error")
Results
- tf1.log
1
2
32020-04-16 04:29:34,398 double/WARNING: hello
2020-04-16 04:29:39,795 double/WARNING: Test A - tf2.log
1
2
3INFO:tensorflow:tf.logging.info
ERROR:tensorflow:tf.logging.error