第 35 课 · 进阶 · 19 分钟

Python 日志

用级别、处理器和格式记录程序发生了什么。

日志级别处理器格式化器

八点零五分,收银台打印了六行,没人知道哪一行才是重点。print 只说有事发生,logging 会说何时、何地、多严重。级别到底过滤掉了什么?

print 适合快速检查,但 logging 多了严重级别、输出目的地和统一格式。日志器把记录交给处理器,每个处理器都可以格式化或过滤。小而明确的配置比全局日志设置更容易测试。 你还会添加上下文、过滤敏感记录,并安全捕获 traceback。

把记录交给处理器

日志器会发出达到或高于自身级别的记录,处理器决定它们去哪里。这里用 StringIO 捕获输出,让示例自成一体,只在最后打印到标准输出。

import logging
from io import StringIO

stream = StringIO()
handler = logging.StreamHandler(stream)
logger = logging.getLogger("demo")
logger.handlers[:] = [handler]
logger.setLevel(logging.INFO)
logger.propagate = False
logger.info("started")
print(stream.getvalue().strip())

带目的地的日志器

用级别过滤

级别是阈值。设为 WARNING 时,info 记录会被忽略,warning 和 error 记录会通过。这样开发时可以保留诊断细节,生产环境只显示重要消息。

import logging
from io import StringIO

stream = StringIO()
handler = logging.StreamHandler(stream)
logger = logging.getLogger("demo")
logger.handlers[:] = [handler]
logger.setLevel(logging.WARNING)
logger.propagate = False
logger.info("hidden")
logger.warning("shown")
print(stream.getvalue().strip())

只显示警告及以上

格式化消息

格式化器把记录变成文本。格式字符串里可以使用 levelname、name 和 message 等字段。把格式化器附加到拥有输出目的地的处理器上。

import logging
from io import StringIO

stream = StringIO()
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
logger = logging.getLogger("demo")
logger.handlers[:] = [handler]
logger.setLevel(logging.INFO)
logger.propagate = False
logger.warning("check")
print(stream.getvalue().strip())

可预测的单行格式

用 LoggerAdapter 添加上下文

LoggerAdapter 会给每条记录加上同一份上下文,例如请求 ID。只有把该字段写进 formatter,值才会真正显示。适配器只是小包装,不会创建第二套 logger 层级。

import io
import logging

stream = io.StringIO()
logger = logging.getLogger("gift.request")
logger.handlers[:] = [logging.StreamHandler(stream)]
handler = logger.handlers[0]
handler.setFormatter(logging.Formatter("%(request_id)s %(message)s"))
logger.setLevel(logging.INFO)
logger.propagate = False

adapter = logging.LoggerAdapter(logger, {"request_id": "r-17"})
adapter.info("loaded %s", "menu")
print(stream.getvalue().strip())

一条记录,一个请求 ID

让记录在离开前被过滤

Filter 可以在任何 handler 格式化之前拒绝记录。它适合做策略检查,但不能代替“绝不把秘密写进日志调用”。这里的过滤器丢掉提到 password 的消息,安全记录仍然能到达 handler。

import io
import logging

class NoSecrets(logging.Filter):
    def filter(self, record):
        return "password" not in record.getMessage().lower()

stream = io.StringIO()
logger = logging.getLogger("gift.safe")
logger.handlers[:] = [logging.StreamHandler(stream)]
logger.handlers[0].addFilter(NoSecrets())
logger.setLevel(logging.INFO)
logger.propagate = False

logger.info("user %s", "maya")
logger.info("password=%s", "hunter2")
print(repr(stream.getvalue()))

敏感记录提前丢掉

不写到 stderr 也能捕获异常

logger.exception() 会记录消息和当前 traceback。测试时把 handler 指向 StringIO,traceback 就不会漏到 stderr。生产环境里,别把 traceback 直接返回给用户,记录前也要清理敏感值。

import io
import logging

stream = io.StringIO()
logger = logging.getLogger("gift.checkout")
logger.handlers[:] = [logging.StreamHandler(stream)]
logger.setLevel(logging.INFO)
logger.propagate = False

try:
    1 / 0
except ZeroDivisionError:
    logger.exception("checkout failed")

text = stream.getvalue()
print(text.splitlines()[0])
print("ZeroDivisionError" in text)

把 traceback 收进受控流

本课要点

  • 日志器发出记录,处理器决定记录去哪里。
  • 日志器级别是记录的阈值。
  • 处理器可以有自己的级别和格式化器。
  • propagate=False 可以避免演示日志器同时使用根日志器。
  • StringIO 让日志示例确定且容易测试。
  • 适配器补上下文,过滤器执行策略,exception() 捕获 traceback。

常见错误

  • 还没有配置任何处理器就调用 logging.info,然后疑惑输出去了哪里。
  • 在库模块里配置根日志器。
  • 忘记记录通过日志器后,还可能被更低级别的处理器过滤。
  • 以为 logging 默认静默:记录去哪里由 handler 和 propagate 决定。
轮到你

创建 INFO 级别的日志器,附加 StringIO 处理器,记录一条消息,再打印捕获的文本。

动手试试

改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← CSV 文件HTTP 请求 →