【问题标题】:How to add a custom loglevel to Python's logging facility如何将自定义日志级别添加到 Python 的日志记录工具
【发布时间】:2011-01-12 02:21:36
【问题描述】:

我想为我的应用程序设置日志级别 TRACE (5),因为我认为 debug() 还不够。另外log(5, msg) 不是我想要的。如何向 Python 记录器添加自定义日志级别?

我有一个mylogger.py,内容如下:

import logging

@property
def log(obj):
    myLogger = logging.getLogger(obj.__class__.__name__)
    return myLogger

在我的代码中,我以下列方式使用它:

class ExampleClass(object):
    from mylogger import log

    def __init__(self):
        '''The constructor with the logger'''
        self.log.debug("Init runs")

现在我想打电话给self.log.trace("foo bar")

提前感谢您的帮助。

编辑(2016 年 12 月 8 日):我将接受的答案更改为 pfa's,恕我直言,这是基于 Eric S 非常好的建议的出色解决方案。

【问题讨论】:

    标签: python logging python-logging


    【解决方案1】:

    结合所有现有答案和大量使用经验,我想我已经列出了所有需要做的事情,以确保完全无缝地使用新关卡。以下步骤假设您正在添加一个新级别 TRACE,其值为 logging.DEBUG - 5 == 5

    1. 需要调用logging.addLevelName(logging.DEBUG - 5, 'TRACE') 以在内部注册新级别,以便可以通过名称引用它。
    2. 需要将新级别作为属性添加到logging 本身以保持一致性:logging.TRACE = logging.DEBUG - 5
    3. 需要将名为trace 的方法添加到logging 模块中。它的行为应该像 debuginfo 等。
    4. 需要将一个名为trace 的方法添加到当前配置的记录器类中。由于不能 100% 保证是 logging.Logger,因此请改用 logging.getLoggerClass()

    所有步骤都在下面的方法中说明:

    def addLoggingLevel(levelName, levelNum, methodName=None):
        """
        Comprehensively adds a new logging level to the `logging` module and the
        currently configured logging class.
    
        `levelName` becomes an attribute of the `logging` module with the value
        `levelNum`. `methodName` becomes a convenience method for both `logging`
        itself and the class returned by `logging.getLoggerClass()` (usually just
        `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
        used.
    
        To avoid accidental clobberings of existing attributes, this method will
        raise an `AttributeError` if the level name is already an attribute of the
        `logging` module or if the method name is already present 
    
        Example
        -------
        >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
        >>> logging.getLogger(__name__).setLevel("TRACE")
        >>> logging.getLogger(__name__).trace('that worked')
        >>> logging.trace('so did this')
        >>> logging.TRACE
        5
    
        """
        if not methodName:
            methodName = levelName.lower()
    
        if hasattr(logging, levelName):
           raise AttributeError('{} already defined in logging module'.format(levelName))
        if hasattr(logging, methodName):
           raise AttributeError('{} already defined in logging module'.format(methodName))
        if hasattr(logging.getLoggerClass(), methodName):
           raise AttributeError('{} already defined in logger class'.format(methodName))
    
        # This method was inspired by the answers to Stack Overflow post
        # http://stackoverflow.com/q/2183233/2988730, especially
        # http://stackoverflow.com/a/13638084/2988730
        def logForLevel(self, message, *args, **kwargs):
            if self.isEnabledFor(levelNum):
                self._log(levelNum, message, args, **kwargs)
        def logToRoot(message, *args, **kwargs):
            logging.log(levelNum, message, *args, **kwargs)
    
        logging.addLevelName(levelNum, levelName)
        setattr(logging, levelName, levelNum)
        setattr(logging.getLoggerClass(), methodName, logForLevel)
        setattr(logging, methodName, logToRoot)
    

    您可以在我维护的实用程序库haggis 中找到更详细的实现。函数haggis.logs.add_logging_level 是这个答案的更适合生产的实现。

    【讨论】:

    • 将答案按Oldest 排序,您会发现这是所有答案中最好的答案!
    • @PeterDolan。让我知道您是否对此有任何疑问。在我的个人工具箱中,我有一个扩展版本,可让您配置如何处理冲突的级别定义。有一次我想到了这个,因为我喜欢添加一个 TRACE 级别,sphinx 的一个组件也是如此。
    • logForLevel 实现中,args 前面缺少星号是有意/必需的吗?
    • @突尼斯。这是无意的。谢谢你的收获。
    • @MattConway。真的,哈哈。这是自动驾驶仪,事后 3 年多。谢谢你的收获。 :) 我恢复到正确的版本。
    【解决方案2】:

    致 2022 年及以后阅读的人:您可能应该在此处查看当前评分第二高的答案:https://stackoverflow.com/a/35804945/1691778

    我的原始答案如下。

    --

    @Eric S.

    Eric S. 的回答非常好,但我通过实验了解到,这总是会导致打印在新调试级别记录的消息——无论日志级别设置为什么。所以如果你新建一个级别号9,如果你调用setLevel(50)下一级的消息会被错误的打印出来。

    为防止这种情况发生,您需要在“debugv”函数中添加另一行来检查是否实际启用了相关的日志记录级别。

    检查日志级别是否启用的固定示例:

    import logging
    DEBUG_LEVELV_NUM = 9 
    logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
    def debugv(self, message, *args, **kws):
        if self.isEnabledFor(DEBUG_LEVELV_NUM):
            # Yes, logger takes its '*args' as 'args'.
            self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
    logging.Logger.debugv = debugv
    

    如果您查看 Python 2.7 的 logging.__init__.py 中的 class Logger 代码,这就是所有标准日志函数(.critical、.debug 等)所做的。

    显然,由于缺乏声誉,我无法回复其他人的答案...希望 Eric 看到此消息后会更新他的帖子。 =)

    【讨论】:

    • 这是更好的答案,因为它正确地检查了日志级别。
    • 肯定比当前答案提供更多信息。
    • @pfa 添加logging.DEBUG_LEVEL_NUM = 9 这样您就可以在您在代码中导入记录器的任何地方访问该调试级别?
    • 绝对是DEBUG_LEVEL_NUM = 9,你应该定义logging.DEBUG_LEVEL_NUM = 9。这样您就可以像使用正确的知道 logging.DEBUGlogging.INFO 一样使用 log_instance.setLevel(logging.DEBUG_LEVEL_NUM)
    • 因为这是一个添加到现有类的运行时定义,你知道是否有办法让 Intellij / Pycharm 发现并允许自动完成?
    【解决方案3】:

    有人可能想做,根级别的自定义日志;并避免使用 logging.get_logger(''):

    import logging
    from datetime import datetime
    c_now=datetime.now()
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] :: %(message)s",
        handlers=[
            logging.StreamHandler(),
            logging.FileHandler("../logs/log_file_{}-{}-{}-{}.log".format(c_now.year,c_now.month,c_now.day,c_now.hour))
        ]
    )
    DEBUG_LEVELV_NUM = 99 
    logging.addLevelName(DEBUG_LEVELV_NUM, "CUSTOM")
    def custom_level(message, *args, **kws):
        logging.Logger._log(logging.root,DEBUG_LEVELV_NUM, message, args, **kws) 
    logging.custom_level = custom_level
    # --- --- --- --- 
    logging.custom_level("Waka")
    

    【讨论】:

      【解决方案4】:

      我接受了avoid seeing "lambda" 的答案,不得不修改添加log_at_my_log_level 的位置。我也看到了保罗所做的问题——我认为这行不通。你不需要 logger 作为log_at_my_log_level 中的第一个参数吗?这对我有用

      import logging
      DEBUG_LEVELV_NUM = 9 
      logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
      def debugv(self, message, *args, **kws):
          # Yes, logger takes its '*args' as 'args'.
          self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
      logging.Logger.debugv = debugv
      

      【讨论】:

      • +1。一种优雅的方法,而且效果很好。重要提示:您只需在单个模块中执行此操作一次,它将适用于所有模块。您甚至不必导入“设置”模块。所以把它扔进一个包裹的__init__.py,然后开心就好:D
      • @Eric S. 你应该看看这个答案:stackoverflow.com/a/13638084/600110
      • 我同意@SamMussmann。我错过了那个答案,因为这是票数最高的答案。
      • @Eric S. 为什么需要没有 * 的参数?如果我这样做,我会得到 TypeError: not all arguments converted during string formatting 但它适用于 *. (Python 3.4.3)。是python版本问题,还是我遗漏了什么?
      • 这个答案对我不起作用。尝试执行“logging.debugv”会出现错误AttributeError: module 'logging' has no attribute 'debugv'
      【解决方案5】:

      虽然我们已经有了很多正确答案,但在我看来,下面的答案更符合 Python 风格:

      import logging
      
      from functools import partial, partialmethod
      
      logging.TRACE = 5
      logging.addLevelName(logging.TRACE, 'TRACE')
      logging.Logger.trace = partialmethod(logging.Logger.log, logging.TRACE)
      logging.trace = partial(logging.log, logging.TRACE)
      

      如果你想在你的代码上使用mypy,建议添加# type: ignore来禁止添加属性的警告。

      【讨论】:

      • 看起来不错,但最后一行令人困惑。不应该是logging.trace = partial(logging.log, logging.TRACE) # type: ignore吗?
      • @SergeyNudnov 感谢您的指出,我已修复它。对我来说是一个错误,我只是从我的代码中复制并且显然搞砸了清洁。
      【解决方案6】:

      基于固定答案, 我写了一个自动创建新日志级别的小方法

      def set_custom_logging_levels(config={}):
          """
              Assign custom levels for logging
                  config: is a dict, like
                  {
                      'EVENT_NAME': EVENT_LEVEL_NUM,
                  }
              EVENT_LEVEL_NUM can't be like already has logging module
              logging.DEBUG       = 10
              logging.INFO        = 20
              logging.WARNING     = 30
              logging.ERROR       = 40
              logging.CRITICAL    = 50
          """
          assert isinstance(config, dict), "Configuration must be a dict"
      
          def get_level_func(level_name, level_num):
              def _blank(self, message, *args, **kws):
                  if self.isEnabledFor(level_num):
                      # Yes, logger takes its '*args' as 'args'.
                      self._log(level_num, message, args, **kws) 
              _blank.__name__ = level_name.lower()
              return _blank
      
          for level_name, level_num in config.items():
              logging.addLevelName(level_num, level_name.upper())
              setattr(logging.Logger, level_name.lower(), get_level_func(level_name, level_num))
      
      

      配置可能是这样的:

      new_log_levels = {
          # level_num is in logging.INFO section, that's why it 21, 22, etc..
          "FOO":      21,
          "BAR":      22,
      }
      

      【讨论】:

        【解决方案7】:

        如果有人想要一种自动化的方式来动态地向日志模块(或它的副本)添加新的日志级别,我已经创建了这个函数,扩展了@pfa 的答案:

        def add_level(log_name,custom_log_module=None,log_num=None,
                        log_call=None,
                           lower_than=None, higher_than=None, same_as=None,
                      verbose=True):
            '''
            Function to dynamically add a new log level to a given custom logging module.
            <custom_log_module>: the logging module. If not provided, then a copy of
                <logging> module is used
            <log_name>: the logging level name
            <log_num>: the logging level num. If not provided, then function checks
                <lower_than>,<higher_than> and <same_as>, at the order mentioned.
                One of those three parameters must hold a string of an already existent
                logging level name.
            In case a level is overwritten and <verbose> is True, then a message in WARNING
                level of the custom logging module is established.
            '''
            if custom_log_module is None:
                import imp
                custom_log_module = imp.load_module('custom_log_module',
                                                    *imp.find_module('logging'))
            log_name = log_name.upper()
            def cust_log(par, message, *args, **kws):
                # Yes, logger takes its '*args' as 'args'.
                if par.isEnabledFor(log_num):
                    par._log(log_num, message, args, **kws)
            available_level_nums = [key for key in custom_log_module._levelNames
                                    if isinstance(key,int)]
        
            available_levels = {key:custom_log_module._levelNames[key]
                                     for key in custom_log_module._levelNames
                                    if isinstance(key,str)}
            if log_num is None:
                try:
                    if lower_than is not None:
                        log_num = available_levels[lower_than]-1
                    elif higher_than is not None:
                        log_num = available_levels[higher_than]+1
                    elif same_as is not None:
                        log_num = available_levels[higher_than]
                    else:
                        raise Exception('Infomation about the '+
                                        'log_num should be provided')
                except KeyError:
                    raise Exception('Non existent logging level name')
            if log_num in available_level_nums and verbose:
                custom_log_module.warn('Changing ' +
                                          custom_log_module._levelNames[log_num] +
                                          ' to '+log_name)
            custom_log_module.addLevelName(log_num, log_name)
        
            if log_call is None:
                log_call = log_name.lower()
        
            setattr(custom_log_module.Logger, log_call, cust_log)
            return custom_log_module
        

        【讨论】:

        • 执行内部的评估。哇。
        • ..... 不知道是什么让我这样做了.... 几个月后,我很乐意将此声明与setattr 互换...
        【解决方案8】:

        我很困惑;至少使用 python 3.5,它可以正常工作:

        import logging
        
        
        TRACE = 5
        """more detail than debug"""
        
        logging.basicConfig()
        logging.addLevelName(TRACE,"TRACE")
        logger = logging.getLogger('')
        logger.debug("n")
        logger.setLevel(logging.DEBUG)
        logger.debug("y1")
        logger.log(TRACE,"n")
        logger.setLevel(TRACE)
        logger.log(TRACE,"y2")
            
        

        输出:

        调试:root:y1

        TRACE:root:y2

        【讨论】:

        • 这不会让你做logger.trace('hi') 我相信这是主要目标
        【解决方案9】:

        添加到 Mad Physicists 示例以获得正确的文件名和行号:

        def logToRoot(message, *args, **kwargs):
            if logging.root.isEnabledFor(levelNum):
                logging.root._log(levelNum, message, args, **kwargs)
        

        【讨论】:

          【解决方案10】:

          创建自定义记录器的提示:

          1. 不要使用_log,使用log(你不必检查isEnabledFor
          2. 日志模块应该是自定义记录器的一个创建实例,因为它在getLogger 中发挥了一些作用,因此您需要通过setLoggerClass 设置类
          3. 如果您不存储任何内容,则无需为记录器、类定义 __init__
          # Lower than debug which is 10
          TRACE = 5
          class MyLogger(logging.Logger):
              def trace(self, msg, *args, **kwargs):
                  self.log(TRACE, msg, *args, **kwargs)
          

          调用此记录器时,使用setLoggerClass(MyLogger) 使其成为getLogger 的默认记录器

          logging.setLoggerClass(MyLogger)
          log = logging.getLogger(__name__)
          # ...
          log.trace("something specific")
          

          您需要在handlerlog 上使用setFormattersetHandlersetLevel(TRACE) 来实际查看此低级别跟踪

          【讨论】:

            【解决方案11】:

            这个问题相当老了,但我只是处理了同样的话题,并找到了一种与已经提到的方法相似的方法,这对我来说似乎更干净一些。这是在 3.4 上测试的,所以我不确定使用的方法是否存在于旧版本中:

            from logging import getLoggerClass, addLevelName, setLoggerClass, NOTSET
            
            VERBOSE = 5
            
            class MyLogger(getLoggerClass()):
                def __init__(self, name, level=NOTSET):
                    super().__init__(name, level)
            
                    addLevelName(VERBOSE, "VERBOSE")
            
                def verbose(self, msg, *args, **kwargs):
                    if self.isEnabledFor(VERBOSE):
                        self._log(VERBOSE, msg, args, **kwargs)
            
            setLoggerClass(MyLogger)
            

            【讨论】:

            • 这是恕我直言最好的答案,因为它避免了猴子修补。 getsetLoggerClass 究竟做了什么以及为什么需要它们?
            • @MarcoSulla 它们被记录为 Python 日志记录模块的一部分。我假设动态子类化是在有人在使用这个库时想要他们自己的 llogger 的情况下使用的。然后这个 MyLogger 将成为我的类的子类,将两者结合起来。
            • 这与this discussion 中提出的解决方案非常相似,即是否在默认日志库中添加TRACE 级别。 +1
            【解决方案12】:

            作为向 Logger 类添加额外方法的替代方法,我建议使用 Logger.log(level, msg) 方法。

            import logging
            
            TRACE = 5
            logging.addLevelName(TRACE, 'TRACE')
            FORMAT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
            
            
            logging.basicConfig(format=FORMAT)
            l = logging.getLogger()
            l.setLevel(TRACE)
            l.log(TRACE, 'trace message')
            l.setLevel(logging.DEBUG)
            l.log(TRACE, 'disabled trace message')
            

            【讨论】:

              【解决方案13】:

              谁开始使用内部方法的不良做法(self._log),为什么每个答案都基于此?! pythonic 的解决方案是改用self.log,这样你就不必弄乱任何内部的东西:

              import logging
              
              SUBDEBUG = 5
              logging.addLevelName(SUBDEBUG, 'SUBDEBUG')
              
              def subdebug(self, message, *args, **kws):
                  self.log(SUBDEBUG, message, *args, **kws) 
              logging.Logger.subdebug = subdebug
              
              logging.basicConfig()
              l = logging.getLogger()
              l.setLevel(SUBDEBUG)
              l.subdebug('test')
              l.setLevel(logging.DEBUG)
              l.subdebug('test')
              

              【讨论】:

              • 需要使用 _log() 而不是 log() 以避免在调用堆栈中引入额外的级别。如果使用 log(),额外堆栈帧的引入会导致多个 LogRecord 属性(funcName、lineno、文件名、路径名...)指向调试函数而不是实际调用者。这可能不是预期的结果。
              • 什么时候不允许调用类自己的内部方法了?仅仅因为函数是在类之外定义的,并不意味着它是一个外部方法。
              • 此方法不仅不必要地更改堆栈跟踪,而且不会检查是否记录了正确的级别。
              • 我觉得,@schlamar 说的是对的,但相反的原因得到了相同的票数。那么用什么?
              • 为什么不使用内部方法?
              【解决方案14】:

              这对我有用:

              import logging
              logging.basicConfig(
                  format='  %(levelname)-8.8s %(funcName)s: %(message)s',
              )
              logging.NOTE = 32  # positive yet important
              logging.addLevelName(logging.NOTE, 'NOTE')      # new level
              logging.addLevelName(logging.CRITICAL, 'FATAL') # rename existing
              
              log = logging.getLogger(__name__)
              log.note = lambda msg, *args: log._log(logging.NOTE, msg, args)
              log.note('school\'s out for summer! %s', 'dude')
              log.fatal('file not found.')
              

              正如@marqueed 所指出的, logger._log 解决了 lambda/funcName 问题。我认为使用 lambda 看起来更干净一些,但缺点是它不能接受关键字参数。我自己从来没有用过,所以没什么大不了的。

              注意设置:学校放暑假了!老兄 致命设置:找不到文件。

              【讨论】:

                【解决方案15】:

                根据我的经验,这是操作问题的完整解决方案...为了避免将“lambda”视为发出消息的函数,请更深入:

                MY_LEVEL_NUM = 25
                logging.addLevelName(MY_LEVEL_NUM, "MY_LEVEL_NAME")
                def log_at_my_log_level(self, message, *args, **kws):
                    # Yes, logger takes its '*args' as 'args'.
                    self._log(MY_LEVEL_NUM, message, args, **kws)
                logger.log_at_my_log_level = log_at_my_log_level
                

                我从未尝试过使用独立的记录器类,但我认为基本思想是相同的(使用 _log)。

                【讨论】:

                • 我认为这行不通。你不需要logger 作为log_at_my_log_level 中的第一个参数吗?
                • 是的,我想你可能会。此答案改编自解决略有不同问题的代码。
                【解决方案16】:

                我发现为传递 log() 函数的记录器对象创建新属性更容易。我认为 logger 模块正是出于这个原因提供了 addLevelName() 和 log() 。因此不需要子类或新方法。

                import logging
                
                @property
                def log(obj):
                    logging.addLevelName(5, 'TRACE')
                    myLogger = logging.getLogger(obj.__class__.__name__)
                    setattr(myLogger, 'trace', lambda *args: myLogger.log(5, *args))
                    return myLogger
                

                现在

                mylogger.trace('This is a trace message')
                

                应该按预期工作。

                【讨论】:

                • 与子类化相比,这不会对性能造成很小的影响吗?使用这种方法,每次有人请求记录器时,他们都必须进行 setattr 调用。您可能会将它们包装在一个自定义类中,但无论如何,必须在创建的每个记录器上调用 setattr,对吗?
                • @Zbigniew 下面表示这不起作用,我认为这是因为您的记录器需要调用_log,而不是log
                【解决方案17】:

                我认为您必须继承Logger 类并添加一个名为trace 的方法,该方法基本上调用Logger.log,其级别低于DEBUG。我还没有尝试过,但这就是docs indicate

                【讨论】:

                • 你可能想要替换 logging.getLogger 来返回你的子类而不是内置类。
                • @S.Lott - 实际上(至少对于当前版本的 Python,2010 年可能不是这种情况)您必须使用 setLoggerClass(MyClass),然后像往常一样调用 getLogger()。 ..
                • IMO,这是迄今为止最好的(也是最 Pythonic)的答案,如果我能给它多个 +1,我会的。执行起来很简单,但是示例代码会很好。 :-D
                • @DougR.Thanks 但就像我说的,我还没有尝试过。 :)
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2018-11-21
                • 1970-01-01
                • 1970-01-01
                • 2015-12-15
                • 1970-01-01
                • 1970-01-01
                • 2016-02-20
                相关资源
                最近更新 更多