Python logging模块教程:从基础日志到生产环境日志管理完整指南

📝 900 字 · ☕ 3 分钟阅读

为什么你还在用print调试?

如果你还在代码里塞满 print(f"变量X的值是:{x}") 来排查问题,这篇文章就是为你写的。

print三宗罪:

  • 上线前要一行行删掉,删完又怕下次还要加回来
  • 程序崩溃了,最后几行print根本没输出到屏幕上
  • 日志和正常业务输出混在一起,grep 都救不了你

Python标准库自带的 logging 模块,能让你用5行代码替换掉满屏的print,而且支持:分级输出、写入文件、自动轮转、多模块统一管理。本教程从零开始,带你一步步掌握。

第一步:5行代码告别print

最简版本——把日志同时输出到控制台和文件:

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

# 像print一样简单,但功能强100倍
logger.info("程序启动")
logger.warning("配置文件未找到,使用默认值")
logger.error("数据库连接失败", exc_info=True)

效果:

  • 控制台实时看到日志
  • 文件 app.log 持久保存,程序崩溃也能追溯
  • 时间戳自动带上,不用手动加
  • exc_info=True 自动打印完整异常堆栈

第二步:理解日志等级——什么时候用什么

logging 有5个标准等级(从低到高):

等级 数值 使用场景
DEBUG 10 开发调试,如”进入函数X,参数Y=42″
INFO 20 业务流程,如”用户登录成功”、”订单创建”
WARNING 30 可恢复的问题,如”重试第3次”、”磁盘空间不足80%”
ERROR 40 功能异常,如”支付失败”、”API超时”
CRITICAL 50 系统级灾难,如”数据库挂了”、”内存耗尽”

关键规则:设置 level=logging.INFO 后,低于INFO的DEBUG日志不会输出。生产环境设为INFO或WARNING,开发环境设为DEBUG。

第三步:日志写进文件,按天/按大小自动切割

直接写到一个文件会越来越大,生产环境必须用 RotatingHandler 实现自动轮转。

方案A:按文件大小切割(RotatingFileHandler)

import logging
from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    'app.log',           # 日志文件
    maxBytes=10*1024*1024,  # 10MB一个文件
    backupCount=5,       # 保留最近5个备份
    encoding='utf-8'
)

handler.setFormatter(logging.Formatter(
    '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
))

logger = logging.getLogger('myapp')
logger.setLevel(logging.INFO)
logger.addHandler(handler)

# 写够10MB后自动生成 app.log.1, app.log.2 ...
for i in range(10000):
    logger.info(f"第{i}条日志:这是一条测试日志数据" * 100)

方案B:按时间切割(TimedRotatingFileHandler)

from logging.handlers import TimedRotatingFileHandler

handler = TimedRotatingFileHandler(
    'app.log',
    when='midnight',     # 每天午夜切割
    interval=1,          # 每1个when单位
    backupCount=30,      # 保留最近30天
    encoding='utf-8'
)
handler.suffix = "%Y-%m-%d"  # 备份文件命名:app.log.2026-06-09

其他 when 选项: 'S'秒、'M'分钟、'H'小时、'D'天、'W0''W6'每周某天。

第四步:多模块项目的日志架构

当项目有多个文件时,每个模块用 logging.getLogger(name) 获取自己的logger,日志会按模块名形成层级。

# main.py —— 主入口,只在这里配置一次basicConfig
import logging
import logging.config
from crawler import fetch_data
from parser import parse_html

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    handlers=[logging.FileHandler('crawler.log', encoding='utf-8'),
              logging.StreamHandler()]
)

logger = logging.getLogger(__name__)
logger.info("爬虫启动")
fetch_data()
logger.info("爬虫完成")

# —— crawler.py ——
import logging
logger = logging.getLogger(__name__)  # logger名为 'crawler'

def fetch_data():
    logger.info("开始抓取页面...")
    # ... HTTP请求 ...
    logger.warning("第3次重试中...")
    return data

# —— parser.py ——
import logging
logger = logging.getLogger(__name__)  # logger名为 'parser'

def parse_html(html):
    logger.debug("解析HTML,长度 %d", len(html))  # level=INFO时不输出
    # ...

控制台输出效果:

2026-06-09 10:00:01 [INFO] __main__: 爬虫启动
2026-06-09 10:00:02 [INFO] crawler: 开始抓取页面...
2026-06-09 10:00:05 [WARNING] crawler: 第3次重试中...
2026-06-09 10:00:06 [INFO] __main__: 爬虫完成

模块名自动出现在日志里,一眼看出问题出在哪个文件。

