八点零五分,收银台打印了六行,没人知道哪一行才是重点。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 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。