Python 调试与日志工程:从 print 到可观测性

Python 调试与日志工程完整指南:print 调试进阶、pdb/ipdb/breakpoint 交互调试、logging 架构(Logger/Handler/Formatter/Filter)、结构化日志(structlog/JSON)、异常追踪与 traceback、性能剖析、生产可观测性集成。

调试不是「出 bug 才做的事」,而是理解程序行为的基本方法。本文从最快的 print 技巧讲到生产级结构化日志,帮你建立一套完整的调试与可观测性工具箱。


目录

  1. print 调试的艺术
  2. pdb 交互式调试
  3. 异常与 traceback 深入
  4. logging 架构
  5. 结构化日志 structlog
  6. 日志配置实战
  7. 性能剖析 py-spy 与 cProfile
  8. 内存泄漏定位
  9. 生产可观测性集成
  10. 速查表与最佳实践

1. print 调试的艺术

print 是最快的调试手段,但要 print 得聪明:

# ❌ 只打值,看不出上下文
print(x)

# ✅ 用 f-string 带变量名
print(f"{x=}")        # Python 3.8+:输出 x=42

# ✅ 加位置标记
print(">>> 进入 parse_config, path =", path)

# ✅ 统一入口,一键关闭
DEBUG = os.environ.get("DEBUG") == "1"
def dbg(*args):
    if DEBUG:
        print(*args, file=sys.stderr)   # 打到 stderr,不污染 stdout 管道

# ✅ 用 pprint 看嵌套结构
from pprint import pprint
pprint(complex_config, indent=2, width=80)

要点:生产代码的 print 应该全部清掉或转成 logging.debug,别让调试输出污染线上日志。


2. pdb 交互式调试

# 3.7+ 直接内建 breakpoint()
def process(data):
    breakpoint()          # 进入 pdb 断点
    result = transform(data)
    return result

pdb 常用命令:

命令作用
n / next单步执行
s / step进入函数
c / continue运行到下一断点
l显示当前行上下文
p expr打印表达式
pp expr美观打印
u / d上/下调用栈
b file:line设置断点
w查看调用栈
r运行到函数返回
q退出

非交互式 / 事后调试:

# 崩溃后进入 pdb(事后分析)
python -m pdb my_script.py

# 只看结果不交互
python -c "import pdb; pdb.runcall(fn, *args)"

# ipdb 增强版(语法高亮 + tab 补全)
pip install ipdb

3. 异常与 traceback 深入

3.1 异常链

# 保留原始异常上下文
try:
    raw = read_file("config.json")
except OSError as e:
    raise ValueError("配置读取失败") from e   # from 保留因果

# __context__ / __cause__
# raise ... from e  → __cause__
# except 内 raise     → __context__

3.2 完整 traceback

import traceback

# 打印完整堆栈
try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()           # 简洁版
    traceback.print_exc(limit=3)    # 限制帧数
    tb = traceback.format_exc()     # 拿到字符串(存日志)

# 获取当前调用栈
stack = traceback.extract_stack()
print(stack)

# 从 traceback 对象提取信息
exc_type, exc_value, exc_tb = sys.exc_info()
for frame in traceback.extract_tb(exc_tb):
    print(frame.filename, frame.lineno, frame.name)

3.3 自定义异常

class AppError(Exception):
    """业务错误基类,附带机器可读 code"""
    def __init__(self, code: str, message: str):
        super().__init__(message)
        self.code = code
        self.message = message

class NotFoundError(AppError):
    pass

# 使用
def get_user(id):
    user = db.find(id)
    if user is None:
        raise NotFoundError("USER_NOT_FOUND", f"用户 {id} 不存在")
    return user

4. logging 架构

logging 的四大组件:Logger(入口)、Handler(输出)、Formatter(格式)、Filter(过滤)。

import logging

logger = logging.getLogger("myapp")   # 分层命名 myapp.sub

# 基本用法
logger.debug("调试信息")      # 默认不输出
logger.info("用户 %s 登录", user)   # %s 延迟格式化,比 f-string 省开销
logger.warning("磁盘空间不足 %d%%", pct)
logger.error("处理失败", exc_info=True)   # 带堆栈
logger.exception("处理失败")   # 只在 except 里用,自动带堆栈

Level 优先级:DEBUG < INFO < WARNING < ERROR < CRITICAL

# 简单配置(开发)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

5. 结构化日志 structlog

文本日志难解析,生产用结构化日志(每行一个 JSON):

# pip install structlog
import structlog

structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.JSONRenderer(),   # 输出 JSON 行
    ]
)

log = structlog.get_logger()

# 上下文绑定(请求级)
structlog.contextvars.bind_contextvars(request_id="req-123", user_id=42)
log.info("支付成功", amount=99.9, gateway="alipay")
# → {"request_id": "req-123", "user_id": 42, "event": "支付成功", "amount": 99.9, "gateway": "alipay", "timestamp": "...", "level": "info"}

JSON 日志的价值:可被 Loki/ELK/Datadog 直接索引,grep、聚合、告警全部可用。


