Python技术迷

日志看不懂?这 6 个 Python 库来得太及时了!

线上报错最怕这种:

ERROR pay callback failed
Traceback (most recent call last):
  File "worker.py", line 48, in run
    handle_order(msg)
KeyError: 'order_id'

一眼看过去,好像知道错了。

再看两分钟,又感觉啥也没说。

KeyError 是谁传的?哪条消息?哪个用户?重试了几次?请求链路从哪来的?这些东西日志里没有,基本就只能靠猜。面试里问“你平时怎么处理日志”,我一般不太喜欢听“打印日志、定位问题”这种话,太空了。

Python 里这 6 个库,真能把日志从“看个热闹”变成“能追现场”。

第一个,logging。

别嫌它老。线上项目里,我第一反应还是先看有没有把标准库用明白。

很多人写日志是这样的:

print("order error", order_id)

开发环境看着挺顺手,一上服务器就麻烦了。没有级别,没有时间,没有文件,没有模块名,出了问题 grep 都不好 grep。

我一般会先把最基础的格式补上:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s"
)

log = logging.getLogger("pay.callback")

defhandle_callback(payload):
    order_id = payload.get("order_id")
    log.info("callback received order_id=%s channel=%s",
             order_id, payload.get("channel"))

ifnot order_id:
        log.warning("callback missing order_id payload=%s", payload)
return

别上来就整很复杂。先让日志能按时间、级别、模块搜出来。很多小项目,做到这一步已经够用了。

第二个,loguru。

这个库适合那种脚本多、定时任务多、又不想写一堆 logging 配置的场景。

比如每天跑一个订单对账脚本,凌晨出错,第二天早上一看日志文件 600M,头都大了。这个时候我会直接加滚动和保留时间:

from loguru import logger

logger.add(
"logs/reconcile_{time:YYYYMMDD}.log",
    rotation="100 MB",
    retention="14 days",
    encoding="utf-8"
)

defcheck_bill(row):
try:
        fee = int(row["fee"])
        logger.info("bill ok order_no={} fee={}", row["order_no"], fee)
except Exception:
        logger.exception("bill parse failed row={}", row)

logger.exception() 这个我挺喜欢,异常栈和上下文一起打出来。排查时不用再问“当时传的参数是什么”。

第三个,traceback。

面试里有人说“我会捕获异常”,然后代码写成:

except Exception as e:
    print(e)

这地方我一般就不太信了。

只打印 e,很多时候只剩一句 invalid literal for int(),出错文件和行号都没了。traceback 至少要会用:

import traceback

defrun_job(job_id, fn):
try:
        fn()
except Exception:
        err = traceback.format_exc()
        logging.error("job failed job_id=%s\n%s", job_id, err)

这东西不花哨,但救命。特别是一些老项目,日志系统没接好,先把完整异常栈打出来,比什么都强。

第四个,rich。

rich 我一般不会直接往生产日志里塞太多颜色,生产环境看文件,颜色没意义。

但本地调试、写命令行工具,它很舒服。尤其是那种嵌套 JSON、接口返回特别长的东西。

from rich.console import Console
from rich.traceback import install

install(show_locals=True)

console = Console()

defdebug_response(resp):
    console.rule("gateway response")
    console.print_json(data=resp)

show_locals=True 要注意,可能会把变量值打出来。里面如果有 token、手机号、身份证号,这就不是调试,是埋雷。这个习惯要有:日志越详细,越要防泄漏。

第五个,structlog。

微服务里日志最烦的是一条链路散在好几个服务里。

支付服务打一条,订单服务打一条,库存服务打一条。没有 trace_id,你只能靠时间凑。时间一密集,基本就废了。

structlog 比较适合结构化日志:

import structlog

log = structlog.get_logger()

defcreate_order(req):
    trace_id = req.headers.get("X-Trace-Id", "-")
    user_id = req.json.get("user_id")

    log.info(
"order_create_start",
        trace_id=trace_id,
        user_id=user_id,
        sku=req.json.get("sku")
    )

这种日志进 ELK、Loki、ClickHouse 都好查。

我更关心的是字段稳定。别今天叫 traceId,明天叫 trace_id,后天又叫 tid。日志字段一乱,后面告警和查询都得跟着擦屁股。

第六个,python-json-logger。

如果项目还是标准 logging,但又想输出 JSON 日志,可以用它。

比如容器里跑服务,日志直接打到 stdout,再由采集器收走。这种场景下,JSON 比普通文本更好处理。

import logging
from pythonjsonlogger import jsonlogger

handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter(
"%(asctime)s %(levelname)s %(name)s %(message)s %(trace_id)s"
)
handler.setFormatter(formatter)

log = logging.getLogger("api")
log.addHandler(handler)
log.setLevel(logging.INFO)

log.info(
"user_login_failed",
    extra={"trace_id": "t-20260424-001"}
)

这里有个坑,extra 里的字段别和 logging 自带字段冲突。比如你硬塞一个 message,可能就把自己绕晕了。

真到了线上,我看日志一般不是从代码开始看。

先看 ERROR 有没有集中爆发,再看有没有 trace_id,再看异常栈,再看关键入参。如果日志里只有一句“处理失败”,那基本等于没打。

日志不是写给机器看的,也不是写给领导看的。

是写给半夜被叫醒的那个倒霉开发看的。

能让他少猜一分钟,这日志就值了。