日志的价值在于事故复盘时三分钟内还原“谁、何时、干了什么、为何失败”,需以黑匣子标准设计:补全请求id等extra字段、固定formatter格式、禁用字符串拼接、用raise from保留异常链、区分warning与error语义、合理采样、关键路径留debug开关,并在写日志前自问是否足以定位代码行与上下文。

Python 日志本身不值钱,值钱的是它能不能在事故复盘时,让你三分钟内还原出「谁、什么时候、干了什么、为什么失败」——这取决于你写日志时有没有把 logging 当成“黑匣子”,而不是“print 的高级替代品”。
日志字段缺失:复盘时连时间线都拼不全
很多团队的日志只留 message 和默认 levelname,结果复盘时发现:错误发生在 2026-02-15T14:22:33,但没记录请求 ID、用户 ID、服务名、线程 ID。你根本没法把 A 服务的报错和 B 服务的超时串起来。
- 必须加
extra字段补关键上下文,比如logging.info("order processed", extra={"order_id": "ORD-789", "user_id": "U123"}) - 用
Formatter固定输出%(asctime)s %(name)s %(levelname)s %(threadName)s %(funcName)s %(message)s,别信默认格式 - 避免在 handler 里做字符串拼接(如
"user " + user_id + " failed"),会丢失结构化能力,后续无法被json.loads()或 ELK 解析
异常链断裂:raise from 没用,导致根因永远藏在底层
复盘时看到 ValueError: payment validation failed,但没人知道是数据库连接超时还是 Redis 返回空值——因为上层异常没带 from,原始 ConnectionResetError 被吞了。
- 所有业务层包装异常,必须用
raise BusinessError("xxx") from e,不能只写raise BusinessError("xxx") - 检查你的
logging.exception()是不是总在最外层 catch,它只打当前异常的 traceback,不递归展开__cause__ - 如果用 structlog 或 loguru,确认它们对
__cause__的支持是否开启(loguru 默认开,structlog 需配exception_formatter)
日志级别混乱:ERROR 堆成山,真正要命的反而被淹没
事故刚发生时,满屏 ERROR:连接池耗尽、HTTP 503、重试失败……但没人注意到那条被刷上去的 WARNING:cache_ttl reduced to 1s due to memory pressure——这才是雪崩起点。
立即学习“Python免费学习笔记(深入)”;
-
WARNING不是“提醒”,是「已降级但尚未中断」的信号;ERROR应严格限定为「本次请求不可恢复失败」 - 禁止在循环里打
logging.error(比如每秒 1000 次重试失败),改用采样:用logging.getLogger().setLevel(logging.WARNING)+ 条件计数器控制频次 - 给关键路径加
logging.debug,但生产环境默认关;用环境变量开关,比如if os.getenv("LOG_DEBUG") == "1": logging.debug(...)
最难的不是加字段或调级别,而是让每个开发在写 logging.info 前,问自己一句:如果这行日志是事故复盘里唯一线索,它够我定位到代码第几行、参数是什么、上下游状态如何?答案不够,就重写。










