【问题标题】:Modifying logging message format based on message logging level in Python3在 Python3 中根据消息日志级别修改日志消息格式
【发布时间】:2013-01-28 11:49:15
【问题描述】:

我为 python 2 here 提出了这个问题,但是当答案不再适用于 Python 3.2.3 时,我又遇到了这个问题。

这是适用于 Python 2.7.3 的代码:

import logging

# Attempt to set up a Python3 logger than will print custom messages
# based on each message's logging level.
# The technique recommended for Python2 does not appear to work for
# Python3

class CustomConsoleFormatter(logging.Formatter):
    """
    Modify the way DEBUG messages are displayed.

    """
    def __init__(self, fmt="%(levelno)d: %(msg)s"):
        logging.Formatter.__init__(self, fmt=fmt)

    def format(self, record):

        # Remember the original format
        format_orig = self._fmt

        if record.levelno == logging.DEBUG:
            self._fmt = "DEBUG: %(msg)s"

        # Call the original formatter to do the grunt work
        result = logging.Formatter.format(self, record)

        # Restore the original format
        self._fmt = format_orig

        return result


# Set up a logger
my_logger = logging.getLogger("my_custom_logger")
my_logger.setLevel(logging.DEBUG)

my_formatter = CustomConsoleFormatter()

console_handler = logging.StreamHandler()
console_handler.setFormatter(my_formatter)

my_logger.addHandler(console_handler)

my_logger.debug("This is a DEBUG-level message")
my_logger.info("This is an INFO-level message")

使用 Python 2.7.3 运行:

tcsh-16: python demo_python_2.7.3.py 
DEBUG: This is a DEBUG-level message
20: This is an INFO-level message


据我所知,转换到 Python3 只需要对 CustomConsoleFormatter 稍作修改。init():

def __init__(self):
    super().__init__(fmt="%(levelno)d: %(msg)s", datefmt=None, style='%')

在 Python 3.2.3 上:

tcsh-26: python3 demo_python_3.2.3.py
10: This is a DEBUG-level message
20: This is an INFO-level message


如您所见,我用 'DEBUG' 替换 '10' 的愿望遭到了挫败。

我尝试在 Python3 源代码中进行挖掘,看起来 PercentStyle 实例化正在破坏 self._fmt,而我自己破坏了它。

我的伐木工作快要解决这个问题了。

谁能推荐另一种方式,或者指出我忽略了什么?