第五步:用dictConfig统一管理配置

当项目变大,把日志配置抽到字典或YAML里,一处修改全局生效:

import logging.config

LOGGING_CONFIG = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'standard': {
            'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
        },
        'detailed': {
            'format': '%(asctime)s [%(levelname)s] %(name)s '
                      '(%(filename)s:%(lineno)d): %(message)s'
        }
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'INFO',
            'formatter': 'standard',
        },
        'file': {
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'level': 'DEBUG',
            'formatter': 'detailed',
            'filename': 'logs/app.log',
            'when': 'midnight',
            'backupCount': 30,
            'encoding': 'utf-8'
        },
        'error_file': {
            'class': 'logging.FileHandler',
            'level': 'ERROR',
            'formatter': 'detailed',
            'filename': 'logs/error.log',
            'encoding': 'utf-8'
        }
    },
    'root': {
        'level': 'DEBUG',
        'handlers': ['console', 'file', 'error_file']
    }
}

logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)

logger.info("配置已加载")       # → 控制台 + app.log
logger.error("磁盘写入失败!")   # → 控制台 + app.log + error.log

这个配置做到了:INFO以上推控制台,所有等级写app.log并按天切割,ERROR以上额外写一份到error.log便于快速定位严重问题。

第六步:生产环境实战——Flask/Django 日志集成

Flask 日志配置

from flask import Flask
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)

# 配置Flask自带的logger
handler = RotatingFileHandler('flask_app.log', maxBytes=5*1024*1024, 
                               backupCount=10, encoding='utf-8')
handler.setFormatter(logging.Formatter(
    '%(asctime)s [%(levelname)s] %(message)s'
))
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)

@app.route('/')
def index():
    app.logger.info("首页被访问,IP: %s", request.remote_addr)
    return "Hello World"

# 同时配置werkzeug(Flask内置服务器)的日志级别
logging.getLogger('werkzeug').setLevel(logging.WARNING)

Django 日志配置

settings.py 中添加:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': '/var/log/django/app.log',
            'when': 'midnight',
            'backupCount': 30,
            'formatter': 'verbose',
        },
    },
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {message}',
            'style': '{',
        },
    },
    'root': {
        'handlers': ['file'],
        'level': 'INFO',
    },
}

第七步:用Filter实现精细化日志过滤

上面讲的是按等级过滤(level),但实际场景往往更复杂。比如:只想看某个用户的日志、只想记录耗时超过1秒的数据库查询。这时就需要自定义Filter。

import logging
import time

class SlowQueryFilter(logging.Filter):
    """只记录慢查询(耗时>1秒)"""
    def filter(self, record):
        # record.args 是 logger.info("... %s", value) 中的 value
        duration = getattr(record, 'duration', 0)
        if duration > 1.0:
            record.msg = f"[慢查询 {duration:.1f}s] {record.msg}"
            return True
        return False  # False = 丢弃这条日志

# 使用
logger = logging.getLogger('db')
slow_handler = logging.FileHandler('slow_queries.log', encoding='utf-8')
slow_handler.addFilter(SlowQueryFilter())
logger.addHandler(slow_handler)

# 模拟
def query(sql):
    start = time.time()
    # ... 执行SQL ...
    elapsed = time.time() - start
    logger.info(sql, extra={'duration': elapsed})

query("SELECT * FROM users")  # 0.1s → 不记录
query("SELECT * FROM orders WHERE ...")  # 2.3s → 记录到slow_queries.log

Filter的强大之处在于:你可以基于任何条件(record内容、上下文变量、时间、用户ID等)动态决定是否记录以及如何记录。

第八步:在日志中携带业务上下文

定位问题时常需要知道”当时是哪个用户在操作”。用 extra 参数把业务上下文注入日志:

import logging

# 自定义formatter,支持%(user_id)s这样的自定义字段
formatter = logging.Formatter(
    '%(asctime)s [%(levelname)s] user=%(user_id)s: %(message)s'
)

handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger = logging.getLogger('api')
logger.addHandler(handler)

# 方案一:每次手动传
logger.info("订单创建成功", extra={'user_id': 'u_8842'})

# 方案二(推荐):用LoggerAdapter自动注入
class UserAdapter(logging.LoggerAdapter):
    def process(self, msg, kwargs):
        kwargs.setdefault('extra', {})
        kwargs['extra']['user_id'] = self.extra.get('user_id', 'anonymous')
        return msg, kwargs

adapted = UserAdapter(logger, {'user_id': 'u_8842'})
adapted.info("支付完成")
adapted.warning("余额不足")
# 输出:2026-06-09 10:00:01 [INFO] user=u_8842: 支付完成
#       2026-06-09 10:00:01 [WARNING] user=u_8842: 余额不足

