日志的价值在于“出事时能还原现场”。多数项目的日志问题不是没打,而是打在了没法用的地方。
库与应用的职责要分开
- 库 / 模块:只
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 又让记录冒泡,结果同一条日志打印两遍。
多进程 / 异步的三个坑
- 多进程写同一文件:可能出现交错或丢行。用
QueueHandler+ 独立进程,或交给 journald。 - 异步里的同步 IO:在事件循环里写日志会阻塞。高频日志要批量或转异步。
- 异常要
logger.exception:它自动带上堆栈;logger.error(str(e))会丢掉最关键的那部分。
try:
do_something()
except Exception:
logger.exception('处理失败 slug=%s', slug) # ✅ 带堆栈
一条简单判据:如果这条日志出现时你无法定位到具体请求与耗时,那它就没写好。
