杰瑞科技汇

Python logging如何高效配置?

  1. 核心概念:理解 logging 的基本组成部分。
  2. 基础用法:不进行配置时,logging 的默认行为。
  3. 配置方法:详细介绍三种主要的配置方式,并推荐最佳实践。
  4. 进阶配置:包括 RotatingFileHandlerTimedRotatingFileHandlerFormatter 格式化等。
  5. 多模块/多应用日志管理:如何避免日志重复和混乱。
  6. 常见问题与最佳实践

核心概念

在深入配置之前,必须理解 logging 的四个核心组件:

  1. Logger (记录器)

    • 这是你在代码中直接交互的对象,通过 logging.getLogger(name) 获取。
    • 它是日志的入口,负责决定哪些日志消息需要被处理,以及传递给哪个 Handler
    • Logger 有一个层级结构(命名空间,如 a, a.b, a.b.c),子 Logger 会继承父 Logger 的配置(HandlerLevel)。root Logger 是所有 Logger 的根。
  2. Handler (处理器)

    • Logger 将日志消息传递给 Handler
    • Handler 负责将日志消息发送到指定的目的地,
      • StreamHandler:输出到终端(标准错误 stderr 或标准输出 stdout)。
      • FileHandler:输出到文件。
      • RotatingFileHandler:输出到文件,当日志文件达到大小时,会进行轮转(备份旧日志,创建新日志)。
      • TimedRotatingFileHandler:按时间(如每天、每小时)轮转日志文件。
      • SocketHandler:发送到网络上的另一台机器。
      • SMTPHandler:通过邮件发送。
  3. Formatter (格式化器)

    • Handler 使用 Formatter 来格式化日志消息的最终输出字符串。
    • 你可以定义消息的格式,例如时间戳、日志级别、模块名、行号、消息内容等。
    • 常用的格式化占位符:
      • %(asctime)s: 日志记录时间。
      • %(levelname)s: 日志级别名称 (DEBUG, INFO, WARNING, ERROR, CRITICAL)。
      • %(message)s: 日志消息内容。
      • %(name)s: Logger 的名称。
      • %(filename)s: 源文件名。
      • %(funcName)s: 调用日志记录的函数名。
      • %(lineno)d: 源代码行号。
  4. Log Record (日志记录)

    • 这是一个由 Logger 创建的内部对象,包含了与日志消息相关的所有信息(时间、级别、消息、模块名、行号等)。HandlerFormatter 都是基于这个对象来工作的。

工作流程你的代码调用 logger.info(...) -> Logger 检查级别 -> 创建 LogRecord -> 传递给所有相关的 Handler -> Handler 使用 Formatter 格式化 LogRecord -> 将格式化后的字符串发送到最终目的地。


基础用法(未配置)

如果你不进行任何配置,直接使用 logging,它会采用一个默认配置

import logging
import time
# 直接调用,没有 getLogger
logging.debug("This is a debug message")
logging.info("This is an info message")
logging.warning("This is a warning message") # 这个会显示
logging.error("This is an error message")    # 这个会显示
logging.critical("This is a critical message") # 这个会显示

默认行为

  • Level: WARNING,只有 WARNING 及以上级别的日志才会被输出。
  • Handler: 一个 StreamHandler,默认输出到 sys.stderr(终端)。
  • Formatter: 输出格式为 levelname: message

上面的代码只会输出 WARNING, ERROR, CRITICAL 三条信息。


配置方法

有三种主要的方式来配置 logging,各有优劣。

函数式配置 (logging.basicConfig)

这是最简单、最直接的方式,适用于小型脚本或快速原型。

优点:简单,一行代码搞定。 缺点只能调用一次,多次调用 basicConfig 不会生效,灵活性差,无法为不同的 Handler 设置不同的 Formatter

import logging
# 使用 basicConfig 进行一次性配置
# filename: 输出到文件
# filemode: 文件打开模式 ('a' 追加, 'w' 覆盖)
# level: 设置根 Logger 的级别
# format: 设置日志格式
# datefmt: 设置时间格式
logging.basicConfig(
    level=logging.DEBUG,  # 设置全局级别为 DEBUG
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
    filename='app.log',  # 输出到文件
    filemode='a'         # 追加模式
)
# 所有级别的日志都会被记录到 app.log 文件中
logging.debug("This is a debug message")
logging.info("This is an info message")
logging.warning("This is a warning message")
# 如果想同时输出到控制台,需要额外配置一个 Handler
# 但这已经超出了 basicConfig 的简单范畴,推荐使用方法二或三

