print 是新手最爱的调试工具,但一到生产环境就成了灾难——没有时间戳、没法分级、没法按模块过滤、没法输出到文件……日志(logging)是任何”能长期运行的程序”的必备基础设施。Python 内置的 logging 模块功能极强,但学习曲线也陡(对新手不友好)。这一篇讲清楚:正确的日志姿势、常见的三种配置方式、生产环境的最佳实践。
一、为什么不能用 print
看这段”用 print 调试”的代码:
1 | def do_task(user_id): |
问题:
- 没有时间戳;
- 没有日志级别(哪些是错误,哪些是提示?);
- 关不掉(生产环境不想看这些还得改代码);
- 没法输出到文件;
- 多进程一起打时序全乱;
- 无法按模块过滤。
用 logging:
1 | import logging |
同样一行,但获得所有优点。
二、五个日志级别
1 | logger.debug("详细的调试信息") |
对应数字:DEBUG(10) < INFO(20) < WARNING(30) < ERROR(40) < CRITICAL(50)。
默认级别是 WARNING——所以直接 logger.info(...) 什么都不打印。要配置。
三、最简配置:basicConfig
新手一行代码搞定:
1 | import logging |
带时间戳:
1 | logging.basicConfig( |
basicConfig 只对 root logger 生效,且只能调一次。所以只用来在脚本入口做初始化,别在库里用。
四、getLogger:正确使用日志的姿势
每个模块用自己的 logger(用 __name__),而不是直接用 logging.info:
1 | # mymodule.py |
为什么:
- 日志里能看到”是哪个模块打的”;
- 可以按模块配置级别(例如
myapp.db打 DEBUG,其他 INFO); - 库不应该硬性配置日志——用户来配。
五、格式化占位符
1 | format = "%(asctime)s %(levelname)-8s [%(name)s] %(message)s" |
常用占位符:
| 占位符 | 含义 |
|---|---|
%(asctime)s |
时间戳 |
%(levelname)s |
级别名 |
%(name)s |
logger 名(模块名) |
%(message)s |
日志内容 |
%(filename)s |
源文件 |
%(lineno)d |
行号 |
%(funcName)s |
函数名 |
%(process)d |
进程 ID |
%(thread)d |
线程 ID |
六、Handler:日志送到哪里
Logger 只是”入口”,真正把日志写出去的是 Handler:
StreamHandler:终端;FileHandler:文件;RotatingFileHandler:按大小滚动;TimedRotatingFileHandler:按时间滚动;SysLogHandler:syslog;SMTPHandler:邮件(错误告警);HTTPHandler:HTTP 接口。
一个 logger 可以有多个 handler——同时输出到终端和文件很常见。
1 | import logging |
七、异常日志
有异常时用 logger.exception——自动附加完整 traceback:
1 | try: |
日志里会有类似:
1 | ERROR do_stuff 出错 |
比 logger.error(str(e)) 有用得多。
八、传参 vs 拼字符串
推荐用 %s + 参数而不是 f-string:
1 | # ✅ 推荐 |
原因:如果 debug 日志没开启,logger.debug(msg, arg) 的格式化根本不会执行;f-string 无论如何都会先格式化。虽然差别不大,但大量 debug 日志时能省 20-30% CPU。
九、字典配置
复杂配置用字典(dictConfig):
1 | import logging.config |
字典配置的好处:
- 全部信息一处写清;
- 可从 JSON/YAML 加载;
- 复杂多 logger 场景不容易乱。
十、logger 的层级
logger 名字用 . 分层:
myappmyapp.dbmyapp.db.query
配置 myapp 会自动作用于所有以 myapp. 开头的 logger(除非子 logger 显式覆盖)。
日志会沿着层级向上传播(propagate=True 默认),最终到 root。避免同一条日志被父子 logger 打印两次:给子 logger 设 propagate=False。
十一、生产环境最佳实践
- 入口配置一次,模块用
getLogger(__name__); - 格式化用 %s,不要 f-string;
- DEBUG 到文件,INFO 到终端;
- 文件按大小或时间滚动(RotatingFileHandler);
- 异常用
logger.exception; - 敏感信息不打日志(密码、令牌、身份证);
- 性能敏感的路径别打大量日志;
- 多进程用 QueueHandler(避免文件写入冲突);
- 接错误告警(SMTPHandler、Sentry、云平台);
- 加请求 id / trace id(用
LoggerAdapter或第三方 structlog)。
十二、第三方推荐
- structlog:结构化日志(每条日志是 JSON),日志聚合平台首选;
- loguru:极简 API,一行搞定日志;比原生 logging 好用得多;
- rich:让终端日志变彩色、更好看;
- Sentry / Datadog:错误监控。
loguru 的极简 API:
1 | from loguru import logger |
新项目非要用原生 logging,除非有历史包袱或严格要求。
十三、加自定义字段:LoggerAdapter
想在每条日志里带上 request_id、user_id 等业务字段:
1 | class RequestAdapter(logging.LoggerAdapter): |
十四、一个完整例子:Web 服务日志
1 | # main.py |
十五、常见陷阱
- 用
logging.info()而不是logger.info():前者用 root logger,配置不清晰。 - 用 f-string 传消息:级别不开也执行格式化。用
%s+ 参数。 - basicConfig 调多次:只有第一次生效。
- 多进程写同一文件:会乱序或丢日志。用 QueueHandler 或按进程分文件。
- DEBUG 日志上生产:文件涨飞,可能包含敏感信息。
disable_existing_loggers: True(默认!):会禁用已定义的 logger。dictConfig 里推荐设False。propagate=True+ 父子都有 handler:日志重复打。
十六、小结与延伸阅读
- 每个模块用
logger = logging.getLogger(__name__); - 五级:DEBUG < INFO < WARNING < ERROR < CRITICAL;
- 一个 logger 可以有多个 handler;
- 生产环境:console + rotating file + error file;
- 异常用
logger.exception; - 传参用
%s + 参数; - 复杂配置用
dictConfig; - 新项目推荐 loguru,更简洁。
延伸阅读:
- logging:https://docs.python.org/zh-cn/3/library/logging.html
- 日志 HOWTO:https://docs.python.org/zh-cn/3/howto/logging.html
- loguru:https://github.com/Delgan/loguru
- structlog:https://www.structlog.org/
到这里模块六(中级进阶)10 篇结束! 下一篇进入 模块七:实用工具与并发,从 HTTP 请求:requests 库入门 开始。