Python 日志库 logging
Python 标准库中的 logging
模块提供了功能完备、可高度自定义的日志记录方案,适用于从简单脚本到复杂应用程序的各种场景。
许多 C/C++ 项目都依赖自行实现的简单日志库或成熟的第三方日志库(如
spdlog
、log4cpp
等),与之不同的是,Python 内置的logging
模块已经可以满足绝大多数开发需求,各种语言的日志库使用逻辑具有很多共性。
基本使用
先讨论在简单脚本文件中的日志使用,不涉及 logger 以及复杂的日志配置逻辑。
极简示例
导入日志库之后,无需任何配置就可以直接使用 1
2
3
4
5
6
7import logging
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")
logging.critical("This is a critical message.")
运行输出如下(默认日志等级为 WARNING
,过滤了
DEBUG
和 INFO
信息) 1
2
3WARNING:root:This is a warning message.
ERROR:root:This is an error message.
CRITICAL:root:This is a critical message.
也可以将日志等级作为一个参数传递,调用logging.log
函数
1
logging.log(logging.WARNING, 'This is a warning message.')
日志信息可以直接支持字符串的经典格式化 1
logging.warning('%s before you %s', 'Look', 'leap!')
使用 f-string
是更现代的做法 1
2
3a = 'Look'
b = 'leap!'
logging.warning(f'{a} before you {b}')
但是它们其实并不一样,因为无论是否需要输出,f-string
都会完成字符串参数的构造,才会进入调用的日志函数,但是如果当前日志不需要输出,经典的格式化可能不会进行,尤其在字符串参数的构造开销很大的情况下,需要考虑这些细节。
日志级别
logging 内置的日志等级从高到低如下:
CRITICAL
(50)ERROR
(40)WARNING
(30)INFO
(20)DEBUG
(10)NOTSET
(0)
默认级别为 WARNING
,因此只有等于 WARNING
或者更高等级的日志会被输出。
注意:这里的致命错误/错误/警告仅仅是日志等级,并不附带任何其它效果(不同于
warnings.warn
或抛出异常),例如不会抛出错误,导致脚本执行中断等。
基本配置
可以在脚本开头使用 logging.basicConfig
进行基本配置。
注意:logging.basicConfig
是初始化配置,必须在第一次记录日志之前调用,否则在第一次记录日志时会自动生成一个默认配置,logging.basicConfig
命令不会对已经存在的配置进行修改,除非加上 fore=True
强制修改。
最常见的配置是通过 level
选项设置日志等级
1
logging.basicConfig(level=logging.INFO)
可以通过 format
选项定制输出格式,默认格式形如
1
2
3
4INFO:root:This is an info message.
WARNING:root:This is a warning message.
ERROR:root:This is an error message.
CRITICAL:root:This is a critical message.
例如 1
2
3
4logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s][%(levelname)s] %(message)s",
)
输出形如 1
2
3
4[2025-07-29 22:39:26,743][INFO] This is an info message.
[2025-07-29 22:39:26,743][WARNING] This is a warning message.
[2025-07-29 22:39:26,743][ERROR] This is an error message.
[2025-07-29 22:39:26,743][CRITICAL] This is a critical message.
还可以提供文件名和行号信息 1
2
3
4logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s][%(levelname)s] %(filename)s:%(lineno)d\n%(message)s",
)
输出形如 1
2
3
4
5
6
7
8[2025-07-29 22:37:10,438][INFO] test.py:12
This is an info message.
[2025-07-29 22:37:10,441][WARNING] test.py:13
This is a warning message.
[2025-07-29 22:37:10,441][ERROR] test.py:14
This is an error message.
[2025-07-29 22:37:10,441][CRITICAL] test.py:15
This is a critical message.
常用的格式字段包括:
%(asctime)s
:时间戳%(levelname)s
:日志等级%(message)s
:日志内容%(name)s
:logger 名称%(filename)s
/%(lineno)d
:文件名 / 行号%(process)d
/%(thread)d
:进程ID / 线程ID(如果可用)
对于时间戳支持通过 datefmt
选项设置格式,例如可以删除不必要的毫秒信息 1
2
3
4
5logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s][%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
默认情况下,日志输出到控制台,可以通过 filename
选项使其输出到指定的日志文件中,例如 1
logging.basicConfig(filename='app.log')
可以改变日志文件的写入模式(默认追加),也可以改变写入文件的编码方式,例如
1
logging.basicConfig(filename='app.log',filemode='w',encoding='utf-8')
如果希望同时输出到控制台和文件,就需要进行更复杂的配置了。
进阶使用
逻辑结构
前面的基本使用实际上只涉及到 logging 这个库提供的简化用法,真正的使用需要了解 logging 库的底层逻辑结构,如下图所示。
重点关注三个组件:
logger
(记录器):面向调用者,提供Logger.info
等调用接口,对应日志的基本数据生成,有自己的日志等级handler
(处理器):对应日志的一个输出渠道,例如控制台或文件,也有自己的日志等级,一个 logger 可以绑定多个 handlerformatter
(格式器):顾名思义,负责日志信息的格式化处理,一个 handler 可以指定一个 formatter 用来格式化消息
实际上还支持
filter
提供更精细的日志过滤规则,这里不做考虑。
这里涉及到多个日志等级,以如下语句为例 1
logger.info(...)
它能否打印日志取决于很多因素:
INFO
是否大于等于 logger 自身的日志等级;(否则丢弃)- 如果大于等于,消息被分配给 logger 绑定的所有 handler;(其实还会自动向上传播给父级 logger 的 handler)
- 每一个 handler 检查:
INFO
是否大于等于 handler 自身的日志等级;(否则丢弃) - 如果大于等于,输出日志(到控制台或文件等)
为什么要给 handler 单独设置日志等级?如果一个 logger 同时输出到控制台和文件,那么我们可能要求控制台的日志等级较高,否则信息太多造成干扰;但是要求文件的日志等级较低,获取更完整的信息,便于后期排除问题。
logging 实际上还准备一个兜底的机制,在处理一个日志等级大于等于
WARNING
的日志时,如果
- 当前 logger 以及所有父级 logger 都没有 handler,
- 没有调用过
basicConfig()
;
此时会使用一个名为 logging.lastResort
的 StreamHandler
进行输出,以确保重要的错误日志信息不会因为配置不当而丢失。
完整控制流如图
logger
使用如下语句创建一个具名的 logger 对象 1
logger = logging.getLogger("<name>")
无参数调用时会返回名为 root 的特殊根 logger 对象 1
root_logger = logging.getLogger()
logger
对象的管理使用了单例模式,使用相同名称获取的是同一个对象,这样就不需要在各个函数中频繁传递
logger 对象 1
2
3
4logger1 = logging.getLogger("myapp")
logger2 = logging.getLogger("myapp")
print(logger1 is logger2) # True
习惯上在每一个模块中使用不同的
logger,使用当前模块的名称(通过__name__
获取)作为 logger
的名称。 1
logger = logging.getLogger(__name__)
logger 的命名体现层级结构:
- 名为
foo.bar
的 logger 被视作名为foo
的 logger 的子级; - 所有 logger 都是
root
logger 的子级。
logger 的层级结构对应的是日志的自动传播机制:
- 子级 logger 的日志信息会自动向上传递给父级 logger 对应的 handler 进行处理,因此不需要对每一个 logger 配置 handler;
- 传递发生在子级 logger 的日志等级检查之后,不需要基于父级 logger 自身的日志等级进行判断;
- 无论子级 logger 是否有 handler,都不影响传递给父级 handler;
- 可以用额外的选项关闭向上传递过程。
如果使用当前模块名称作为对应的 logger 名称,就可以让 logger 的层级结构自动匹配模块和子模块的层级结构。
logger 的使用是非常自然的: 1
2
3
4
5
6
7
8
9
10import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.debug("This is a debug message.")
logger.info("This is an info message.")
logger.warning("This is a warning message.")
logger.error("This is an error message.")
logger.critical("This is a critical message.")
在前面的简单使用中,直接调用 logging.info(...)
函数就相当于使用 root logger 对象调用它的 Logger.info(...)
方法。
logger 的常见配置包括:
Logger.setLevel
:设置 logger 的日志等级;(默认为WARNING
)Logger.addHandler
/Logger.removeHandler
:添加和移除 handler,一个 logger 可以对应零个,一个或多个 handler,可以直接通过handlers
属性查看;(见下文)
handler
通常不需要使用 Handler 基类,而是应该使用由此派生的 handler 类型:
StreamHandler
:流处理器,将消息发送到控制台(标准错误流)FileHandler
:文件处理器,将消息发送到文件RotatingFileHandler
:文件处理器,在文件达到指定大小后,启用新文件存储日志TimedRotatingFileHandler
:文件处理器,以特定的时间间隔轮换日志文件
这里主要关注前两个类型。
handler 的创建例如 1
2stream_handler = logging.StreamHandler()
file_handler = logging.FileHandler(filename="test.log")
handler 支持的配置包括
setLevel
:设置 handler 的日志等级;(默认为最低的NOTSET
)setFormatter
:绑定 formatter 对象,一个 handler 只能对应一个 formatter 对象;(见下文)
有一个特殊的空 handler,将其绑定到 logger
会丢弃对应的日志输出,可以用来关闭某些库的日志记录 1
logging.getLogger('foo').addHandler(logging.NullHandler())
不建议在库的源码中指定日志系统的 handler,应该把 handler 的具体选择留给使用方,在库的源码中只使用以当前模块命名的 logger 发出日志即可。
formatter
formatter 用于设置消息的格式化细节,例如使用下面的代码构造 Formatter
对象 1
2
3
4
5formatter = logging.Formatter(
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
style='%'
)
这里主要涉及到 fmt
,datefmt
和
style
三个参数,前两个分别对应日志的格式化和时间戳的格式,fmt
默认效果如下 1
%(message)s
datefmt
默认格式如下 1
%Y-%m-%d %H:%M:%S
最后一个选项 style
决定了 fmt
的占位符语法风格,包括默认的 %
和 {
、$
,例如 1
2
3
4
5
6
7
8
9formatter = logging.Formatter(
fmt='%(asctime)s - %(levelname)s - %(message)s',
style='%'
)
formatter = logging.Formatter(
fmt='{asctime} - {levelname:^8} - {message}',
style='{'
)
示例
下面是一个 logger 同时输出到控制台和文件的示例:
- 关于逻辑关系
- logger
- logger 绑定了两个 handler
- 每一个 handler 绑定了对应的 formatter
- 关于日志等级
- logger 的日志等级为
DEBUG
- console_handler 的日志等级为 WARNING
- file_handler 的日志等级为
INFO
(这里只是为了演示效果,实际日志文件的等级通常是最低的)
- logger 的日志等级为
1 | import logging |
此时向控制台输出 1
2
3[WARNING] Warning message
[ERROR] Error message
[CRITICAL] Critical message
向日志文件输出 1
2
3
4[2025-07-30 00:25:56]{__main__}[INFO] Info message
[2025-07-30 00:25:56]{__main__}[WARNING] Warning message
[2025-07-30 00:25:56]{__main__}[ERROR] Error message
[2025-07-30 00:25:56]{__main__}[CRITICAL] Critical message
补充
异常处理
考虑在异常触发时的日志记录 1
2
3
4try:
1 / 0
except ZeroDivisionError:
logging.error("Exception occurred")
输出内容如下,无法在日志中输出捕获的异常信息。 1
ERROR:root:Exception occurred
logging 提供了 exc_info
选项,可以获取当前的异常信息(只是获取信息,并不会实际影响程序执行)
1
2
3
4try:
1 / 0
except ZeroDivisionError:
logging.error("Exception occurred", exc_info=True)
输出内容如下 1
2
3
4
5
6ERROR:root:Exception occurred
Traceback (most recent call last):
File "xxx/test.py", line 13, in <module>
1 / 0
~~^~~
ZeroDivisionError: division by zero
这个功能仅在 except
块中有效,在其它位置获得的信息是
None。
logging 还提供了如下接口简化使用 1
2logging.exception(...)
# = logging.error(..., exc_info=True)
basicConfig
现在重新解释 logging.basicConfig
的底层原理:logging.info(...)
在记录日志之前会依次进行如下的初始化操作
- 创建 root logger
- 设置 root logger 的日志级别为 warning
- 为 root logger 添加 StreamHandler 类型的 handler,并设置对应的 formatter
注意:如果 root logger 已经含有
handler,logging.basicConfig
不会进行修改。
下面的一行代码 1
2
3
4import logging
# logging.basicConfig()
logging.info("hello")
在使用 root logger 记录日志之前会自动调用
logging.basicConfig
进行必要的初始化,大致等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import sys
import logging
from logging import StreamHandler
from logging import Formatter
root_logger = logging.getLogger()
root_logger.setLevel(logging.WARNING)
handler = StreamHandler(sys.stderr)
root_logger.addHandler(handler)
formatter = Formatter("%(levelname)s:%(name)s:%(message)s")
handler.setFormatter(formatter)
root_logger.info("hello")
我们也可以手动执行 logging.basicConfig
并设置相关参数,当然这需要在使用日志之前。
需要强调的是,root logger 只有在调用 logging.warning
等接口,或者显式调用 logging.basicConfig
时才会进行上述初始化配置,添加输出到控制台的 handler。
考虑下面的例子 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import logging
logger = logging.getLogger(__name__)
root_logger = logging.getLogger()
logger.info("info 1")
logger.warning("warning 1")
logger.error("error 1")
print(logger.handlers)
print(root_logger.handlers)
logging.error("error")
print(logger.handlers)
print(root_logger.handlers)
logger.setLevel(logging.ERROR)
logger.info("info 2")
logger.warning("warning 2")
logger.error("error 2")
输出形如 1
2
3
4
5
6
7
8warning 1
error 1
[]
[]
ERROR:root:error
[]
[<StreamHandler <stderr> (NOTSET)>]
ERROR:__main__:error 2
解释一下结果:
- 开始时,包括 root 在内的两个 logger 都没有设置 handler;
- 成功输出
warning 1
和error 1
是利用了 lastResort handler 的兜底机制,info 1
则因为等级太低被放弃; - 调用
logging.error
函数触发了logging.basicConfig
,此时 root logger 添加了 handler; - 成功输出
error 2
是因为 logger 设置了日志等级 ERROR,logger 没有 handler,但是 root 有 handler。注意这里的名称是__main__
而不是root
。
非常不建议混用简单方式和标准方式,因为很容易出现配置错误,例如考虑下面的例子
1
2
3
4
5
6
7
8import logging
logging.error("info from root")
logger = logging.getLogger("test")
logger.addHandler(logging.StreamHandler())
logger.error("info from test")
输出形如 1
2
3ERROR:root:info from root
info from test
ERROR:test:info from test
这里最后一个日志出现了两次,因为 test logger 配置了自己的
handler,但是因为 logging.error
方法自动触发了
logging.basicConfig
的调用,使得 root logger 也配置了默认的
handler,test logger 的日志也向上传播给了父级 logger 的
handler,导致日志重复生成。
彩色日志
虽然彩色日志看起来非常好用,但是在实际使用中并不容易,因为需要考虑对不同的终端以及文件输出进行适配,在控制台的色彩输出通常要加上特殊的 ANSI 转义序列,但是在文件中则会显示为乱码,有的终端环境甚至不支持 ANSI 转义序列,转义序列对于输出重定向操作也不友好。
这里提供一个简单的基于 ANSI 转义序列的实现,首先需要自定义 Formatter
类型,继承 logging.Formatter
并重写 format
方法 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class ColoredFormatter(logging.Formatter):
COLOR_MAP = {
"DEBUG": "\033[36m", # 青色
"INFO": "\033[32m", # 绿色
"WARNING": "\033[33m", # 黄色
"ERROR": "\033[31m", # 红色
"CRITICAL": "\033[41m", # 红色背景
"RESET": "\033[0m", # 重置
}
def format(self, record):
color = ColoredFormatter.COLOR_MAP.get(
record.levelname, ColoredFormatter.COLOR_MAP["RESET"]
)
reset = ColoredFormatter.COLOR_MAP["RESET"]
original_msg = super().format(record)
return f"{color}{original_msg}{reset}"
然后将其绑定到控制台输出所对应的 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
39
40
41
42
43
44
45
46import logging
class ColoredFormatter(logging.Formatter):
COLOR_MAP = {
"DEBUG": "\033[36m", # 青色
"INFO": "\033[32m", # 绿色
"WARNING": "\033[33m", # 黄色
"ERROR": "\033[31m", # 红色
"CRITICAL": "\033[41m", # 红色背景
"RESET": "\033[0m", # 重置
}
def format(self, record):
color = ColoredFormatter.COLOR_MAP.get(
record.levelname, ColoredFormatter.COLOR_MAP["RESET"]
)
reset = ColoredFormatter.COLOR_MAP["RESET"]
original_msg = super().format(record)
return f"{color}{original_msg}{reset}"
logger = logging.getLogger("test")
logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_handler.setFormatter(ColoredFormatter(fmt="[%(levelname)s] %(message)s"))
file_handler = logging.FileHandler("app.log", mode="w", encoding="utf-8")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(
logging.Formatter(
fmt="[%(asctime)s]{%(name)s}[%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.debug("Debug message")
logger.info("Info message")
logger.warning("Warning message")
logger.error("Error message")
logger.critical("Critical message")
这种实现比较简陋,更建议使用成熟的第三方库。