如何在多个有自己格式化程序的logger处理器中使用python日志包插入额外格式?

6 投票
3 回答
3789 浏览
提问于 2025-04-16 10:17

我有一个日志记录器,它有多个处理器,每个处理器都有自己的格式化器。现在我想加一个缩进功能,缩进的级别可以在运行时控制。我希望所有处理器发出的消息都能有这个缩进。我试着把它做成一个过滤器,但发现我似乎无法改变消息的内容。然后我又试着把它做成一个格式化器,但每个处理器只能有一个格式化器。我该如何在不明确更改每个处理器的格式化器的情况下添加这样的缩进呢?
我还要提到,我有一个格式化器是一个类,它可以给输出添加颜色。这不是一个简单的格式字符串。


另外,我正在使用一个配置文件。理想情况下,我希望大部分功能都能通过这个文件来控制。然而,我需要修改缩进格式化器的状态(例如,设置缩进级别),但我不知道如何找到那个特定的格式化器,因为没有 logger.getFormatter("by_name") 这样的方法。
为了更清楚,我需要访问特定的格式化器实例,基本上是为了动态调整格式。这个实例是通过 logging.config 从文件中创建的。我找不到任何可以让我根据名称获取特定格式化器的方法。

3 个回答

0

好的,这里有一种方法可以让我得到我想要的结果,差不多能满足我的需求。首先,我创建一个LogRecord的子类,重写它的getMessage方法,以便可以插入缩进;然后,我再创建一个logger的子类,使用这个新的LogRecord来生成记录:

import logging
import logging.config  

################################################################################
class IndentingLogger(logging.Logger):
    """A Logger subclass to add indent on top of any logger output
    """
    ############################################################################
    def __init__(self, name = 'root', logging_level = logging.NOTSET):
        "Constructor to keep indent persistent"
        logging.Logger.__init__(self, name, logging_level)
        self.indenter = IndentedRecord("", logging.NOTSET, "", 0, None, None, None, None, None)

    ############################################################################
    def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
        return self.indenter.set_record(name, level, fn, lno, msg, args, exc_info, func, extra)


################################################################################
class IndentedRecord(logging.LogRecord):
    """A LogRecord subclass to add indent on top of any logger output
    """
    ######## Class data #########
    DEFAULT_INDENT_STR = '    '

    ############################################################################
    def __init__(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
        "Constructor"
        logging.LogRecord.__init__(self, name, level, fn, lno, msg, args, exc_info, func)
        self._indent_level = 0
        self._indent_str_base = IndentedRecord.DEFAULT_INDENT_STR
        self._indent_str = ""    # cache it

    ############################################################################
    def set_record(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
        "Constructs the base record"
        logging.LogRecord.__init__(self, name, level, fn, lno, msg, args, exc_info, func)
        return self

    ################################################################################
    def getMessage(self):
        "Adds indent on top of the normal getMessage result"

        # Call base class to get the formatted message
        message = logging.LogRecord.getMessage(self)

       # Now insert the indent
        return self._indent_str + message

    ################################################################################
    def indent(self, step = 1):
        "Change the current indent level by the step (use negative to decrease)"
        self._indent_level += step
        if self._indent_level < 0:
            self._indent_level = 0
        self._indent_str = self._indent_str_base * self._indent_level

    ################################################################################
    def set_indent_str(self, chars):
        "Change the current indent string"
        if not isinstance(chars, str):
            raise ValueError("Argument must be a string. Got %s" % chars)
        self._indent_str_base = chars

logging.config.fileConfig("reporter.conf")
logging.setLoggerClass(IndentingLogger)
logger = logging.getLogger('root') # will be wrong logger, if without argument

logger.debug("debug message")
logger.info("info message")
logger.indenter.indent(+1)
logger.warn("Indented? warn message")
logger.indenter.set_indent_str("***")
logger.error("Indented? error message: %s", "Oops, I did it again!")
logger.indenter.indent(+1)
logger.error("Indented? error message: %s", "Oops, I did it again!")
logger.indenter.indent(-1)
logger.critical("No indent; critical message")

结果是(实际上是有颜色的):

Debug: debug message
Info: info message
Warning:     Indented? warn message
Error:     Indented? error message: Oops, I did it again!
Error: ******Indented? error message: Oops, I did it again!
Internal Error: ***No indent; critical message

不过,日志级别的字符串还是不知怎么地跑到了前面,所以结果并不是我想要的样子。而且,这样做有点麻烦——为了这么一个简单的功能,搞得太复杂了 :(
有没有更好的主意?

5
#!/usr/bin/env python

import logging
from random import randint

log = logging.getLogger("mylog")
log.setLevel(logging.DEBUG)

class MyFormatter(logging.Formatter):
    def __init__(self, fmt):
        logging.Formatter.__init__(self, fmt)

    def format(self, record):
        indent = " " * randint(0, 10) # To show that it works
        msg = logging.Formatter.format(self, record)
        return "\n".join([indent + x for x in msg.split("\n")])

# Log to file
filehandler = logging.FileHandler("indent.txt", "w")
filehandler.setLevel(logging.DEBUG)
filehandler.setFormatter(MyFormatter("%(levelname)-10s %(message)s"))
log.addHandler(filehandler)

# Log to stdout too
streamhandler = logging.StreamHandler()
streamhandler.setLevel(logging.INFO)
streamhandler.setFormatter(MyFormatter("%(message)s"))
log.addHandler(streamhandler)

# Test it
log.debug("Can you show me the dog-kennels, please")
log.info("They could grip it by the husk")
log.warning("That's no ordinary rabbit!")
log.error("Nobody expects the spanish inquisition")
try:
    crunchy_frog()
except:
    log.exception("It's a real frog")

结果:

    They could grip it by the husk
    That's no ordinary rabbit!
          Nobody expects the spanish inquisition
         It's a real frog
         Traceback (most recent call last):
           File "./logtest2.py", line 36, in 
             crunchy_frog()
         NameError: name 'crunchy_frog' is not defined

我不太明白你的第二个问题。

1

这里有一个简单但有点小技巧的方法。我的所有处理程序的消息总是以一个消息级别的字符串开头。只需要在每次缩进变化时修改这些字符串就可以了:

# (make a LEVELS dict out of all the logging levels first)    
def indent(self, step = 1):
        "Change the current indent level by the step (use negative to decrease)"
        self._indent_level += step
        if self._indent_level < 0:
            self._indent_level = 0
        self._indent_str = self._indent_str_base * self._indent_level
        for lvl in LEVELS:
            level_name = self._indent_str + LEVELS[lvl]
            logging.addLevelName(lvl, level_name)

(关于缩进函数周围的内容,可以看看我其他的回答)
现在,缩进器可以作为一个独立的类存在,而不需要去处理日志过程中的细节。只要消息中包含级别字符串,缩进就会存在,即使在它之前还有其他内容。虽然这不是最理想的做法,但对我来说可能有效。
有没有人有其他适用于任何消息格式的好主意?

撰写回答