在 Python3 中根据消息日志级别修改日志消息格式

Posted

技术标签:

【中文标题】在 Python3 中根据消息日志级别修改日志消息格式【英文标题】:Modifying logging message format based on message logging level in 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,而我自己破坏了它。

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

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

【问题讨论】:

【参考方案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 与此(请参阅***.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[colorm%(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

【讨论】:

以上是关于在 Python3 中根据消息日志级别修改日志消息格式的主要内容,如果未能解决你的问题,请参考以下文章

删除启动消息以更改 Spark 日志级别

如何在谷歌应用引擎日志查看器中正确过滤日志消息?

Python日志模块介绍

Python - 在打印前按级别对日志消息进行排序

ROS学习之日志消息

在 slf4j 运行时设置消息的日志级别