前置知识: Python

Python与日志

00:00
5 min Beginner 2026/6/14

logging模块与日志配置

什么是日志

日志是程序运行时输出的记录信息。当程序出现问题时,日志是你排查错误的主要依据。没有日志的程序就像黑盒,出了问题完全无从下手。

Python 标准库中的 logging 模块提供了完整的日志功能,不需要安装第三方库。它支持多种日志级别、灵活的输出格式、文件轮转等特性,能满足从简单脚本到大型项目的各种需求。

基础概念

日志级别

日志级别从低到高分为五级,不同级别表示信息的重要程度不同:

  • DEBUG:调试信息,最详细的日志,只在开发时使用
  • INFO:普通信息,确认程序按预期运行
  • WARNING:警告信息,表示有潜在问题,但程序仍能正常工作
  • ERROR:错误信息,某些功能无法正常执行
  • CRITICAL:严重错误,程序可能无法继续运行

设置日志级别后,只有等于或高于该级别的日志才会被输出。例如设置为 INFO,则 DEBUG 级别的日志不会显示。

Logger、Handler 与 Formatter

  • Logger:日志记录器,是代码中直接使用的接口。每个 Logger 有一个名称,通常用模块名命名
  • Handler:日志处理器,决定日志输出到哪里(控制台、文件、网络等)
  • Formatter:日志格式器,决定日志的输出格式

一个 Logger 可以有多个 Handler,每个 Handler 可以有自己的 Formatter。

快速上手

最简单的日志

import logging

# 配置基本日志(只需一行)
logging.basicConfig(level=logging.INFO)

# 输出不同级别的日志
logging.debug("这是调试信息")      # 不会显示(级别低于 INFO)
logging.info("程序启动成功")       # 会显示
logging.warning("磁盘空间不足")    # 会显示
logging.error("文件读取失败")      # 会显示
logging.critical("数据库连接断开") # 会显示

自定义日志格式

import logging

# 配置日志格式
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

logger = logging.getLogger(__name__)

logger.info("处理开始")
logger.error("出错了")

输出示例:

2026-01-15 10:30:00 [INFO] __main__: 处理开始
2026-01-15 10:30:01 [ERROR] __main__: 出错了

输出到文件

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    filename='app.log',      # 日志输出到文件
    filemode='a'             # 追加模式(默认),'w' 为覆盖模式
)

logging.info("这条日志会写入 app.log 文件")

详细用法

使用 Logger 对象

在大型项目中,应该为每个模块创建独立的 Logger,而不是直接使用 logging.info:

import logging

# 为不同模块创建不同的 Logger
db_logger = logging.getLogger('app.database')
api_logger = logging.getLogger('app.api')

# 配置根 Logger
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)

# 不同模块的日志会带上不同的 Logger 名称
db_logger.info("数据库连接成功")   # 显示 app.database
api_logger.info("API 请求处理中")  # 显示 app.api
db_logger.error("查询超时")       # 显示 app.database

同时输出到控制台和文件

import logging

# 创建 Logger
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)

# 创建控制台 Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)  # 控制台只显示 INFO 及以上

# 创建文件 Handler
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'
)

# 给 Handler 设置格式
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

# 给 Logger 添加 Handler
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# 使用
logger.debug("这条只写入文件")     # 控制台不显示
logger.info("这条同时显示和写入")   # 控制台和文件都有
logger.error("错误信息")

日志文件轮转

长期运行的应用如果一直写入同一个日志文件,文件会越来越大。使用 RotatingFileHandler 可以按文件大小轮转:

import logging
from logging.handlers import RotatingFileHandler

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

# 创建轮转文件 Handler
# maxBytes:单个文件最大字节数(这里设为 10MB)
# backupCount:保留的备份文件数量
handler = RotatingFileHandler(
    'app.log',
    maxBytes=10 * 1024 * 1024,  # 10MB
    backupCount=5,
    encoding='utf-8'
)

handler.setFormatter(logging.Formatter(
    '%(asctime)s [%(levelname)s] %(message)s'
))
logger.addHandler(handler)

# 当 app.log 达到 10MB 时,会自动重命名为 app.log.1
# 然后创建新的 app.log 继续写入
# 最多保留 5 个备份文件(app.log.1 到 app.log.5)

