日志的价值在于“出事时能还原现场”。多数项目的日志问题不是没打,而是打在了没法用的地方。

库与应用的职责要分开

  • 库 / 模块:只 logger = logging.getLogger(__name__),不要调用 basicConfig()、不要加 handler。
  • 应用入口:统一配置 handler、级别、格式。
# 库内部:什么都不配
import logging
logger = logging.getLogger(__name__)
logger.debug('cache hit', extra={'key': key})

# 应用入口:集中配置一次
import logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s %(levelname)-7s %(name)s %(message)s',
)

在库里加 handler 会导致重复输出(日志被同一个 handler 处理多次),而且使用方无法统一控制格式。

格式里必须有时间、级别、来源

2026-09-24 10:00:01 INFO    app.views 计数成功 slug=linux-swap views=4
  • 时间:带时区或至少统一为 UTC,否则跨时区排查会很痛苦。
  • 级别:固定宽度便于肉眼扫。
  • %(name)s:出问题的是哪个模块,一眼可见。
  • 结构化字段:关键变量(slug、用户、耗时)放 extra=,方便日后检索。

传播(propagate)与层级

app.views 的 logger 会把记录冒泡给 app、再到 root。所以给 app 设级别就能一次性控制整个子树:

logging.getLogger('app.views').setLevel(logging.INFO)   # 只调一个子模块
logging.getLogger('app').setLevel(logging.WARNING)       # 整个包

重复配置时常常是因为既加了 handler 又让记录冒泡,结果同一条日志打印两遍。

多进程 / 异步的三个坑

  1. 多进程写同一文件:可能出现交错或丢行。用 QueueHandler + 独立进程,或交给 journald。
  2. 异步里的同步 IO:在事件循环里写日志会阻塞。高频日志要批量或转异步。
  3. 异常要 logger.exception:它自动带上堆栈;logger.error(str(e)) 会丢掉最关键的那部分。
try:
    do_something()
except Exception:
    logger.exception('处理失败 slug=%s', slug)   # ✅ 带堆栈

一条简单判据:如果这条日志出现时你无法定位到具体请求与耗时,那它就没写好。