简体   繁体   English

如何将自定义日志级别添加到 Python 的日志记录工具

[英]How to add a custom loglevel to Python's logging facility

I'd like to have loglevel TRACE (5) for my application, as I don't think that debug() is sufficient.我想为我的应用程序设置日志级别跟踪 (5),因为我认为debug()不够。 Additionally log(5, msg) isn't what I want.另外log(5, msg)不是我想要的。 How can I add a custom loglevel to a Python logger?如何将自定义日志级别添加到 Python 记录器?

I've a mylogger.py with the following content:我有一个包含以下内容的mylogger.py

import logging

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

In my code I use it in the following way:在我的代码中,我按以下方式使用它:

class ExampleClass(object):
    from mylogger import log

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

Now I'd like to call self.log.trace("foo bar")现在我想调用self.log.trace("foo bar")

Thanks in advance for your help.在此先感谢您的帮助。

Edit (Dec 8th 2016): I changed the accepted answer to pfa's which is, IMHO, an excellent solution based on the very good proposal from Eric S.编辑(2016 年 12 月 8 日):我将接受的答案更改为pfa ,恕我直言,这是一个基于 Eric S 的非常好的建议的出色解决方案。

@Eric S. @埃里克S。

Eric S.'s answer is excellent, but I learned by experimentation that this will always cause messages logged at the new debug level to be printed -- regardless of what the log level is set to. Eric S. 的回答非常好,但我通过实验了解到,这将始终导致打印在新调试级别记录的消息——无论日志级别设置为什么。 So if you make a new level number of 9 , if you call setLevel(50) , the lower level messages will erroneously be printed.因此,如果您将新级别编号设为9 ,如果您调用setLevel(50) ,则会错误地打印较低级别的消息。

To prevent that from happening, you need another line inside the "debugv" function to check if the logging level in question is actually enabled.为了防止这种情况发生,您需要在“debugv”函数中使用另一行来检查所讨论的日志记录级别是否实际启用。

Fixed example that checks if the logging level is enabled:修复了检查是否启用日志记录级别的示例:

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

If you look at the code for class Logger in logging.__init__.py for Python 2.7, this is what all the standard log functions do (.critical, .debug, etc.).如果您查看 Python 2.7 的logging.__init__.py class Logger的代码,这就是所有标准日志函数所做的(.critical、.debug 等)。

I apparently can't post replies to others' answers for lack of reputation... hopefully Eric will update his post if he sees this.由于缺乏声誉,我显然无法回复其他人的答案......希望 Eric 看到这个后会更新他的帖子。 =) =)

Combining all of the existing answers with a bunch of usage experience, I think that I have come up with a list of all the things that need to be done to ensure completely seamless usage of the new level.将所有现有答案与一堆使用经验相结合,我想我已经列出了需要完成的所有事情的列表,以确保完全无缝地使用新级别。 The steps below assume that you are adding a new level TRACE with value logging.DEBUG - 5 == 5 :以下步骤假设您正在添加一个新级别的TRACE ,其值为logging.DEBUG - 5 == 5

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') needs to be invoked to get the new level registered internally so that it can be referenced by name.需要调用logging.addLevelName(logging.DEBUG - 5, 'TRACE')来获取内部注册的新级别,以便可以通过名称引用它。
  2. The new level needs to be added as an attribute to logging itself for consistency: logging.TRACE = logging.DEBUG - 5 .需要将新级别作为属性添加到logging本身以保持一致性: logging.TRACE = logging.DEBUG - 5
  3. A method called trace needs to be added to the logging module.需要在logging模块中添加一个名为trace的方法。 It should behave just like debug , info , etc.它的行为应该像debuginfo等。
  4. A method called trace needs to be added to the currently configured logger class.需要在当前配置的记录器类中添加一个名为trace的方法。 Since this is not 100% guaranteed to be logging.Logger , use logging.getLoggerClass() instead.由于这不是 100% 保证是logging.Logger ,请改用logging.getLoggerClass()

All the steps are illustrated in the method below:所有步骤都在下面的方法中说明:

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)

I took the avoid seeing "lambda" answer and had to modify where the log_at_my_log_level was being added.我采取了避免看到“lambda”的答案,不得不修改添加log_at_my_log_level I too saw the problem that Paul did – I don't think this works.我也看到了保罗所做的问题——我认为这行不通。 Don't you need logger as the first arg in log_at_my_log_level ?您不需要 logger 作为log_at_my_log_level的第一个参数吗? This worked for me这对我有用

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

