日志是排查线上问题时最重要的信息来源之一。Python 标准库自带 logging 模块,功能足够支撑大部分脚本、Web 服务和后台任务。很多项目一开始直接使用 print,等任务跑到服务器上才发现无法区分级别、没有时间、没有模块名,也不好重定向到文件。

本文介绍一套适合中小型 Python 项目的 logging 配置方式,包括控制台输出、文件轮转、模块 logger、异常堆栈和常见坑。

logging 的基本概念

logging 主要包含四个对象:

  • Logger:代码中使用的日志入口。
  • Handler:决定日志输出到哪里,例如控制台、文件、网络。
  • Formatter:决定日志格式。
  • Level:决定哪些日志会被输出,例如 DEBUG、INFO、WARNING、ERROR。

在业务代码中不要直接操作 root logger,推荐为每个模块创建自己的 logger:

1
2
3
4
5
6
import logging

logger = logging.getLogger(__name__)

def run():
logger.info("task started")

__name__ 会使用当前模块路径作为 logger 名称,排查时能看出日志来自哪个模块。

最小可用配置

对于简单脚本,可以使用 basicConfig

1
2
3
4
5
6
7
8
9
import logging

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

logger = logging.getLogger(__name__)
logger.info("hello logging")

输出类似:

1
2026-06-05 09:00:00,123 INFO [__main__] hello logging

这比 print 多了时间、级别和模块名。脚本排查问题时,这些字段非常关键。

同时输出到控制台和文件

服务端程序通常希望控制台输出给容器日志系统,同时保留本地文件。可以手动配置 handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path

def setup_logging(log_dir: str = "logs", level: int = logging.INFO) -> None:
Path(log_dir).mkdir(parents=True, exist_ok=True)

formatter = logging.Formatter(
"%(asctime)s %(levelname)s [%(name)s:%(lineno)d] %(message)s"
)

console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.setLevel(level)

file_handler = RotatingFileHandler(
filename=f"{log_dir}/app.log",
maxBytes=20 * 1024 * 1024,
backupCount=5,
encoding="utf-8",
)
file_handler.setFormatter(formatter)
file_handler.setLevel(level)

root = logging.getLogger()
root.setLevel(level)
root.handlers.clear()
root.addHandler(console_handler)
root.addHandler(file_handler)

RotatingFileHandler 会在文件超过 20MB 后自动轮转,并保留 5 个历史文件,避免日志无限增长占满磁盘。

在入口文件中调用:

1
2
3
4
def main():
setup_logging()
logger = logging.getLogger(__name__)
logger.info("application started")

记录异常堆栈

捕获异常时,不要只记录 str(e),否则缺少堆栈信息。推荐使用 logger.exception

1
2
3
4
try:
sync_data()
except Exception:
logger.exception("sync data failed")

logger.exception 只能在 except 块中使用,它会自动附加当前异常堆栈。如果不在 except 块中,可以使用:

1
logger.error("request failed", exc_info=True)

堆栈信息对定位文件行号、调用链和真实异常类型非常重要。

日志级别如何选择

常见级别可以这样划分:

  • DEBUG:开发调试信息,例如变量值、分支选择、SQL 参数。
  • INFO:正常业务流程,例如服务启动、任务完成、关键状态变化。
  • WARNING:非预期但还能继续运行,例如重试、配置缺省、缓存失效。
  • ERROR:当前操作失败,需要人工关注或上层处理。
  • CRITICAL:程序可能无法继续运行,例如核心依赖不可用。

生产环境通常使用 INFO 或 WARNING。DEBUG 日志可能包含大量细节,既影响性能,也可能暴露敏感信息。

在模块中使用 logger

业务模块只需要获取 logger,不应该重复配置 handler:

1
2
3
4
5
6
import logging

logger = logging.getLogger(__name__)

def create_order(order_id: str) -> None:
logger.info("create order started order_id=%s", order_id)

注意这里使用 %s 参数化,而不是 f-string:

1
logger.info("create order started order_id=%s", order_id)

当日志级别未开启时,logging 可以避免不必要的字符串格式化开销。对于普通项目影响不大,但这是更稳妥的习惯。

避免重复日志

如果日志出现重复输出,通常是因为多次调用配置函数,或者子 logger 和 root logger 都绑定了 handler。入口配置时可以使用:

1
root.handlers.clear()

另外,库代码不应该主动调用 basicConfig,否则会影响宿主程序的日志行为。库只创建 logger,把配置权交给应用入口。

结构化字段

如果暂时没有接入 JSON 日志系统,也可以用固定格式记录关键字段:

1
2
3
4
5
6
logger.info(
"payment callback received order_id=%s user_id=%s status=%s",
order_id,
user_id,
status,
)

字段名保持一致,后续使用 grep、日志平台或正则解析都更方便。不要只写“处理失败”“请求异常”这类没有上下文的日志。

小结

Python 的 logging 标准库已经能满足大多数项目需求。推荐在程序入口统一配置 handler 和 formatter,在业务模块中通过 logging.getLogger(__name__) 获取 logger。异常使用 logger.exception,文件输出使用轮转,生产环境控制 DEBUG 日志。只要日志格式稳定、字段充分,排查问题的效率会明显提升。