Jak dodać niestandardowy poziom logowania do funkcji logowania Pythona

Chciałbym mieć LogLevel TRACE (5) dla mojej aplikacji, ponieważ nie sądzę, że debug() jest wystarczająca. Dodatkowo log(5, msg) nie jest tym, czego chcę. Jak Mogę dodać niestandardowy poziom logowania do dziennika Pythona?

Mam {[4] } o następującej treści:

import logging

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

W moim kodzie używam go w następujący sposób:

class ExampleClass(object):
    from mylogger import log

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

Teraz chciałbym zadzwonić self.log.trace("foo bar")

Z góry dziękuję za pomoc.

Edit (Dec 8th 2016): zmieniłem zaakceptowaną odpowiedź na pfa czyli, IMHO, doskonałe rozwiązanie oparte na bardzo dobrej propozycji Erica s.

Author: DennisLi, 2010-02-02

16 answers

@Eric S.

Odpowiedź Erica S. jest doskonała, ale nauczyłem się przez eksperymenty, że zawsze spowoduje to wyświetlenie wiadomości rejestrowanych na nowym poziomie debugowania-niezależnie od tego, na jaki poziom dziennika jest ustawiony. Więc jeśli stworzysz nowy numer poziomu 9, jeśli wywołasz setLevel(50), Wiadomości niższego poziomu zostaną błędnie wydrukowane.

Aby temu zapobiec, potrzebujesz innej linii wewnątrz funkcji "debugv", aby sprawdzić, czy dany poziom logowania jest faktycznie włączone.

Poprawiono przykład sprawdzający, czy poziom logowania jest włączony:

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

Jeśli spojrzysz na kod class Logger w logging.__init__.py dla Pythona 2.7, to właśnie robią wszystkie standardowe funkcje dziennika (.krytyczne,debugowanie itp.).

Najwyraźniej nie mogę publikować odpowiedzi na odpowiedzi innych z powodu braku reputacji... mam nadzieję, że Eric zaktualizuje swój post, jeśli to zobaczy. =)

 185
Author: pfa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-08-12 08:02:02

Wziąłem unikaj oglądania "lambda" odpowiedzi i musiał zmodyfikować miejsce dodania log_at_my_log_level. Ja też widziałem problem, który zrobił Paul-nie sądzę, że to działa. Nie potrzebujesz loggera jako pierwszego arg w log_at_my_log_level? to zadziałało dla mnie

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
 64
Author: Eric S.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-10-13 16:13:59

Łącząc wszystkie istniejące odpowiedzi z kilkoma doświadczeniami użytkowania, myślę, że wymyśliłem listę wszystkich rzeczy, które należy zrobić, aby zapewnić całkowicie bezproblemowe korzystanie z nowego poziomu. Poniższe kroki zakładają, że dodajesz nowy poziom TRACE o wartości logging.DEBUG - 5 == 5:

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') musi być wywołany, aby nowy poziom został wewnętrznie zarejestrowany, aby można było się do niego odwoływać po nazwie.
  2. nowy poziom musi być dodany jako atrybut do samego logging dla spójności: logging.TRACE = logging.DEBUG - 5.
  3. do modułu logging należy dodać metodę o nazwie trace. Powinien zachowywać się tak jak debug, info, itd.
  4. metoda o nazwie trace musi zostać dodana do aktualnie skonfigurowanej klasy logger. Ponieważ nie jest to w 100% gwarantowane logging.Logger, zamiast tego użyj logging.getLoggerClass().

Wszystkie kroki są zilustrowane w poniższej metodzie:

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)
 60
Author: Mad Physicist,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-03-10 19:14:44

To pytanie jest dość stare, ale właśnie zająłem się tym samym tematem i znalazłem sposób podobny do tych już wymienionych, który wydaje mi się trochę czystszy. To było testowane na 3.4, więc nie jestem pewien, czy stosowane metody istnieją w starszych wersjach:

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)
 42
Author: Wisperwind,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-23 02:07:19

Kto rozpoczął złą praktykę stosowania metod wewnętrznych (self._log) i dlaczego każda odpowiedź jest na tym oparta?! Rozwiązaniem pythonicznym byłoby użycie self.log zamiast tego, abyś nie musiał zadzierać z żadnymi wewnętrznymi rzeczami:

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')
 20
Author: schlamar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-06 06:27:25

