一个简单实用全面的python logger创建方法

2 minute read

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 函数的功能是创建一个可定制的日志记录器,用于记录程序运行的日志信息。它:

  1. 支持将日志输出到控制台(标准输出)和文件。
  2. 根据不同的 dist_rank 参数,可以为不同的进程创建独立的日志。
  3. 格式化日志信息,提供时间戳、文件名、行号等信息。
  4. 通过 @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)处理,而不会传递到父级日志记录器。

为什么要禁用日志冒泡?

  1. 避免重复日志输出:
    • 如果 logger 和其父级日志记录器都绑定了处理器,消息会被重复处理(例如,输出到控制台两次)。
    • 禁用冒泡可以确保每条日志只被处理一次。
  2. 定制化日志记录:
    • 当需要为某些特定模块或功能创建独立的日志记录器时,可以通过禁用冒泡使它们的日志行为互不干扰。

举例说明

没有禁用冒泡(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_loggerroot_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 处理日志消息。
  • 这样可以避免日志重复输出到控制台或文件,特别是在多层次日志配置的情况下。