【问题讨论】:

    标签: logging python-3.x


    【解决方案1】:

    经过一番挖掘,我能够修改 Python 2 解决方案以使用 Python 3。在 Python2 中,有必要临时覆盖 Formatter._fmt。在 Python3 中,对多种格式字符串类型的支持需要我们暂时覆盖 Formatter._style._fmt

    # Custom formatter
    class MyFormatter(logging.Formatter):
    
        err_fmt  = "ERROR: %(msg)s"
        dbg_fmt  = "DBG: %(module)s: %(lineno)d: %(msg)s"
        info_fmt = "%(msg)s"
    
        def __init__(self):
            super().__init__(fmt="%(levelno)d: %(msg)s", datefmt=None, style='%')  
        
        def format(self, record):
    
            # Save the original format configured by the user
            # when the logger formatter was instantiated
            format_orig = self._style._fmt
    
            # Replace the original format with one customized by logging level
            if record.levelno == logging.DEBUG:
                self._style._fmt = MyFormatter.dbg_fmt
    
            elif record.levelno == logging.INFO:
                self._style._fmt = MyFormatter.info_fmt
    
            elif record.levelno == logging.ERROR:
                self._style._fmt = MyFormatter.err_fmt
    
            # Call the original formatter class to do the grunt work
            result = logging.Formatter.format(self, record)
    
            # Restore the original format configured by the user
            self._style._fmt = format_orig
    
            return result
    

    这里是 Halloleo 的示例,说明如何在您的脚本中使用上述内容(来自 Python2 version of this question):

    fmt = MyFormatter()
    hdlr = logging.StreamHandler(sys.stdout)
    
    hdlr.setFormatter(fmt)
    logging.root.addHandler(hdlr)
    logging.root.setLevel(logging.DEBUG)
    

    【讨论】:

    • logging.info('test %i', 3) 不会打印test 3 与此(请参阅stackoverflow.com/a/62488520/4417769)。另请注意,DEBUG 应为 logging.DEBUG
    • @sezanzeb s/DEBUG/logging.DEBUG/ 已修复。感谢您指出这一点。
    • 至关重要的是,这不尊重关卡的整体性。它应该能够为 DEBUG 和 INFO 之间的级别选择合理的格式化程序,但不能。此外,它会进行非线程安全的实例突变,没有锁也没有finally
    【解决方案2】:

    我更喜欢这个,因为它更短、更简单,并且不需要像 'ERROR' 这样的字符串进行硬编码。无需重新设置._fmt,因为else: 可以处理得很好。

    另外,使用 "%(msg)s" 不适用于惰性日志记录!

    class Formatter(logging.Formatter):
        def format(self, record):
            if record.levelno == logging.INFO:
                self._style._fmt = "%(message)s"
            else:
                self._style._fmt = "%(levelname)s: %(message)s"
            return super().format(record)
    

    使用示例:

    import logging
    
    logger = logging.getLogger()
    handler = logging.StreamHandler()
    handler.setFormatter(Formatter())
    logger.setLevel(logging.DEBUG)
    logger.addHandler(handler)
    
    logger.debug('foo')
    logger.info('bar %d', 4)
    
    DEBUG: foo
    bar 4
    

    如果你想让关卡名称着色:

    class Formatter(logging.Formatter):
        def format(self, record):
            if record.levelno == logging.INFO:
                self._style._fmt = "%(message)s"
            else:
                color = {
                    logging.WARNING: 33,
                    logging.ERROR: 31,
                    logging.FATAL: 31,
                    logging.DEBUG: 36
                }.get(record.levelno, 0)
                self._style._fmt = f"\033[{color}m%(levelname)s\033[0m: %(message)s"
            return super().format(record)
    

    请参阅https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit 了解颜色编号

    【讨论】:

    • 这会进行非线程安全的实例突变,没有锁,没有finally,也没有级别恢复。
    【解决方案3】:

    another answer 的交叉发布。它不起作用,因为 logging.Formatter 的新实现(截至目前为 3.2+,3.4)现在依赖于格式化样式。这依赖于'{' 样式格式,但可以进行调整。可以改进为更通用,并允许选择格式样式和自定义消息作为 __init__ 的参数。

    class SpecialFormatter(logging.Formatter):
        FORMATS = {logging.DEBUG : logging._STYLES['{']("{module} DEBUG: {lineno}: {message}"),
               logging.ERROR : logging._STYLES['{']("{module} ERROR: {message}"),
               logging.INFO : logging._STYLES['{']("{module}: {message}"),
               'DEFAULT' : logging._STYLES['{']("{module}: {message}")}
    
        def format(self, record):
            # Ugly. Should be better
            self._style = self.FORMATS.get(record.levelno, self.FORMATS['DEFAULT'])
            return logging.Formatter.format(self, record)
    
    hdlr = logging.StreamHandler(sys.stderr)
    hdlr.setFormatter(SpecialFormatter())
    logging.root.addHandler(hdlr)
    logging.root.setLevel(logging.INFO)
    

    【讨论】:

    • 这会进行非线程安全的实例突变,没有锁,没有finally,也没有级别恢复。
    【解决方案4】:

    这个问题我迟到了,但这是我的解决方案。它遵循原始的 python 2 语法风格。通常,由于添加了样式支持,您应该使用三个新类。它们是:PercentStyle、StrFormatStyle 和 StringTemplateStyle。

    from logging import Formatter, PercentStyle, ERROR, WARNING, INFO, DEBUG
    class SrvLogFormat(Formatter):
        def __init__(self):
            super().__init__(fmt=env.fmt_log, datefmt=env.fmt_log_date)
    
        def format(self, record):
            original_style = self._style
    
            if record.levelno == DEBUG:
                self._style = PercentStyle(env.fmt_dflt)
            if record.levelno == INFO:
                self._style = PercentStyle(env.fmt_dflt)
            if record.levelno == WARNING:
                self._style = PercentStyle(env.fmt_dflt)
            if record.levelno == ERROR:
                self._style = PercentStyle(env.fmt_err)
    
            result = Formatter.format(self, record)
            self._style = original_style
            return result
    

    【讨论】:

      【解决方案5】:

      由于一些奇怪的原因,@JS 和@Evpok 的解决方案引发了一些错误(我使用的是 Python 3.7,这可能就是原因)。

      这个解决方案对我有用:

      class CustomFormatter(logging.Formatter):
          """Logging Formatter to add colors and count warning / errors"""
      
          FORMATS = {
              logging.ERROR: "ERROR: %(msg)s",
              logging.WARNING: "WARNING: %(msg)s",
              logging.DEBUG: "DBG: %(module)s: %(lineno)d: %(msg)s",
              "DEFAULT": "%(msg)s",
          }
      
          def format(self, record):
              log_fmt = self.FORMATS.get(record.levelno, self.FORMATS['DEFAULT'])
              formatter = logging.Formatter(log_fmt)
              return formatter.format(record)
      
      logger = logging.getLogger(__name__)
      logger.setLevel(logging.DEBUG)
      logger_ch = logging.StreamHandler()
      logger_ch.setLevel(logging.INFO)
      logger_ch.setFormatter(CustomFormatter())
      logger.addHandler(logger_ch)
      

      【讨论】:

        【解决方案6】:
        import logging
        from logging import DEBUG, INFO, WARN, ERROR
        
        class LogFormatter(logging.Formatter):
        
            formats = {
                DEBUG: "DEBUG: %(msg)s",
                INFO:  "%(msg)s",
                WARN:  "WARNING: %(msg)s",
                ERROR: "ERROR: %(msg)s"
            }
            
            def format(self, record):
                return LogFormatter.formats.get(
                    record.levelno, self._fmt) % record.__dict__
        

        在logging/__init__.py的源码中,PercentStyle的方法_format很简单

            def _format(self, record):
                return self._fmt % record.__dict__
        

        因此使用% 运算符也可以。

        【讨论】:

          【解决方案7】:

          这个 Python3 问题和它的 Python2 问题的答案都存在重大缺陷:

          • 当级别不打算离散时,通过相等比较或字典查找来选择级别;它们存在于整数行上并且应该是可排序的。
          • 当格式化程序实例发生变异时,没有锁来保护_fmt 成员,因此这是线程不安全的。
          • 对该成员的修改至少应被try/finally包围。

          真的,该成员根本不应该被修改。解决此问题的一种简单方法是实例化一个或多个不可变代理格式化程序并根据需要推迟它们。另外,通过二等分而不是相等来选择它们,以支持任意级别:

          import logging
          from bisect import bisect
          from logging import getLogger, Formatter, LogRecord, StreamHandler
          from typing import Dict
          
          
          class LevelFormatter(Formatter):
              def __init__(self, formats: Dict[int, str], **kwargs):
                  super().__init__()
          
                  if 'fmt' in kwargs:
                      raise ValueError(
                          'Format string must be passed to level-surrogate formatters, '
                          'not this one'
                      )
          
                  self.formats = sorted(
                      (level, Formatter(fmt, **kwargs)) for level, fmt in formats.items()
                  )
          
              def format(self, record: LogRecord) -> str:
                  idx = bisect(self.formats, (record.levelno,), hi=len(self.formats)-1)
                  level, formatter = self.formats[idx]
                  return formatter.format(record)
          
          
          def test():
              handler = StreamHandler()
              handler.setFormatter(
                  LevelFormatter(
                      {
                          logging.INFO: '%(levelname)s (info): %(message)s',
                          logging.WARNING: '%(levelname)s: (warning): %(message)s',
                      }
                  )
              )
              handler.setLevel(logging.DEBUG)
          
              logger = getLogger('test_logger')
              logger.setLevel(logging.DEBUG)
              logger.addHandler(handler)
          
              logger.debug('mdebug')
              logger.info('minfo')
              logger.log(logging.INFO + 1, 'higher minfo')
              logger.warning('mwarning')
              logger.error('merror')
              logger.critical('mcritical')
          
          
          test()
          

          输出

          DEBUG (info): mdebug
          INFO (info): minfo
          Level 21: (warning): higher minfo
          WARNING: (warning): mwarning
          ERROR: (warning): merror
          CRITICAL: (warning): mcritical
          

          【讨论】:

            猜你喜欢
            • 2010-11-23
            • 2011-03-09
            • 2016-02-20
            • 1970-01-01
            • 2015-06-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多