面向对象配置 (推荐)

这是最灵活、最强大、最推荐的方式,尤其是在大型应用程序或库中,它允许你精确控制每一个 LoggerHandlerFormatter

核心思想:手动创建 LoggerHandlerFormatter,并将它们关联起来。

import logging
# 1. 创建 Logger
logger = logging.getLogger("my_app") # 创建一个名为 "my_app" 的 Logger
logger.setLevel(logging.DEBUG)       # 设置此 Logger 的最低级别为 DEBUG
# 2. 创建 Handler
# 2.1 控制台 Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) # 控制台只处理 INFO 及以上级别的日志
# 2.2 文件 Handler
file_handler = logging.FileHandler('app.log', mode='a', encoding='utf-8')
file_handler.setLevel(logging.DEBUG) # 文件处理所有 DEBUG 及以上级别的日志
# 3. 创建 Formatter 并绑定到 Handler
# 为控制台和文件设置不同的格式
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s')
console_handler.setFormatter(console_formatter)
file_handler.setFormatter(file_formatter)
# 4. 将 Handler 添加到 Logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# --- 使用 Logger ---
logger.debug("This is a debug message, only in file.")
logger.info("This is an info message, in both console and file.")
logger.warning("This is a warning message, in both console and file.")

解释

  • my_app Logger 级别为 DEBUG,所以它接收所有级别的消息。
  • console_handler 级别为 INFO,所以它只会处理 INFOWARNINGERRORCRITICAL
  • file_handler 级别为 DEBUG,所以它会处理所有消息。
  • INFO 及以上的消息会同时出现在控制台和文件中,而 DEBUG 消息只会在文件中出现。

配置文件 (logging.config.dictConfig)

对于复杂的应用程序,将配置与代码分离是最佳实践。logging.config 模块允许你使用一个字典或一个配置文件(如 YAML, JSON)来定义整个日志系统。

优点:配置与代码完全解耦,易于管理和修改,适合大型项目。 缺点:需要额外的配置文件,初期设置稍显复杂。

创建一个配置文件 logging_config.yaml:

# logging_config.yaml
version: 1
disable_existing_loggers: False # 不要禁用已存在的 logger
formatters:
  simple:
    format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
  detailed:
    format: "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s"
handlers:
  console:
    class: logging.StreamHandler
    level: INFO
    formatter: simple
    stream: ext://sys.stdout
  file:
    class: logging.handlers.RotatingFileHandler
    level: DEBUG
    formatter: detailed
    filename: app.log
    maxBytes: 1048576 # 1MB
    backupCount: 3
    encoding: utf8
loggers:
  my_app:
    level: DEBUG
    handlers: [console, file]
    propagate: no # 不将日志传递给父 logger (root)
root:
  level: INFO
  handlers: [console]

在 Python 代码中加载配置:

import logging
import logging.config
import yaml
import os
# 从 YAML 文件加载配置
def setup_logging(default_path='logging_config.yaml', default_level=logging.INFO):
    """Setup logging configuration"""
    path = default_path
    if os.path.exists(path):
        with open(path, 'rt') as f:
            try:
                config = yaml.safe_load(f.read())
                logging.config.dictConfig(config)
                print("Logging configured from YAML file.")
            except Exception as e:
                print(f"Error loading logging config: {e}")
                logging.basicConfig(level=default_level)
    else:
        logging.basicConfig(level=default_level)
        print("Logging configured using basicConfig (YAML file not found).")
# 初始化日志配置
setup_logging()
# --- 使用 Logger ---
# 这个 logger 会在配置文件中被定义
logger = logging.getLogger("my_app")
logger.debug("This is a debug message from my_app (only in file).")
logger.info("This is an info message from my_app (in console and file).")
# root logger 也会生效
logging.info("This is an info message from root logger (only in console).")

进阶配置

日志轮转

