Python 日志库 logging
Python 标准库中的 logging 模块提供了功能完备、可高度自定义的日志记录方案,适用于从简单脚本到复杂应用程序的各种场景。
许多 C/C++ 项目都依赖自行实现的简单日志库或成熟的第三方日志库(如
spdlog、log4cpp等),与之不同的是,Python 内置的logging模块已经可以满足绝大多数开发需求,各种语言的日志库使用逻辑具有很多共性。
基本使用
先讨论在简单脚本文件中的日志使用,不涉及 logger 以及复杂的日志配置逻辑。
极简示例
导入日志库之后,无需任何配置就可以直接使用
1 | import logging |
运行输出如下(默认日志等级为 WARNING,过滤了 DEBUG 和 INFO 信息)
1 | WARNING:root:This is a warning 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 | a = 'Look' |
但是它们其实并不一样,因为无论是否需要输出,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 | INFO:root:This is an info message. |
例如
1 | logging.basicConfig( |
输出形如
1 | [2025-07-29 22:39:26,743][INFO] This is an info message. |
还可以提供文件名和行号信息
1 | logging.basicConfig( |
输出形如
1 | [2025-07-29 22:37:10,438][INFO] test.py:12 |
但是单条日志习惯上不会换行,还可以像下面这样配置
1 | logging.basicConfig( |
输出形如
1 | 2026-02-07 16:00:03,595 [INFO] [root:tmp.py:test] This is an info message. |
常用的格式字段包括:
%(asctime)s:时间戳%(levelname)s:日志等级%(message)s:日志内容%(name)s:logger 名称%(filename)s/%(lineno)d:文件名 / 行号%(process)d/%(thread)d:进程ID / 线程ID(如果可用)
对于时间戳支持通过 datefmt 选项设置格式,例如可以删除不必要的毫秒信息
1 | logging.basicConfig( |
默认情况下,日志输出到控制台,可以通过 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 | logger1 = logging.getLogger("myapp") |
习惯上在每一个模块中使用不同的 logger,使用当前模块的名称(通过__name__获取)作为 logger 的名称。
1 | logger = logging.getLogger(__name__) |
logger 的命名体现层级结构:
- 名为
foo.bar的 logger 被视作名为foo的 logger 的子级; - 所有 logger 都是
rootlogger 的子级。
logger 的层级结构对应的是日志的自动传播机制:
- 子级 logger 的日志信息会自动向上传递给父级 logger 对应的 handler 进行处理,因此不需要对每一个 logger 配置 handler;
- 传递发生在子级 logger 的日志等级检查之后,不需要基于父级 logger 自身的日志等级进行判断;
- 无论子级 logger 是否有 handler,都不影响传递给父级 handler;
- 可以用额外的选项关闭向上传递过程。
如果使用当前模块名称作为对应的 logger 名称,就可以让 logger 的层级结构自动匹配模块和子模块的层级结构。
logger 的使用是非常自然的:
1 | import logging |
在前面的简单使用中,直接调用 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 | stream_handler = logging.StreamHandler() |
handler 支持的配置包括
setLevel:设置 handler 的日志等级;(默认为最低的NOTSET)setFormatter:绑定 formatter 对象,一个 handler 只能对应一个 formatter 对象;(见下文)
有一个特殊的空 handler,将其绑定到 logger 会丢弃对应的日志输出,可以用来关闭某些库的日志记录
1 | logging.getLogger('foo').addHandler(logging.NullHandler()) |
不建议在库的源码中指定日志系统的 handler,应该把 handler 的具体选择留给使用方,在库的源码中只使用以当前模块命名的 logger 发出日志即可。
formatter
formatter 用于设置消息的格式化细节,例如使用下面的代码构造 Formatter 对象
1 | formatter = logging.Formatter( |
这里主要涉及到 fmt,datefmt 和 style 三个参数,前两个分别对应日志的格式化和时间戳的格式,fmt 默认效果如下
1 | %(message)s |
datefmt 默认格式如下
1 | %Y-%m-%d %H:%M:%S |
最后一个选项 style 决定了 fmt 的占位符语法风格,包括默认的 % 和 { 、$,例如
1 | formatter = logging.Formatter( |
示例
下面是一个 logger 同时输出到控制台和文件的示例:
- 关于逻辑关系
- logger
- logger 绑定了两个 handler
- 每一个 handler 绑定了对应的 formatter
- 关于日志等级
- logger 的日志等级为
DEBUG - console_handler 的日志等级为 WARNING
- file_handler 的日志等级为
INFO(这里只是为了演示效果,实际日志文件的等级通常是最低的)
- logger 的日志等级为
1 | import logging |
此时向控制台输出
1 | [WARNING] Warning message |
向日志文件输出
1 | [2025-07-30 00:25:56]{__main__}[INFO] Info message |
补充
异常处理
考虑在异常触发时的日志记录
1 | try: |
输出内容如下,无法在日志中输出捕获的异常信息。
1 | ERROR:root:Exception occurred |
logging 提供了 exc_info 选项,可以获取当前的异常信息(只是获取信息,并不会实际影响程序执行)
1 | try: |
输出内容如下
1 | ERROR:root:Exception occurred |
这个功能仅在 except 块中有效,在其它位置获得的信息是 None。
logging 还提供了如下接口简化使用
1 | logging.exception(...) |
basicConfig
现在重新解释 logging.basicConfig 的底层原理:logging.info(...) 在记录日志之前会依次进行如下的初始化操作
- 创建 root logger
- 设置 root logger 的日志级别为 warning
- 为 root logger 添加 StreamHandler 类型的 handler,并设置对应的 formatter
注意:如果 root logger 已经含有 handler,logging.basicConfig 不会进行修改。
下面的一行代码
1 | import logging |
在使用 root logger 记录日志之前会自动调用 logging.basicConfig 进行必要的初始化,大致等价于
1 | import sys |
我们也可以手动执行 logging.basicConfig 并设置相关参数,当然这需要在使用日志之前。
需要强调的是,root logger 只有在调用 logging.warning 等接口,或者显式调用 logging.basicConfig 时才会进行上述初始化配置,添加输出到控制台的 handler。
考虑下面的例子
1 | import logging |
输出形如
1 | warning 1 |
解释一下结果:
- 开始时,包括 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 | import logging |
输出形如
1 | ERROR:root:info from root |
这里最后一个日志出现了两次,因为 test logger 配置了自己的 handler,但是因为 logging.error 方法自动触发了 logging.basicConfig 的调用,使得 root logger 也配置了默认的 handler,test logger 的日志也向上传播给了父级 logger 的 handler,导致日志重复生成。
彩色日志
虽然彩色日志看起来非常好用,但是在实际使用中并不容易,因为需要考虑对不同的终端以及文件输出进行适配,在控制台的色彩输出通常要加上特殊的 ANSI 转义序列,但是在文件中则会显示为乱码,有的终端环境甚至不支持 ANSI 转义序列,转义序列对于输出重定向操作也不友好。
这里提供一个简单的基于 ANSI 转义序列的实现,首先需要自定义 Formatter 类型,继承 logging.Formatter 并重写 format 方法
1 | class ColoredFormatter(logging.Formatter): |
然后将其绑定到控制台输出所对应的 handler 即可。
完整代码如下
1 | import logging |
这种实现比较简陋,更建议使用成熟的第三方库。