Python技术迷

日志别再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 TimedRotatingFileHandler

defbuild_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 sys

logger.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 structlog

logging.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

这种日志,等于没写。