This question is rather old, but I just dealt with the same topic and found a way similiar to those already mentioned which appears a little cleaner to me.这个问题相当古老,但我只是处理了相同的主题,并找到了一种与已经提到的方法类似的方法,对我来说似乎更清晰一些。 This was tested on 3.4, so I'm not sure whether the methods used exist in older versions:这是在 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)

Who started the bad practice of using internal methods ( self._log ) and why is each answer based on that?!谁开始使用内部方法( self._log )的不良做法,为什么每个答案都基于此?! The pythonic solution would be to use self.log instead so you don't have to mess with any internal stuff: self.log解决方案是使用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')

While we have already plenty of correct answers, the following is in my opinion more pythonic:虽然我们已经有很多正确的答案,但在我看来,以下更像是 Pythonic:

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)

If you want to use mypy on your code, it is recommended to add # type: ignore to suppress warnings from adding attribute.如果你想在你的代码上使用mypy ,建议添加mypy # type: ignore以抑制添加属性的警告。

I think you'll have to subclass the Logger class and add a method called trace which basically calls Logger.log with a level lower than DEBUG .我认为您必须Logger类并添加一个名为trace的方法,该方法基本上调用Logger.log的级别低于DEBUG I haven't tried this but this is what the docs indicate .我没有试过这个,但这就是文档所表明的

I find it easier to create a new attribute for the logger object that passes the log() function.我发现为传递 log() 函数的记录器对象创建一个新属性更容易。 I think the logger module provides the addLevelName() and the log() for this very reason.我认为记录器模块正是因为这个原因提供了 addLevelName() 和 log() 。 Thus no subclasses or new method needed.因此不需要子类或新方法。

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

now现在

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

should work as expected.应该按预期工作。

Tips for creating a custom logger:创建自定义记录器的提示:

  1. Do not use _log , use log (you don't have to check isEnabledFor )不要使用_log ,使用log (你不必检查isEnabledFor
  2. the logging module should be the one creating instance of the custom logger since it does some magic in getLogger , so you will need to set the class via setLoggerClass日志记录模块应该是自定义记录器的一个创建实例,因为它在getLoggergetLogger一些getLogger ,因此您需要通过setLoggerClass设置该类
  3. You do not need to define __init__ for the logger, class if you are not storing anything如果您不存储任何内容,则无需为记录器和类定义__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)

When calling this logger use setLoggerClass(MyLogger) to make this the default logger from getLogger当我们使用这个记录仪使用setLoggerClass(MyLogger)使这个从默认记录getLogger

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

You will need to setFormatter , setHandler , and setLevel(TRACE) on the handler and on the log itself to actually se this low level trace您需要在handlerlog本身上设置setFormattersetHandlersetLevel(TRACE)以实际设置此低级别跟踪

This worked for me:这对我有用:

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.')

The lambda/funcName issue is fixed with logger._log as @marqueed pointed out.正如@marqueed 指出的那样,lambda/funcName 问题已通过 logger._log 修复。 I think using lambda looks a bit cleaner, but the drawback is that it can't take keyword arguments.我认为使用 lambda 看起来更简洁,但缺点是它不能接受关键字参数。 I've never used that myself, so no biggie.我自己从来没有用过,所以没什么大不了的。

NOTE     setup: school's out for summer! dude
  FATAL    setup: file not found.

Addition to Mad Physicists example to get file name and line number correct:除了 Mad Physicists 示例以获取正确的文件名和行号:

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

In my experience, this is the full solution the the op's problem... to avoid seeing "lambda" as the function in which the message is emitted, go deeper:根据我的经验,这是操作问题的完整解决方案......为了避免将“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

I've never tried working with a standalone logger class, but I think the basic idea is the same (use _log).我从未尝试过使用独立的记录器类,但我认为基本思想是相同的(使用 _log)。

based on pinned answer, i wrote a little method which automaticaly create new logging levels基于固定答案,我写了一个小方法,它自动创建新的日志记录级别

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))

config may smth like that:配置可能是这样的:

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

Someone might wanna do, a root level custom logging;有人可能想做,根级自定义日志记录; and avoid the usage of logging.get_logger(''):并避免使用 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")

As alternative to adding an extra method to the Logger class I would recommend using the Logger.log(level, msg) method.作为向 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')

I'm confused;我很困惑; with python 3.5, at least, it just works:至少使用 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")
    

output:输出:

DEBUG:root:y1调试:root:y1

TRACE:root:y2跟踪:根:y2

In case anyone wants an automated way to add a new logging level to the logging module (or a copy of it) dynamically, I have created this function, expanding @pfa's answer:如果有人想要一种自动的方式来动态地向日志记录模块(或其副本)添加新的日志记录级别,我已经创建了这个函数,扩展了@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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM