- 核心概念:理解
logging的基本组成部分。 - 基础用法:不进行配置时,
logging的默认行为。 - 配置方法:详细介绍三种主要的配置方式,并推荐最佳实践。
- 进阶配置:包括
RotatingFileHandler、TimedRotatingFileHandler、Formatter格式化等。 - 多模块/多应用日志管理:如何避免日志重复和混乱。
- 常见问题与最佳实践。
核心概念
在深入配置之前,必须理解 logging 的四个核心组件:
-
Logger (记录器):
- 这是你在代码中直接交互的对象,通过
logging.getLogger(name)获取。 - 它是日志的入口,负责决定哪些日志消息需要被处理,以及传递给哪个
Handler。 Logger有一个层级结构(命名空间,如a,a.b,a.b.c),子 Logger 会继承父 Logger 的配置(Handler和Level)。rootLogger 是所有 Logger 的根。
- 这是你在代码中直接交互的对象,通过
-
Handler (处理器):
Logger将日志消息传递给Handler。Handler负责将日志消息发送到指定的目的地,StreamHandler:输出到终端(标准错误stderr或标准输出stdout)。FileHandler:输出到文件。RotatingFileHandler:输出到文件,当日志文件达到大小时,会进行轮转(备份旧日志,创建新日志)。TimedRotatingFileHandler:按时间(如每天、每小时)轮转日志文件。SocketHandler:发送到网络上的另一台机器。SMTPHandler:通过邮件发送。
-
Formatter (格式化器):
Handler使用Formatter来格式化日志消息的最终输出字符串。- 你可以定义消息的格式,例如时间戳、日志级别、模块名、行号、消息内容等。
- 常用的格式化占位符:
%(asctime)s: 日志记录时间。%(levelname)s: 日志级别名称 (DEBUG, INFO, WARNING, ERROR, CRITICAL)。%(message)s: 日志消息内容。%(name)s: Logger 的名称。%(filename)s: 源文件名。%(funcName)s: 调用日志记录的函数名。%(lineno)d: 源代码行号。
-
Log Record (日志记录):
- 这是一个由
Logger创建的内部对象,包含了与日志消息相关的所有信息(时间、级别、消息、模块名、行号等)。Handler和Formatter都是基于这个对象来工作的。
- 这是一个由
工作流程:
你的代码调用 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 的简单范畴,推荐使用方法二或三
面向对象配置 (推荐)
这是最灵活、最强大、最推荐的方式,尤其是在大型应用程序或库中,它允许你精确控制每一个 Logger、Handler 和 Formatter。
核心思想:手动创建 Logger、Handler、Formatter,并将它们关联起来。
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_appLogger 级别为DEBUG,所以它接收所有级别的消息。console_handler级别为INFO,所以它只会处理INFO、WARNING、ERROR、CRITICAL。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).")
进阶配置
日志轮转
当日志文件变得非常大时,需要对其进行轮转。RotatingFileHandler 和 TimedRotatingFileHandler 是两个非常有用的工具。
-
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 属性
Logger 的 propagate 属性(默认为 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,就可以统一管理所有子模块的日志。
常见问题与最佳实践
常见问题
-
日志重复输出:
- 原因:通常是因为
Handler被重复添加,在basicConfig之后,又手动添加了一个Handler到rootlogger。 - 解决:确保
Handler只被添加一次,在面向对象配置中,在添加Handler前可以先移除所有已存在的Handler:logger.handlers.clear()。
- 原因:通常是因为
-
日志不显示:
- 原因:
Logger或Handler的level设置过高。Logger级别是INFO,但你尝试记录DEBUG级别的日志。 - 解决:检查
Logger和所有关联的Handler的level设置。
- 原因:
最佳实践
- 使用
__name__获取 Logger:在每个模块中,使用logger = logging.getLogger(__name__),这能自动创建与模块结构对应的 Logger 名称。 - 在应用程序的入口点配置日志:在
main.py或类似的主脚本中进行日志配置,而不是在每个模块中都配置一遍。 - 优先使用
dictConfig:对于任何非 trivial 的应用,都使用配置文件的方式来管理日志,将配置与代码逻辑分离。 - 为不同环境设置不同配置:可以通过环境变量来决定加载哪个配置文件(
dev_config.yaml,prod_config.yaml)。 - 合理设置日志级别:
DEBUG: 详细的调试信息,仅在开发时使用。INFO: 正常的应用流程信息,用于跟踪程序运行状态。- `WARNING**: 潜在的问题,但不影响程序运行。
- `ERROR**: 发生了错误,影响了程序的部分功能。
- `CRITICAL**: 严重的错误,可能导致整个程序崩溃。
- 使用日志轮转:对于文件日志,一定要使用
RotatingFileHandler或TimedRotatingFileHandler,避免日志文件无限增长。 - 结构化日志:对于复杂的应用,考虑使用 JSON 格式的日志,便于后续被日志分析系统(如 ELK Stack, Splunk)解析和分析,可以通过自定义
Formatter实现。