按时间轮转日志

import logging
from logging.handlers import TimedRotatingFileHandler

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

# 创建按时间轮转的 Handler
handler = TimedRotatingFileHandler(
    'app.log',
    when='midnight',    # 每天午夜轮转
    interval=1,         # 间隔
    backupCount=30,     # 保留 30 天的日志
    encoding='utf-8'
)

# when 参数可选值:
# 'S' - 秒, 'M' - 分, 'H' - 小时, 'D' - 天
# 'midnight' - 每天午夜, 'W0'-'W6' - 每周几(W0=周一)

handler.setFormatter(logging.Formatter(
    '%(asctime)s [%(levelname)s] %(message)s'
))
logger.addHandler(handler)

记录异常信息

当捕获异常时,使用 exc_info=True 可以把完整的堆栈跟踪写入日志:

import logging

logging.basicConfig(
    level=logging.ERROR,
    format='%(asctime)s [%(levelname)s] %(message)s'
)

try:
    result = 1 / 0
except ZeroDivisionError:
    # exc_info=True 会记录完整的异常堆栈
    logging.error("计算出错", exc_info=True)

# 也可以用 logging.exception(),它自动设置 exc_info=True
try:
    result = 1 / 0
except ZeroDivisionError:
    logging.exception("计算出错")

使用字典配置

对于复杂项目,可以使用字典来配置日志,比代码配置更清晰:

import logging
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 %(funcName)s:%(lineno)d: %(message)s'
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'INFO',
            'formatter': 'standard',
            'stream': 'ext://sys.stdout',
        },
        'file': {
            'class': 'logging.handlers.RotatingFileHandler',
            'level': 'DEBUG',
            'formatter': 'detailed',
            'filename': 'app.log',
            'maxBytes': 10485760,  # 10MB
            'backupCount': 5,
            'encoding': 'utf-8',
        },
    },
    'loggers': {
        'myapp': {
            'level': 'DEBUG',
            'handlers': ['console', 'file'],
        },
        'myapp.database': {
            'level': 'INFO',
            'handlers': ['file'],
            'propagate': False,  # 不向父 Logger 传播
        },
    },
    'root': {
        'level': 'WARNING',
        'handlers': ['console'],
    },
}

# 应用配置
logging.config.dictConfig(LOGGING_CONFIG)

# 使用
logger = logging.getLogger('myapp')
logger.info("应用启动")

结构化日志(JSON 格式)

微服务日志分析平台(如 ELK)中,JSON 格式日志更容易被机器解析:

import logging
import json
from datetime import datetime

class JsonFormatter(logging.Formatter):
    """自定义 JSON 格式化器"""
    def format(self, record):
        log_entry = {
            'timestamp': datetime.utcnow().isoformat(),
            'level': record.levelname,
            'logger': record.name,
            'message': record.getMessage(),
            'module': record.module,
            'line': record.lineno,
        }
        if record.exc_info:
            log_entry['exception'] = self.formatException(record.exc_info)
        return json.dumps(log_entry, ensure_ascii=False)

# 使用 JSON 格式化器
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())

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

logger.info("用户登录", extra={'user_id': 123})

常见场景

FastAPI 项目中的日志配置

import logging
from logging.handlers import RotatingFileHandler
from fastapi import FastAPI, Request
import time

app = FastAPI()

# 配置日志
logger = logging.getLogger('api')
logger.setLevel(logging.INFO)

handler = RotatingFileHandler(
    'api.log', maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'
)
handler.setFormatter(logging.Formatter(
    '%(asctime)s [%(levelname)s] %(message)s'
))
logger.addHandler(handler)

# 请求日志中间件
@app.middleware("http")
async def log_requests(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    duration = time.time() - start_time
    logger.info(
        f"{request.method} {request.url.path} "
        f"status={response.status_code} duration={duration:.3f}s"
    )
    return response

@app.get("/users")
async def get_users():
    logger.info("获取用户列表")
    return {"users": []}

在类中使用日志

import logging

class UserService:
    """在类中使用日志的推荐方式"""
    def __init__(self):
        # 用类名作为 Logger 名称
        self.logger = logging.getLogger(self.__class__.__name__)

    def get_user(self, user_id: int):
        self.logger.info(f"查询用户: user_id={user_id}")
        try:
            user = self._fetch_user(user_id)
            self.logger.debug(f"查询成功: {user}")
            return user
        except Exception as e:
            self.logger.error(f"查询失败: user_id={user_id}", exc_info=True)
            raise

    def _fetch_user(self, user_id):
        # 模拟数据库查询
        return {"id": user_id, "name": "张三"}

注意事项与常见错误

不要用 print 代替 logging

print 输出信息无法控制级别、无法关闭、无法写入文件、无法添加时间戳。在正式项目中,始终使用 logging 而不是 print。

日志中的敏感信息

不要在日志记录密码、Token身份证号等敏感信息。如果必须记录,应该脱敏处理

# 错误:记录了明文密码
# logger.info(f"用户登录: password={password}")

# 正确:脱敏处理
logger.info(f"用户登录: password=***")

避免在日志中使用 f-string 的性能陷阱

日志级别过滤时,f-string 仍然会被求值。使用 % 式化或延迟格式化可以避免这个问题

# 不推荐:即使 DEBUG 级别被过滤,f-string 仍然会执行
# logger.debug(f"处理数据: {expensive_function()}")

# 推荐:使用 % 格式化,只在日志实际输出时才求值
logger.debug("处理数据: %s", expensive_function)

# 或者用 logger.isEnabledFor 检查
if logger.isEnabledFor(logging.DEBUG):
    logger.debug(f"处理数据: {expensive_function()}")

Logger 的传播机制

默认情况下, Logger 的日志会向上传播给父 Logger。如果你给 Logger 添加了 Handler,又没有设置 propagate=False,日志可能会被重复输出:

# 设置不向父 Logger 传播
child_logger = logging.getLogger('myapp.child')
child_logger.propagate = False  # 防止日志重复输出

basicConfig 只在第一次调用时生效

logging.basicConfig() 只在第一次调用时生效。如果之前已经调用过(或者其他库已经配置日志),再次调用不会有效果。建议程序入口处尽早调用 basicConfig。

进阶用法

自定义 Handler 发送日志到远程服务

import logging
import json
import urllib.request

class WebhookHandler(logging.Handler):
    """将日志发送到 Webhook(如飞书、钉钉、Slack)"""

    def __init__(self, webhook_url: str):
        super().__init__()
        self.webhook_url = webhook_url

    def emit(self, record):
        try:
            # 只发送 ERROR 及以上级别的日志
            if record.levelno < logging.ERROR:
                return

            log_entry = {
                'level': record.levelname,
                'message': record.getMessage(),
                'logger': record.name,
                'timestamp': record.created,
            }

            data = json.dumps(log_entry).encode('utf-8')
            req = urllib.request.Request(
                self.webhook_url,
                data=data,
                headers={'Content-Type': 'application/json'}
            )
            urllib.request.urlopen(req)
        except Exception:
            # 日志发送失败不应该影响程序运行
            self.handleError(record)

# 使用
logger = logging.getLogger('myapp')
logger.addHandler(WebhookHandler('https://your-webhook-url'))

使用 structlog 库

structlog 是一个更现代的日志库,提供更好的结构日志支持:

pip install structlog
import structlog

# 配置 structlog
structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.dev.ConsoleRenderer(),  # 开发环境用彩色控制台输出
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
)

logger = structlog.get_logger()

# 使用关键字参数记录结构化日志
logger.info("用户登录", user_id=123, ip="192.168.1.1")
logger.error("支付失败", order_id="ORD-001", reason="余额不足")

使用 loguru 库

loguru 是一个更简洁的日志库,开箱即用,不需要复杂配置

pip install loguru
from loguru import logger

# 默认输出到控制台,带颜色
logger.info("程序启动")

# 添加文件输出
logger.add("app.log", rotation="10 MB", retention="30 days", encoding="utf-8")

# 不同级别
logger.debug("调试信息")
logger.info("普通信息")
logger.warning("警告信息")
logger.error("错误信息")

# 记录异常
try:
    1 / 0
except ZeroDivisionError:
    logger.exception("计算出错")

# 结构化日志
logger.info("用户登录", user_id=123, ip="192.168.1.1")

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式