当日志文件变得非常大时,需要对其进行轮转。RotatingFileHandlerTimedRotatingFileHandler 是两个非常有用的工具。

  • RotatingFileHandler: 当文件大小达到 maxBytes 时,会重命名当前文件(如 app.log.1),然后创建一个新的 app.log,最多保留 backupCount 个备份文件。

    from logging.handlers import RotatingFileHandler
    file_handler = RotatingFileHandler(
        'app.log',
        maxBytes=1024 * 1024, # 1MB
        backupCount=5,
        encoding='utf-8'
    )
  • TimedRotatingFileHandler: 按时间间隔轮转日志文件。

    • when: 可以是 'S' (秒), 'M' (分), 'H' (时), 'D' (天), 'midnight' (每天午夜), 'W0-W6' (每周几)。
    • interval: when 的时间间隔。
      from logging.handlers import TimedRotatingFileHandler
      file_handler = TimedRotatingFileHandler(
      'app.log',
      when='midnight', # 每天午夜轮转
      interval=1,
      backupCount=7,   # 保留7天的日志
      encoding='utf-8'
      )

propagate 属性

Loggerpropagate 属性(默认为 True)决定了日志是否应该传递给其父 Logger(最终到达 root Logger)。

import logging
# root logger 默认有一个 StreamHandler
logging.basicConfig(level=logging.DEBUG)
# 创建一个 child logger
child_logger = logging.getLogger("parent.child")
child_logger.propagate = False # 阻止日志向上传播
child_logger.info("This message will ONLY be handled by child_logger's handlers, not root's.")
logging.info("This message is handled by root logger.")

多模块/多应用日志管理

在大型项目中,多个模块可能需要记录日志,最佳实践是每个模块都获取一个以该模块名为名的 Logger

# my_app/main.py
import logging
import my_module
# 获取一个名为 "my_app" 的 logger
# 它的配置将在 my_module 中被设置,或者通过全局配置文件
logger = logging.getLogger("my_app")
logger.info("Main application started.")
my_module.do_something()
# my_app/my_module.py
import logging
# 获取一个名为 "my_app.my_module" 的 logger
# 它会继承 "my_app" logger 的配置
logger = logging.getLogger(__name__) # __name__ 是 'my_app.my_module'
def do_something():
    logger.debug("Doing something in my_module.")
    logger.warning("Something might be wrong here.")

这样,my_module 中的日志会自动带有 my_app.my_module 的前缀,方便在日志中追踪来源,通过在 my_app 的顶层 Logger(如 my_app)上配置 Handler,就可以统一管理所有子模块的日志。


常见问题与最佳实践

常见问题

  1. 日志重复输出

    • 原因:通常是因为 Handler 被重复添加,在 basicConfig 之后,又手动添加了一个 Handlerroot logger。
    • 解决:确保 Handler 只被添加一次,在面向对象配置中,在添加 Handler 前可以先移除所有已存在的 Handlerlogger.handlers.clear()
  2. 日志不显示

    • 原因LoggerHandlerlevel 设置过高。Logger 级别是 INFO,但你尝试记录 DEBUG 级别的日志。
    • 解决:检查 Logger 和所有关联的 Handlerlevel 设置。

最佳实践

  1. 使用 __name__ 获取 Logger:在每个模块中,使用 logger = logging.getLogger(__name__),这能自动创建与模块结构对应的 Logger 名称。
  2. 在应用程序的入口点配置日志:在 main.py 或类似的主脚本中进行日志配置,而不是在每个模块中都配置一遍。
  3. 优先使用 dictConfig:对于任何非 trivial 的应用,都使用配置文件的方式来管理日志,将配置与代码逻辑分离。
  4. 为不同环境设置不同配置:可以通过环境变量来决定加载哪个配置文件(dev_config.yaml, prod_config.yaml)。
  5. 合理设置日志级别
    • DEBUG: 详细的调试信息,仅在开发时使用。
    • INFO: 正常的应用流程信息,用于跟踪程序运行状态。
    • `WARNING**: 潜在的问题,但不影响程序运行。
    • `ERROR**: 发生了错误,影响了程序的部分功能。
    • `CRITICAL**: 严重的错误,可能导致整个程序崩溃。
  6. 使用日志轮转:对于文件日志,一定要使用 RotatingFileHandlerTimedRotatingFileHandler,避免日志文件无限增长。
  7. 结构化日志:对于复杂的应用,考虑使用 JSON 格式的日志,便于后续被日志分析系统(如 ELK Stack, Splunk)解析和分析,可以通过自定义 Formatter 实现。
分享:
扫描分享到社交APP
上一篇
下一篇