Python logging 实战:从 print 到生产级日志

从 print 调试到 logging 模块的完整入门,覆盖日志级别、格式化、Handler、文件轮转与生产环境最佳实践。

大多数 Python 入门教程都用 print 调试,但到了真实项目里,print 会带来很多问题:没有时间戳、没有级别区分、无法控制输出目标。logging 模块就是为了解决这些问题。

一、为什么不能只靠 print

# 问题 1:不知道什么时候发生的
print("开始处理")

# 问题 2:没有级别区分,调试信息和错误信息混在一起
print("用户数据:", user_data)
print("出错了!")

# 问题 3:无法关闭,上线后仍然会输出
# 问题 4:无法同时写入文件和终端

二、logging 最简单的用法

import logging

# 配置基础日志级别为 INFO,低于 INFO 的日志(如 DEBUG)将被忽略
logging.basicConfig(level=logging.INFO)

# 记录不同级别的日志信息
logging.info("程序启动")
logging.warning("配置文件缺失,使用默认值")
logging.error("数据库连接失败")

输出效果:

INFO:root:程序启动
WARNING:root:配置文件缺失,使用默认值
ERROR:root:数据库连接失败

三、日志级别

从低到高排列:

级别 用途 何时使用
DEBUG 详细调试信息 开发阶段排查问题
INFO 确认程序按预期运行 关键流程节点
WARNING 意外情况,但程序仍可运行 配置回退、非致命异常
ERROR 严重问题,功能受影响 请求失败、数据库异常
CRITICAL 程序可能无法继续运行 系统崩溃、核心服务不可用
# 记录调试信息,通常用于开发阶段
logging.debug("变量 x = %s", x)
# 记录常规运行信息
logging.info("用户 %s 登录成功", username)
# 记录警告信息,程序仍可继续运行
logging.warning("磁盘使用率 %d%%", usage_percent)
# 记录错误信息,部分功能可能受影响
logging.error("接口调用失败: %s", error_msg)
# 记录严重错误,程序可能即将崩溃
logging.critical("主进程异常退出")

实际经验

  • 生产环境通常只输出 WARNING 及以上
  • DEBUG 日志只在排查问题时临时开启
  • INFO 用于记录关键业务节点

四、自定义日志格式

import logging

# 配置日志级别、输出格式和时间格式
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s", # 自定义日志格式
    datefmt="%Y-%m-%d %H:%M:%S", # 自定义时间格式
)

# 记录一条 INFO 级别的日志
logging.info("用户登录")

输出:

2026-06-20 10:30:00 [INFO] root - 用户登录

常用格式占位符

占位符 含义
%(asctime)s 时间戳
%(levelname)s 日志级别
%(name)s Logger 名称
%(message)s 日志消息
%(filename)s 文件名
%(lineno)d 行号
%(funcName)s 函数名

五、使用 Logger 对象而不是 root

import logging

# 创建模块级别的 logger
logger = logging.getLogger(__name__)

def process_order(order_id):
    logger.info("开始处理订单 %s", order_id)
    try:
        # 业务逻辑
        logger.info("订单 %s 处理完成", order_id)
    except Exception as e:
        logger.error("订单 %s 处理失败: %s", order_id, e)
        raise

好处

  • __name__ 自动使用模块名作为 logger 名称
  • 可以按模块或功能区设置不同的日志级别
  • 日志输出中能清楚看到来源模块

六、Handler:日志输出到哪里

Handler 决定日志写到终端、文件还是网络。一个 logger 可以同时挂多个 handler。

import logging

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

# 输出到终端
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# 输出到文件
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)

# 统一格式
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s - %(message)s")
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

logger.addHandler(console_handler)
logger.addHandler(file_handler)

logger.info("这条消息会同时出现在终端和文件中")
logger.debug("这条消息只会出现在文件中")

七、日志文件轮转

日志文件不能无限增长,需要用 RotatingFileHandler 做轮转。

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

# 单个文件最大 5MB,最多保留 5 个备份
handler = RotatingFileHandler(
    "app.log",
    maxBytes=5 * 1024 * 1024,
    backupCount=5,
    encoding="utf-8",
)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logger.addHandler(handler)

还有按时间轮转的 TimedRotatingFileHandler

from logging.handlers import TimedRotatingFileHandler

# 每天轮转一次,保留 30 天
handler = TimedRotatingFileHandler(
    "app.log",
    when="midnight",
    interval=1,
    backupCount=30,
    encoding="utf-8",
)

八、字典配置:更灵活的方式

import logging.config

# 定义日志配置字典
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False, # 是否禁用已存在的 logger
    "formatters": {
        "standard": {
            "format": "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler", # 输出到控制台
            "level": "INFO",
            "formatter": "standard",
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler", # 输出到文件并支持轮转
            "filename": "app.log",
            "maxBytes": 5242880, # 5MB
            "backupCount": 5,
            "encoding": "utf-8",
            "level": "DEBUG",
            "formatter": "standard",
        },
    },
    "root": {
        "level": "DEBUG",
        "handlers": ["console", "file"], # 绑定 handler
    },
}

# 应用字典配置
logging.config.dictConfig(LOGGING)

这套字典配置不只适用于普通 Python 项目,在 Django 里也经常直接放到 settings.pyLOGGING 变量中。

九、实战建议

  • 不要在日志中打印敏感信息(密码、token、身份证号)
  • 日志消息要有足够的上下文(用户ID、订单号、请求路径)
  • 使用 %s 格式化而不是 f-string,避免不必要的字符串拼接开销
# 好:延迟格式化,只有实际输出时才拼接字符串
logger.info("用户 %s 发起订单 %s", user_id, order_id)

# 不好:即使日志级别不输出,f-string 也会执行拼接
logger.info(f"用户 {user_id} 发起订单 {order_id}")
  • 异常日志要带上 traceback:
try:
    risky_operation()
except Exception:
    logger.exception("操作失败")  # 自动记录完整堆栈

小结

printlogging 的关键跃迁:

  • 用级别区分日志重要性
  • 用 logger 对象代替 root logger
  • 用 handler 控制输出目标
  • 用文件轮转控制日志大小
  • 用字典配置统一管理

掌握这些后,你的代码就具备了生产级的可观测性。

相关阅读

  • 《Python 输入输出与异常》:先熟悉文件、withtry / except
  • 《Python 函数与模块》:先熟悉函数、模块与类型提示,让日志工具函数更清晰
  • 《Python 网络与并发》:服务端程序和并发任务尤其依赖可观测性