Chociaż mamy już wiele poprawnych odpowiedzi, następujące są moim zdaniem bardziej pythoniczne:

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)

Jeśli chcesz użyć mypy na swoim kodzie, zaleca się dodanie # type: ignore, aby uniknąć ostrzeżenia przed dodaniem atrybutu.

 13
Author: DerWeh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-03-27 08:39:05

Myślę, że będziesz musiał podklasować klasę Logger i dodać metodę o nazwie trace, która w zasadzie wywołuje Logger.log z poziomem niższym niż DEBUG. Nie próbowałem tego, ale to jest to, co dokumenty wskazują .

 8
Author: Noufal Ibrahim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-02-02 10:27:50

Łatwiej jest mi utworzyć nowy atrybut dla obiektu logger, który przekazuje funkcję log (). Myślę, że moduł logger dostarcza addLevelName () i log () z tego właśnie powodu. W związku z tym nie są potrzebne podklasy ani nowa metoda.

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

Teraz

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

Powinno działać zgodnie z oczekiwaniami.

 8
Author: LtPinback,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-05-13 02:20:30

Wskazówki dotyczące tworzenia niestandardowego rejestratora:

  1. nie używaj _log, używaj log (nie musisz sprawdzać isEnabledFor)
  2. moduł logowania powinien być instancją tworzącą Niestandardowy logger, ponieważ robi to magicznie w getLogger, więc będziesz musiał ustawić klasę za pomocą setLoggerClass
  3. nie musisz definiować __init__ dla klasy logger, jeśli niczego nie przechowujesz
# Lower than debug which is 10
TRACE = 5
class MyLogger(logging.Logger):
    def trace(self, msg, *args, **kwargs):
        self.log(TRACE, msg, *args, **kwargs)

Podczas wywoływania tego rejestratora użyj setLoggerClass(MyLogger), aby uczynić go domyślnym rejestratorem z getLogger

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

Będziesz musiał setFormatter, setHandler, i setLevel(TRACE) na handler i na samym log, aby faktycznie se ten ślad niskiego poziomu

 6
Author: Bryce Guinta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-17 23:31:51

To mi pomogło:

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

Problem z lambda / funcName został rozwiązany za pomocą loggera._log jak zauważył @marqueed. Myślę, że korzystanie z lambda wygląda nieco czystsze, ale wadą jest to,że nie może przyjmować argumentów kluczowych. Nigdy tego nie używałem, więc nic wielkiego.

  NOTE     setup: school's out for summer! dude
  FATAL    setup: file not found.
 3
Author: Gringo Suave,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-21 06:24:39

Z mojego doświadczenia wynika, że jest to pełne rozwiązanie problemu operacji... aby uniknąć postrzegania "lambda" jako funkcji, w której emitowana jest wiadomość, wejdź głębiej:

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

Nigdy nie próbowałem pracować z samodzielną klasą loggera, ale myślę, że podstawowa idea jest taka sama(użyj _log).

 2
Author: marqueed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-10 01:46:58

Dodanie do przykładu Mad Physicians aby uzyskać poprawną nazwę pliku i numer linii:

def logToRoot(message, *args, **kwargs):
    if logging.root.isEnabledFor(levelNum):
        logging.root._log(levelNum, message, args, **kwargs)
 2
Author: Frederik Holljen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-21 12:17:40

Na podstawie przypiętej odpowiedzi, napisałem małą metodę, która automatycznie tworzy nowe poziomy logowania

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 może wyglądać tak:

new_log_levels = {
    # level_num is in logging.INFO section, that's why it 21, 22, etc..
    "FOO":      21,
    "BAR":      22,
}
 1
Author: groshevpavel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-01-11 13:36:02

Jako alternatywę dla dodania dodatkowej metody do klasy Logger polecam użycie metody 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')
 0
Author: schlamar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-30 13:40:33

Jestem zdezorientowany; z Pythonem 3.5 przynajmniej działa:

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

Wyjście:

DEBUG:root: y1

TRACE: root: y2

 0
Author: gerardw,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-20 09:12:55

Jeśli ktoś chce zautomatyzowanego sposobu dodawania nowego poziomu logowania do modułu logowania (lub jego kopii) dynamicznie, stworzyłem tę funkcję, rozwijając odpowiedź @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
 -3
Author: Vasilis Lemonidis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-11-30 15:06:55