🐍 第12课:日志

—— 程序的黑匣子

🏆 分级日志+文件轮转
✅ Python验证通过

📌 本课目标

1️⃣ 日志基础

import logging

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

# 五个日志级别
logging.debug("调试信息 - 详细诊断")
logging.info("一般信息 - 确认程序正常")
logging.warning("警告信息 - 意外但可处理")
logging.error("错误信息 - 某功能失败")
logging.critical("严重错误 - 程序可能无法继续")

# 级别数值: DEBUG=10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50

2️⃣ 创建专用 Logger

import logging

# 获取模块级 Logger(推荐方式)
logger = logging.getLogger(__name__)

# 配置 Logger
logger.setLevel(logging.DEBUG)

# 控制台 Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_fmt = logging.Formatter(
    '%(asctime)s [%(levelname)s] %(message)s',
    datefmt='%H:%M:%S'
)
console_handler.setFormatter(console_fmt)

# 文件 Handler
file_handler = logging.FileHandler(
    'app.log', encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)
file_fmt = logging.Formatter(
    '%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s'
)
file_handler.setFormatter(file_fmt)

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

# 使用
logger.info("程序启动")
logger.debug("详细调试信息(只写文件)")
logger.warning("需要注意")

3️⃣ 文件轮转

import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler

# 按大小轮转
rotating_handler = RotatingFileHandler(
    filename='app.log',
    maxBytes=10 * 1024 * 1024,  # 10MB
    backupCount=5,                   # 保留5个备份
    encoding='utf-8',
)
# 文件超过10MB时自动轮转: app.log → app.log.1 → app.log.2 ...

# 按时间轮转
timed_handler = TimedRotatingFileHandler(
    filename='app.log',
    when='midnight',     # 每天午夜轮转
    backupCount=30,           # 保留30天
    encoding='utf-8',
)
# when 参数: 'S'秒 'M'分 'H'时 'D'天 'midnight' 'W0'-'W6'(周一到周日)
# 生成文件名: app.log.2026-05-18

4️⃣ 完整日志配置函数

import logging
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path

def setup_logging(
    log_dir: str = "logs",
    level: str = "INFO",
    max_bytes: int = 10 * 1024 * 1024,
    backup_count: int = 5,
) -> logging.Logger:
    """一站式日志配置"""
    # 创建日志目录
    log_path = Path(log_dir)
    log_path.mkdir(parents=True, exist_ok=True)

    # Root Logger
    root_logger = logging.getLogger()
    root_logger.setLevel(logging.DEBUG)

    # 清除已有 Handler(避免重复)
    root_logger.handlers.clear()

    # 控制台
    console = logging.StreamHandler(sys.stdout)
    console.setLevel(getattr(logging, level.upper()))
    console.setFormatter(logging.Formatter(
        '%(asctime)s [%(levelname)s] %(message)s',
        datefmt='%H:%M:%S'
    ))
    root_logger.addHandler(console)

    # 全量日志文件
    all_handler = RotatingFileHandler(
        log_path / "all.log",
        maxBytes=max_bytes,
        backupCount=backup_count,
        encoding="utf-8",
    )
    all_handler.setLevel(logging.DEBUG)
    all_handler.setFormatter(logging.Formatter(
        '%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s'
    ))
    root_logger.addHandler(all_handler)

    # 错误日志文件
    err_handler = RotatingFileHandler(
        log_path / "error.log",
        maxBytes=max_bytes,
        backupCount=backup_count,
        encoding="utf-8",
    )
    err_handler.setLevel(logging.ERROR)
    err_handler.setFormatter(logging.Formatter(
        '%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s'
    ))
    root_logger.addHandler(err_handler)

    return root_logger

# 使用
logger = setup_logging(level="DEBUG")
logger.info("日志系统初始化完成")

5️⃣ 日志最佳实践

import logging

logger = logging.getLogger(__name__)

# ✅ 用 f-string 或 % 格式化(延迟格式化)
# logging 内部会在日志级别不够时跳过格式化,更高效
logger.info("处理文件: %s, 大小: %d", filename, size)

# ❌ 不要这样写(无论级别都会格式化)
# logger.info(f"处理文件: {filename}, 大小: {size}")

# ✅ 异常日志用 exc_info
try:
    risky_operation()
except Exception as e:
    logger.error("操作失败: %s", e, exc_info=True)
    # 或使用 logger.exception() 自动包含堆栈
    # logger.exception("操作失败: %s", e)

# ✅ 结构化日志(JSON格式,方便ELK分析)
import json

class JsonFormatter(logging.Formatter):
    def format(self, record):
        log_data = {
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "module": record.module,
            "line": record.lineno,
        }
        if record.exc_info:
            log_data["exception"] = self.formatException(record.exc_info)
        return json.dumps(log_data, ensure_ascii=False)

6️⃣ 验证脚本

#!/usr/bin/env python3
"""第12课 日志验证"""
import logging
import tempfile
import os
from pathlib import Path
from logging.handlers import RotatingFileHandler

def test_logging_levels():
    """日志级别测试"""
    assert logging.DEBUG == 10
    assert logging.INFO == 20
    assert logging.WARNING == 30
    assert logging.ERROR == 40
    assert logging.CRITICAL == 50
    print("✅ 日志级别测试通过")

def test_logger_creation():
    """Logger创建测试"""
    logger = logging.getLogger("test_module")
    logger.setLevel(logging.DEBUG)
    assert logger.name == "test_module"
    assert logger.level == logging.DEBUG
    print("✅ Logger创建测试通过")

def test_file_handler():
    """文件Handler测试"""
    with tempfile.TemporaryDirectory() as tmpdir:
        log_file = Path(tmpdir) / "test.log"
        handler = logging.FileHandler(str(log_file), encoding="utf-8")
        handler.setLevel(logging.DEBUG)

        logger = logging.getLogger("test_file")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        logger.info("测试消息")
        handler.flush()

        content = log_file.read_text(encoding="utf-8")
        assert "测试消息" in content
        logger.removeHandler(handler)
        handler.close()
    print("✅ 文件Handler测试通过")

def test_rotating_handler():
    """轮转Handler测试"""
    with tempfile.TemporaryDirectory() as tmpdir:
        log_file = Path(tmpdir) / "rotate.log"
        handler = RotatingFileHandler(
            str(log_file), maxBytes=100, backupCount=3, encoding="utf-8"
        )
        logger = logging.getLogger("test_rotate")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)

        # 写入足够多的日志触发轮转
        for i in range(50):
            logger.info("Line %d: padding text to fill space", i)
        handler.flush()

        # 验证轮转文件存在
        log_dir = Path(tmpdir)
        log_files = list(log_dir.glob("rotate.log*"))
        assert len(log_files) >= 1
        logger.removeHandler(handler)
        handler.close()
    print("✅ 轮转Handler测试通过")

if __name__ == "__main__":
    test_logging_levels()
    test_logger_creation()
    test_file_handler()
    test_rotating_handler()
    print("\n🎉 第12课全部验证通过!")
✅ 日志级别测试通过 ✅ Logger创建测试通过 ✅ 文件Handler测试通过 ✅ 轮转Handler测试通过 🎉 第12课全部验证通过!

🔑 本课要点

  1. getLogger(__name__)——模块级 Logger,自动带模块名
  2. 延迟格式化——logger.info("%s", var) 比 f-string 更安全
  3. RotatingFileHandler——按大小轮转,避免日志文件无限增长
  4. TimedRotatingFileHandler——按时间轮转,适合生产环境
  5. exc_info=True——异常日志必须带堆栈信息