6. 日志配置实战

# logging_config.py
import logging
import logging.config
import json

def setup_logging(level: str = "INFO", json_logs: bool = False) -> None:
    config = {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "text": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s"},
            "json": {"format": json.dumps({
                "time": "%(asctime)s", "level": "%(levelname)s",
                "logger": "%(name)s", "msg": "%(message)s"})},
        },
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "formatter": "json" if json_logs else "text",
            },
        },
        "root": {"level": level.upper(), "handlers": ["console"]},
        "loggers": {
            "uvicorn": {"level": "WARNING"},     # 降噪框架日志
            "urllib3": {"level": "WARNING"},
        },
    }
    logging.config.dictConfig(config)

文件轮转:

from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    "app.log", maxBytes=10 * 1024 * 1024,  # 10MB
    backupCount=5,                        # 保留 5 个
    encoding="utf-8",
)

7. 性能剖析 py-spy 与 cProfile

7.1 cProfile(离线)

python -m cProfile -o prof.out my_script.py
python -m pstats prof.out
# 代码内剖析
import cProfile, pstats
prof = cProfile.Profile()
prof.enable()
# ... 被测代码 ...
prof.disable()
stats = pstats.Stats(prof)
stats.sort_stats("cumulative").print_stats(20)   # 累计耗时 Top20

7.2 py-spy(线上不侵入)

pip install py-spy

# 附着到运行中的进程(无需重启、不改代码)
sudo py-spy top --pid 12345
sudo py-spy record --pid 12345 -o profile.svg --duration 30

区别:cProfile 需改代码且本身有开销;py-spy 用 ptrace 采样,可分析正在线上运行的进程,秒级定位 CPU 热点。

7.3 定位热点函数

# 手动打点测量
import time

def timed(fn):
    import functools
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        r = fn(*args, **kwargs)
        print(f"{fn.__name__}: {(time.perf_counter()-t0)*1000:.2f}ms")
        return r
    return wrapper

8. 内存泄漏定位

import tracemalloc

# 启动追踪
tracemalloc.start()

# 快照对比
snap1 = tracemalloc.take_snapshot()
# ... 运行被测代码 ...
snap2 = tracemalloc.take_snapshot()

for stat in snap2.compare_to(snap1, "lineno")[:10]:
    print(stat)   # 显示新增分配最大的 10 行

# 查看当前最大占用
top = tracemalloc.take_snapshot().statistics("lineno")
for stat in top[:5]:
    print(stat)

泄漏常见根因:全局缓存无限增长、循环引用未清理、对象被闭包/异常链持有。

# 配合弱引用
import weakref
cache = weakref.WeakValueDictionary()   # 对象不存活就自动清除

9. 生产可观测性集成

场景工具集成方式
日志采集Loki / ELKJSON 日志 → 采集 agent
指标Prometheusprometheus_client + Histogram
链路追踪OpenTelemetryopentelemetry-python 自动埋点
异常上报Sentrysentry-sdk 一行接入
# Prometheus 指标示例
from prometheus_client import Histogram, Counter, start_http_server

request_dur = Histogram("http_request_duration_seconds", "HTTP 耗时",
                        buckets=[0.1, 0.5, 1, 2.5, 5, 10])
errors = Counter("http_errors_total", "错误数", ["status"])

start_http_server(8000)

def handle(req):
    with request_dur.time():
        try:
            process(req)
        except Exception:
            errors.labels(status="500").inc()
            raise
# Sentry
import sentry_sdk
sentry_sdk.init(dsn="...", traces_sample_rate=0.2)
# 自动捕获未处理异常 + 性能追踪

10. 速查表与最佳实践

需求工具
快速看值print(f"{x=}")
交互断点breakpoint() / ipdb
事后分析python -m pdb script.py
详细堆栈traceback.print_exc()
规范日志logging + dictConfig
结构化日志structlog + JSON
CPU 热点cProfile / py-spy
内存泄漏tracemalloc / weakref
线上可观测Prometheus + OTel + Sentry

最佳实践:

  1. 调试用 print/breakpoint,但上线前清理或转成 logging。
  2. 生产日志结构化(JSON),别靠肉眼看文本。
  3. logger.exception 只在 except 块里用,自动带堆栈。
  4. 用 %s 占位而非 f-string,避免不必要格式化开销。
  5. 框架日志(uvicorn/urllib3)调高 level,降噪。
  6. 任何服务都要有指标 + 日志 + 追踪三件套。

一句话记忆:开发期靠 pdb + traceback 精准定位,生产期靠 JSON 日志 + 指标 + 追踪兜底;性能与内存问题分别用 py-spy 和 tracemalloc 下药。

延伸阅读

调试能力 = 理解工具 + 理解程序状态。当你能「像看监控仪表一样」看代码运行时,几乎所有问题都能在一个小时内定位。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. Python 微服务架构:从单体拆分到服务治理
  2. Python 网络爬虫与自动化:从 requests 到 Playwright
  3. Python 库与 API 设计:从包结构到向后兼容