一个简单实用全面的python logger创建方法
Published:
import os
import sys
import logging
import functools
from termcolor import colored
@functools.lru_cache()
def create_logger(output_dir, dist_rank=0, name=''):
# create logger
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
logger.propagate = False
# create formatter
fmt = '[%(asctime)s %(name)s] (%(filename)s %(lineno)d): %(levelname)s %(message)s'
color_fmt = colored('[%(asctime)s %(name)s]', 'green') + \
colored('(%(filename)s %(lineno)d)', 'yellow') + ': %(levelname)s %(message)s'
# create console handlers for master process
if dist_rank == 0:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(
logging.Formatter(fmt=color_fmt, datefmt='%Y-%m-%d %H:%M:%S'))
logger.addHandler(console_handler)
# create file handlers
file_handler = logging.FileHandler(os.path.join(output_dir, f'log_rank{dist_rank}.txt'), mode='a')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(fmt=fmt, datefmt='%Y-%m-%d %H:%M:%S'))
logger.addHandler(file_handler)
return logger
这个函数 create_logger
是一个用来创建日志记录器的工具函数,使用 Python 的 logging
模块进行日志管理,同时借助 functools.lru_cache
提供了缓存功能。下面是对代码的详细解析:
函数功能
create_logger
函数的功能是创建一个可定制的日志记录器,用于记录程序运行的日志信息。它:
- 支持将日志输出到控制台(标准输出)和文件。
- 根据不同的
dist_rank
参数,可以为不同的进程创建独立的日志。 - 格式化日志信息,提供时间戳、文件名、行号等信息。
- 通过
@functools.lru_cache
实现日志器实例的缓存,避免重复创建。
代码详解
1. 装饰器 @functools.lru_cache()
@functools.lru_cache()
是 Python 内置的装饰器,提供函数结果的缓存功能。- 在这里,它确保每次调用
create_logger
时,如果传入的参数(output_dir
,dist_rank
,name
)相同,则返回缓存中的日志记录器实例,而不会重复创建。 - 这对于多次调用日志创建函数时非常高效。
2. 创建日志记录器
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
logger.propagate = False
logging.getLogger(name)
: 获取或创建一个名称为name
的日志记录器实例。logger.setLevel(logging.DEBUG)
: 设置日志的记录级别为DEBUG
,表示记录所有级别(DEBUG
及以上)的日志。logger.propagate = False
: 禁止日志冒泡,避免重复记录日志。
3. 日志格式
fmt = '[%(asctime)s %(name)s] (%(filename)s %(lineno)d): %(levelname)s %(message)s'
color_fmt = colored('[%(asctime)s %(name)s]', 'green') + \
colored('(%(filename)s %(lineno)d)', 'yellow') + ': %(levelname)s %(message)s'
fmt
: 定义日志的格式,包括时间、日志名称、文件名、行号、日志级别和日志消息。color_fmt
: 为控制台输出提供带颜色的日志格式(依赖colored
函数,可能来自termcolor
库),提高可读性。
4. 控制台日志处理器
if dist_rank == 0:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(logging.Formatter(fmt=color_fmt, datefmt='%Y-%m-%d %H:%M:%S'))
logger.addHandler(console_handler)
- 条件: 只有主进程(
dist_rank == 0
)会创建控制台日志处理器。 logging.StreamHandler(sys.stdout)
: 将日志输出到标准输出。setFormatter
: 使用带颜色的格式器color_fmt
格式化日志输出。addHandler
: 将处理器添加到日志记录器。
5. 文件日志处理器
file_handler = logging.FileHandler(os.path.join(output_dir, f'log_rank{dist_rank}.txt'), mode='a')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(fmt=fmt, datefmt='%Y-%m-%d %H:%M:%S'))
logger.addHandler(file_handler)
logging.FileHandler
: 将日志输出到文件。- 文件名: 根据
dist_rank
区分日志文件,确保不同进程的日志被独立保存。 mode='a'
: 以追加模式写入文件,不会覆盖已有内容。setFormatter
: 使用普通格式器fmt
格式化日志。addHandler
: 将文件处理器添加到日志记录器。
6. 返回日志记录器
return logger
- 返回创建的日志记录器实例,供外部使用。
使用场景
- 分布式训练: 通过
dist_rank
创建不同进程的日志记录器,分别记录日志信息。 - 日志管理: 同时记录到控制台和文件,便于实时查看和后续分析。
- 性能优化: 使用
@lru_cache
避免重复创建日志记录器。
小结
- 灵活性: 允许根据需要定制日志记录器名称、输出目录等。
- 多功能: 支持多种输出(控制台、文件)和格式(带颜色、普通格式)。
- 高效性: 借助缓存减少重复计算,提高效率。
在 Python 的 logging
模块中,日志冒泡(propagation)是一个重要的概念,它决定了日志消息是否会传递到父级的日志记录器。
日志冒泡机制
- 默认行为:
- 如果一个日志记录器记录了一条日志消息(比如调用了
logger.debug()
),该消息会传递给它的父级日志记录器,直到根日志记录器为止。 - 每一级日志记录器都会处理这条消息(如果有相应的处理器
Handler
)。
- 如果一个日志记录器记录了一条日志消息(比如调用了
- 结果:
- 如果多个日志记录器都包含处理器(
Handler
),同一条日志消息可能会被处理多次,导致重复输出。
- 如果多个日志记录器都包含处理器(
logger.propagate = False
的作用
- 当
logger.propagate = False
时:- 日志记录器会阻止日志消息向父级日志记录器传递(即禁用冒泡)。
- 日志消息只会由当前日志记录器的处理器(
Handler
)处理,而不会传递到父级日志记录器。
为什么要禁用日志冒泡?
- 避免重复日志输出:
- 如果
logger
和其父级日志记录器都绑定了处理器,消息会被重复处理(例如,输出到控制台两次)。 - 禁用冒泡可以确保每条日志只被处理一次。
- 如果
- 定制化日志记录:
- 当需要为某些特定模块或功能创建独立的日志记录器时,可以通过禁用冒泡使它们的日志行为互不干扰。
举例说明
没有禁用冒泡(propagate = True
,默认行为)
import logging
# 根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
root_handler = logging.StreamHandler()
root_handler.setFormatter(logging.Formatter('[ROOT] %(message)s'))
root_logger.addHandler(root_handler)
# 子日志记录器
child_logger = logging.getLogger('child')
child_logger.setLevel(logging.DEBUG)
child_handler = logging.StreamHandler()
child_handler.setFormatter(logging.Formatter('[CHILD] %(message)s'))
child_logger.addHandler(child_handler)
child_logger.debug("This is a message.")
输出:
[CHILD] This is a message.
[ROOT] This is a message.
- 消息被
child_logger
和root_logger
的处理器同时处理,重复输出。
禁用冒泡(propagate = False
)
child_logger.propagate = False
child_logger.debug("This is a message.")
输出:
[CHILD] This is a message.
- 消息仅由
child_logger
的处理器处理,不会传递给root_logger
,避免重复。
在 create_logger
中的使用
logger.propagate = False
- 禁用冒泡是为了确保创建的
logger
只由自身的Handler
处理日志消息。 - 这样可以避免日志重复输出到控制台或文件,特别是在多层次日志配置的情况下。