Python 标准库中的 logging 模块提供了功能完备、可高度自定义的日志记录方案,适用于从简单脚本到复杂应用程序的各种场景。

许多 C/C++ 项目都依赖自行实现的简单日志库或成熟的第三方日志库(如 spdloglog4cpp 等),与之不同的是,Python 内置的 logging 模块已经可以满足绝大多数开发需求,各种语言的日志库使用逻辑具有很多共性。

基本使用

先讨论在简单脚本文件中的日志使用,不涉及 logger 以及复杂的日志配置逻辑。

极简示例

导入日志库之后,无需任何配置就可以直接使用

1
2
3
4
5
6
7
import 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,过滤了 DEBUGINFO 信息)

1
2
3
WARNING: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
3
a = '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
4
INFO: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
4
logging.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
4
logging.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
5
logging.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 可以绑定多个 handler
  • formatter(格式器):顾名思义,负责日志信息的格式化处理,一个 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
4
logger1 = 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
10
import 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
2
stream_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
5
formatter = logging.Formatter(
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
style='%'
)

这里主要涉及到 fmtdatefmtstyle 三个参数,前两个分别对应日志的格式化和时间戳的格式,fmt 默认效果如下

1
%(message)s

datefmt 默认格式如下

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

最后一个选项 style 决定了 fmt 的占位符语法风格,包括默认的 %{$,例如

1
2
3
4
5
6
7
8
9
formatter = 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(这里只是为了演示效果,实际日志文件的等级通常是最低的)
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
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

console_fmt = logging.Formatter(fmt="[%(levelname)s] %(message)s")

file_fmt = logging.Formatter(
fmt="[%(asctime)s]{%(name)s}[%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)

console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_handler.setFormatter(console_fmt)

file_handler = logging.FileHandler("app.log", mode="w", encoding="utf-8")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(file_fmt)

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

此时向控制台输出

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
4
try:
1 / 0
except ZeroDivisionError:
logging.error("Exception occurred")

输出内容如下,无法在日志中输出捕获的异常信息。

1
ERROR:root:Exception occurred

logging 提供了 exc_info 选项,可以获取当前的异常信息(只是获取信息,并不会实际影响程序执行)

1
2
3
4
try:
1 / 0
except ZeroDivisionError:
logging.error("Exception occurred", exc_info=True)

输出内容如下

1
2
3
4
5
6
ERROR: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
2
logging.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
4
import logging

# logging.basicConfig()
logging.info("hello")

在使用 root logger 记录日志之前会自动调用 logging.basicConfig 进行必要的初始化,大致等价于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 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
24
import 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
8
warning 1
error 1
[]
[]
ERROR:root:error
[]
[<StreamHandler <stderr> (NOTSET)>]
ERROR:__main__:error 2

解释一下结果:

  • 开始时,包括 root 在内的两个 logger 都没有设置 handler;
  • 成功输出 warning 1error 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
8
import logging

logging.error("info from root")

logger = logging.getLogger("test")
logger.addHandler(logging.StreamHandler())

logger.error("info from test")

输出形如

1
2
3
ERROR: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
17
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}"

然后将其绑定到控制台输出所对应的 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
46
import 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")

这种实现比较简陋,更建议使用成熟的第三方库。