使用 LoggerAdapter,一次传入用户上下文,后续所有日志自动带上用户ID,比每次手动写 extra= 干净得多。

第九步:异常日志最佳实践

捕获异常时记录日志,有四种写法——但只有一种是对的:

import logging
logger = logging.getLogger(__name__)

# ❌ 错误写法1:丢失了堆栈信息
try:
    1 / 0
except ZeroDivisionError as e:
    logger.error(f"出错:{e}")  # 只有"division by zero",不知道哪行

# ❌ 错误写法2:日志等级用错了
try:
    open('/nofile')
except FileNotFoundError:
    logger.warning("文件不存在")  # 应该用error,不是warning

# ❌ 错误写法3:字符串格式化在异常发生前就执行了
# logger.debug("用户 %s 的数据: %s" % (user, get_expensive_data()))
# get_expensive_data() 即使level=INFO也会执行!

# ✅ 正确写法
try:
    result = risky_operation()
except Exception:
    logger.exception("执行risky_operation时发生异常")
    # exception() 等价于 error(msg, exc_info=True)

核心规则:

  • logger.exception() 代替 logger.error(str(e)),保留完整堆栈
  • %s 占位符而非f-string: logger.debug("用户 %s 登录", user) 优于 logger.debug(f"用户 {user} 登录"),因为前者的字符串拼接只在日志实际输出时才发生
  • 不要吞异常:记录完日志后,决定是 raise 还是返回默认值,不要把异常悄悄吃掉

常见报错与解决

1. 日志重复输出

现象:同一条日志在控制台打印了两次甚至三次。

原因:多次调用 basicConfig()addHandler() 加重复了。

解决:

# 在addHandler前检查
if not logger.handlers:
    logger.addHandler(handler)

# 或者用 hasHandlers()
if not logger.hasHandlers():
    logger.addHandler(handler)

2. 日志文件中文乱码

解决:所有 FileHandler 都加上 encoding='utf-8'

handler = logging.FileHandler('app.log', encoding='utf-8')

3. basicConfig 不生效

原因:basicConfig() 只在第一次调用时生效。如果某个模块导入时就调了 logging.info()(会触发默认配置),后续的 basicConfig() 就被忽略了。

解决:在入口文件最开始(所有import之前)调用 basicConfig(),或用 logging.config.dictConfig 替代。

FAQ

Q: 日志文件会无限增大吗?怎么控制磁盘占用?

不会,只要你用 RotatingFileHandler(按大小轮转)或 TimedRotatingFileHandler(按时间轮转)。设置 maxBytesbackupCount 限制总占用空间。一个10MB×5备份的配置最多占60MB磁盘。生产环境建议配合logrotate或云日志服务(如阿里云SLS、腾讯云CLS)做长期归档。

Q: print 和 logging 到底能不能混用?

能,但不推荐。logging模块内部也用了 sys.stdout,混用时日志顺序可能错乱。如果你有历史遗留代码一时改不完,可以把print重定向到logging:import sys; class PrintLogger: write = lambda self, msg: logging.info(msg.strip()) if msg.strip() else None; sys.stdout = PrintLogger()。但生产环境建议彻底替换。

Q: 多进程环境下写同一个日志文件安全吗?

不推荐。Python的logging模块在单进程下是线程安全的(内部有锁),但多进程同时写同一个文件可能导致日志交错或丢失。解决方案:(1) 用SocketHandler将日志发送到独立日志进程;(2) 每个进程写自己的文件;(3) 使用支持多进程的QueueHandler+QueueListener模式(Python 3.2+)。生产环境通常用ELK(Elasticsearch+Logstash+Kibana)或Loki做集中式日志管理。

总结

日志是程序员排查问题的第一工具,也是生产环境监控的基础。核心要点:

  • 5行代码起步: basicConfig() + getLogger() 就能替换掉print
  • 日志等级管理: DEBUG→INFO→WARNING→ERROR→CRITICAL,生产环境设INFO以上
  • 文件自动轮转: RotatingFileHandler和TimedRotatingFileHandler防止磁盘撑爆
  • 多模块架构:__name__ 做logger名,统一在入口配置
  • 生产级配置: dictConfig统一管理,Flask/Django都有自己的LOGGING配置入口

把print换成logging,你的代码离专业级又近了一步。如果你还想深入了解日志聚合系统(ELK/Loki)或者Python异常处理最佳实践,可以看看本站相关文章。

📤 分享这篇文章