日志别再print了!深入对比Python三大日志方案
线上接口又 500 了,控制台里只剩一堆:
start
user_id 89321
ok
error
这种日志我一看就头疼。
不是不能 print,本地临时看一眼当然行。但代码一旦进服务、进定时任务、进容器,print 基本就开始添乱:没有级别、没有时间、没有模块名、不能滚动、不能按 trace 查,出了问题只能靠猜。
我一般先看这几个东西:
2026-06-26 10:21:33 INFO order.worker create_order user_id=89321
2026-06-26 10:21:34 ERROR pay.client request failed trace_id=7f91 cost=3021ms
能不能定位问题,不看日志写得多不多,看它有没有把关键现场留下来。
Python 里常见日志方案,我会分三类看:标准库 logging、第三方 loguru、结构化日志 structlog。
标准库 logging:丑,但稳
logging 这玩意儿第一眼不讨喜,配置啰嗦,写起来也没那么顺手。但它有个好处:不用装包,稳定,框架也认。
线上服务我不会这样写:
print("开始处理订单", order_id)
print("支付失败", err)
我会至少改成这样:
import logging
from logging.handlers import TimedRotatingFileHandlerdefbuild_logger():
logger = logging.getLogger("order-service")
logger.setLevel(logging.INFO)
logger.propagate = False
fmt = logging.Formatter(
"%(asctime)s %(levelname)s %(name)s "
"%(filename)s:%(lineno)d %(message)s"
)
file_handler = TimedRotatingFileHandler(
filename="logs/order.log",
when="midnight",
backupCount=14,
encoding="utf-8"
)
file_handler.setFormatter(fmt)
console_handler = logging.StreamHandler()
console_handler.setFormatter(fmt)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
log = build_logger()
defcreate_order(user_id, sku_id):
log.info("create order start user_id=%s sku_id=%s", user_id, sku_id)
try:
# 这里省略真实下单逻辑
raise TimeoutError("pay gateway timeout")
except TimeoutError:
log.exception("create order failed user_id=%s sku_id=%s", user_id, sku_id)
raise
这里我特别不喜欢两种写法。
一种是 log.error(str(e)),异常栈没了。另一种是字符串拼接:
log.info("user_id=" + str(user_id))
日志级别被过滤掉的时候,拼接已经发生了。量小没事,量一上来这种细节就开始恶心人。
logging 适合什么?适合项目不想引入太多依赖,或者公司里有统一采集规范。它不香,但不容易翻车。
loguru:写脚本是真舒服
如果是批处理脚本、数据清洗、爬虫任务、临时运维工具,我一般会用 loguru。
它省事。
from loguru import logger
import syslogger.remove()
logger.add(
sys.stdout,
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} {level} {file}:{line} {message}"
)
logger.add(
"logs/import_user_{time:YYYYMMDD}.log",
rotation="50 MB",
retention="10 days",
compression="zip",
encoding="utf-8",
enqueue=True
)
defimport_user(row):
user_no = row.get("user_no")
mobile = row.get("mobile")
ifnot user_no:
logger.warning("skip row, user_no empty row={}", row)
returnFalse
try:
# 假装这里写入数据库
logger.info("import user ok user_no={} mobile={}", user_no, mobile)
returnTrue
except Exception:
logger.exception("import user failed user_no={} row={}", user_no, row)
returnFalse
rotation、retention、compression 这些东西,标准库不是不能做,但写起来没这么顺。
enqueue=True 也要留意一下,多进程或者日志量比较大的脚本里,它能少一些写文件互相抢的问题。
但我不太建议一上来就在大项目里全盘换成 loguru。不是它不好,是很多框架、组件、老代码都还在用标准 logging。你要么做桥接,要么最后项目里两套日志风格混着跑,排查时很别扭。
structlog:给日志系统看的,不是给人慢慢读的
服务上了规模以后,日志不是打开文件一行行翻,而是进 ELK、Loki、Datadog 这类系统里查。
这时候普通文本日志就有点吃亏。
比如你想查:
trace_id = abc123
user_id = 89321
接口耗时 > 2000ms
如果日志是纯字符串,就得靠正则扒。字段多了之后,谁扒谁知道。
structlog 更适合这种场景,它会把日志打成结构化数据。
import logging
import sys
import structloglogging.basicConfig(
format="%(message)s",
stream=sys.stdout,
level=logging.INFO
)
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.JSONRenderer(ensure_ascii=False),
],
logger_factory=structlog.stdlib.LoggerFactory(),
)
log = structlog.get_logger("pay-api")
defcall_pay(trace_id, user_id, amount):
pay_log = log.bind(trace_id=trace_id, user_id=user_id)
pay_log.info("pay_request_start", amount=amount)
try:
# 这里模拟请求第三方支付
raise TimeoutError("remote timeout")
except TimeoutError as err:
pay_log.error(
"pay_request_failed",
amount=amount,
error=str(err),
retryable=True
)
raise
输出大概是这样:
{"trace_id":"abc123","user_id":89321,"amount":19900,"event":"pay_request_failed","level":"error","retryable":true}
这种日志人眼看没那么舒服,但机器很喜欢。
我以前查过一次支付超时,普通文本日志里搜 user_id 能搜出一片,trace 又断断续续。后来把关键接口改成结构化日志,按 trace_id 一过滤,请求从入口、库存、支付、回调整个链路都能串起来。
这类日志还有个细节:字段名别乱。
今天叫 userId,明天叫 uid,后天叫 user_id,采集系统里会乱成一锅粥。日志字段最好像数据库字段一样管。
怎么选
小脚本、一次性任务、导入导出工具,用 loguru,少写很多配置。
普通 Web 服务、公司内部项目、需要兼容各种框架,用标准库 logging。不好看,但够稳。
日志要进平台,要按 trace、user、order、cost 查询,用 structlog。别犹豫,纯文本后面一定会难受。
print 留在本地调试就行,别带到线上。
线上排障时,最怕的不是报错。
最怕的是它明明错了,但日志只告诉你一句:
error
这种日志,等于没写。