Jak dodać niestandardowy loglevel do narzędzia logowania Pythona

116

Chciałbym mieć loglevel TRACE (5) dla mojej aplikacji, ponieważ uważam, że debug()to nie wystarczy. Poza log(5, msg)tym nie jest tym, czego chcę. Jak mogę dodać niestandardowy loglevel do programu rejestrującego Python?

Mam mylogger.pynastępującą treść:

import logging

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

W swoim 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ękuje za twoją pomoc.

Edycja (8 grudnia 2016 r.): Zmieniłem zaakceptowaną odpowiedź na pfa's, czyli IMHO, doskonałe rozwiązanie oparte na bardzo dobrej propozycji Erica S.

tuergeist
źródło

Odpowiedzi:

171

@Eric S.

Odpowiedź Erica S. jest doskonała, ale eksperymentując nauczyłem się, że zawsze spowoduje to, że komunikaty rejestrowane na nowym poziomie debugowania będą drukowane - niezależnie od ustawionego poziomu dziennika. Więc jeśli utworzysz nowy numer poziomu 9, jeśli zadzwonisz setLevel(50), komunikaty niższego poziomu zostaną błędnie wydrukowane.

Aby temu zapobiec, potrzebujesz innego wiersza w funkcji „debugv”, aby sprawdzić, czy dany poziom rejestrowania jest rzeczywiście włączony.

Naprawiono przykład, który sprawdza, 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 Loggerw logging.__init__.pyPythonie 2.7, to właśnie robią wszystkie standardowe funkcje dziennika (.critical, .debug 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. =)

pfa
źródło
7
To jest lepsza odpowiedź, ponieważ poprawnie sprawdza poziom dziennika.
Colonel Panic
2
Z pewnością dużo więcej informacji niż obecna odpowiedź.
Mad Physicist
4
@pfa A co z dodawaniem, logging.DEBUG_LEVEL_NUM = 9aby mieć dostęp do tego poziomu debugowania wszędzie tam, gdzie importujesz rejestrator w swoim kodzie?
edgarstack
4
Zdecydowanie zamiast tego DEBUG_LEVEL_NUM = 9powinieneś zdefiniować logging.DEBUG_LEVEL_NUM = 9. W ten sposób będziesz mógł korzystać w log_instance.setLevel(logging.DEBUG_LEVEL_NUM)ten sam sposób, w jaki korzystałeś z right know logging.DEBUGlublogging.INFO
maQ
Ta odpowiedź była bardzo pomocna. Dziękuję pfa i EricS. Chciałbym zasugerować, aby dla kompletności zawrzeć jeszcze dwa stwierdzenia: logging.DEBUGV = DEBUG_LEVELV_NUMi logging.__all__ += ['DEBUGV'] Drugie nie jest strasznie ważne, ale pierwsze jest konieczne, jeśli masz jakiś kod, który dynamicznie dostosowuje poziom logowania i chcesz mieć możliwość zrobienia czegoś takiego jak if verbose: logger.setLevel(logging.DEBUGV)`
Keith Hanlan,
63

Przyjąłem odpowiedź „unikaj wyświetlania lambda” i musiałem zmodyfikować miejsce dodawania log_at_my_log_level. Widziałem również problem, który zrobił Paul "Nie sądzę, że to działa. Czy nie potrzebujesz loggera jako pierwszego argumentu 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
Eric S.
źródło
7
+1 też. Eleganckie podejście i działało idealnie. Ważna uwaga: wystarczy to zrobić tylko raz, w jednym module, a zadziała to dla wszystkich modułów . Nie musisz nawet importować modułu „setup”. Więc wrzuć to do paczki __init__.pyi bądź szczęśliwy: D
MestreLion
4
@Eric S. Powinieneś spojrzeć na tę odpowiedź: stackoverflow.com/a/13638084/600110
Sam Mussmann
1
Zgadzam się z @SamMussmann. Brakowało mi tej odpowiedzi, ponieważ była to najczęściej głosowana odpowiedź.
Colonel Panic
@Eric S. Dlaczego potrzebujesz argumentów bez *? Jeśli to zrobię, otrzymam, TypeError: not all arguments converted during string formattingale działa dobrze z *. (Python 3.4.3). Czy jest to problem z wersją Pythona, czy coś, czego mi brakuje?
Peter
Ta odpowiedź nie działa dla mnie. Próba wykonania „logging.debugv” powoduje błądAttributeError: module 'logging' has no attribute 'debugv'
Alex
51

Łącząc wszystkie istniejące odpowiedzi z wieloma doświadczeniami dotyczącymi użytkowania, myślę, że opracował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 TRACEz wartością logging.DEBUG - 5 == 5:

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') musi zostać wywołane, aby nowy poziom został zarejestrowany wewnętrznie, tak aby można było do niego odwoływać się za pomocą nazwy.
  2. Nowy poziom musi zostać dodany jako atrybut do loggingsiebie o konsystencji: logging.TRACE = logging.DEBUG - 5.
  3. Wywołaną metodę tracenależy dodać do loggingmodułu. Należy zachowywać się jak debug, infoitp
  4. Wywołaną metodę tracenależy dodać do aktualnie skonfigurowanej klasy programu rejestrującego. Ponieważ nie jest to w 100% gwarantowane logging.Logger, użyj logging.getLoggerClass()zamiast tego.

Wszystkie kroki ilustruje poniższa metoda:

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)
Szalony Fizyk
źródło
Sortuj odpowiedzi według Oldest, a z pewnością docenisz, że jest to najlepsza odpowiedź ze wszystkich!
Serge Stroobandt
Dzięki. Wykonałem sporo pracy, układając razem coś takiego, a ta kontrola jakości była bardzo pomocna, więc próbowałem coś dodać.
Mad Physicist
1
@PeterDolan. Daj mi znać, jeśli masz z tym problem. W moim osobistym zestawie narzędzi mam wersję rozszerzoną, która pozwala skonfigurować sposób obsługi sprzecznych definicji poziomów. Przyszło mi to kiedyś, ponieważ lubię dodawać poziom TRACE, podobnie jak jeden ze składników sfinksa.
Mad Physicist
1
Jest brak gwiazdką z przodu argsw logForLevelrealizacji umyślne / wymagane?
Chris L. Barnes
1
@Tunezja. To niezamierzone. Dzięki za połów.
Mad Physicist
40

To dość stare pytanie, ale zająłem się tylko tym samym tematem i znalazłem sposób podobny do tych, o których już wspomniałem, który wydaje mi się nieco czystszy. Zostało to przetestowane na 3.4, więc nie jestem pewien, czy użyte 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)
Wisperwind
źródło
1
To najlepsza odpowiedź IMHO, ponieważ pozwala uniknąć małpiego łatania. Co geti setLoggerClassdokładnie robić i dlaczego są potrzebne?
Marco Sulla
3
@MarcoSulla Są one udokumentowane jako część modułu logowania Pythona. Zakładam, że dynamiczna podklasa jest używana na wypadek, gdyby ktoś chciał mieć własnego llogera podczas korzystania z tej biblioteki. Ten MyLogger stałby się wówczas podklasą mojej klasy, łącząc te dwie.
CrackerJack9
Jest to bardzo podobne do rozwiązania przedstawionego w tej dyskusji, czy dodać TRACEpoziom do domyślnej biblioteki rejestrowania. +1
IMP1
18

Kto rozpoczął złą praktykę korzystania z metod wewnętrznych ( self._log) i dlaczego każda odpowiedź jest oparta na tym ?! Rozwiązaniem w Pythonie byłoby użycie self.logzamiast tego, więc nie musisz majstrować przy żadnych wewnętrznych rzeczach:

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')
schlamar
źródło
18
Użycie _log () zamiast log () jest potrzebne, aby uniknąć wprowadzania dodatkowego poziomu w stosie wywołań. Jeśli używana jest funkcja log (), wprowadzenie dodatkowej ramki stosu powoduje, że kilka atrybutów LogRecord (funcName, lineno, filename, pathname, ...) wskazuje funkcję debugowania zamiast faktycznego obiektu wywołującego. To prawdopodobnie nie jest pożądany rezultat.
rivy
5
Od kiedy wywoływanie własnych metod wewnętrznych klasy jest niedozwolone? To, że funkcja jest zdefiniowana poza klasą, nie oznacza, że ​​jest to metoda zewnętrzna.
OozeMeister
3
Ta metoda nie tylko niepotrzebnie zmienia ślad stosu, ale także nie sprawdza, czy rejestrowany jest prawidłowy poziom.
Mad Physicist
Wydaje mi się, że to, co mówi @schlamar, jest słuszne, ale przeciwny powód otrzymał taką samą liczbę głosów. Więc czego użyć?
Sumit Murari
1
Dlaczego metoda nie miałaby używać metody wewnętrznej?
Gringo Suave
9

Łatwiej jest mi utworzyć nowy atrybut dla obiektu rejestrującego, który przekazuje funkcję log (). Myślę, że moduł rejestrujący zapewnia addLevelName () i log () z tego właśnie powodu. Dlatego nie są potrzebne żadne 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')

powinien działać zgodnie z oczekiwaniami.

LtPinback
źródło
Czy nie miałoby to niewielkiego wpływu na wydajność w porównaniu z podklasą? Przy takim podejściu za każdym razem, gdy ktoś poprosi o rejestrator, będzie musiał wykonać wywołanie setattr. Prawdopodobnie zapakowałbyś je razem w niestandardową klasę, ale mimo to setattr musi być wywoływany w każdym utworzonym loggerze, prawda?
Matthew Lund,
@Zbigniew poniżej wskazał, że to nie zadziałało, co moim zdaniem wynika z tego, że Twój rejestrator musi wykonać swoje wywołanie _log, a nie log.
Marqueed
9

Chociaż mamy już wiele poprawnych odpowiedzi, to moim zdaniem bardziej pytoniczne:

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ć mypyw swoim kodzie, zaleca się dodanie, # type: ignoreaby pomijać ostrzeżenia przed dodawaniem atrybutu.

DerWeh
źródło
1
Wygląda świetnie, ale ostatnia linijka jest zagmatwana. Nie powinno być logging.trace = partial(logging.log, logging.TRACE) # type: ignore?
Sergey Nudnov
@SergeyNudnov dzięki za wskazanie, naprawiłem to. To był błąd z mojej strony, po prostu skopiowałem kod i najwyraźniej zepsułem czyszczenie.
DerWeh
8

Myślę, że będziesz musiał podklasować Loggerklasę i dodać metodę o nazwie, tracektóra w zasadzie wywołuje Logger.logpoziom niższy niż DEBUG. Nie próbowałem tego, ale to jest to, co wskazują doktorzy .

Noufal Ibrahim
źródło
3
Prawdopodobnie będziesz chciał zamienić, logging.getLoggeraby zwrócić swoją podklasę zamiast klasy wbudowanej.
S.Lott
4
@ S.Lott - Właściwie (przynajmniej z obecną wersją Pythona, może nie było w 2010 roku) musisz użyć, setLoggerClass(MyClass)a potem zadzwonić getLogger()jak zwykle ...
mac
IMO, to jest zdecydowanie najlepsza (i najbardziej Pythonowa) odpowiedź, a gdybym mógł dać jej wiele + 1, zrobiłbym to. Jest łatwy do wykonania, ale przykładowy kod byłby fajny. :-D
Doug R.
@ DougR.Dzięki, ale jak powiedziałem, nie próbowałem tego. :)
Noufal Ibrahim
6

Wskazówki dotyczące tworzenia niestandardowego rejestratora:

  1. Nie używaj _log, używaj log(nie musisz sprawdzać isEnabledFor)
  2. moduł logowania powinien być tym, który tworzy instancję niestandardowego rejestratora, ponieważ robi trochę magii getLogger, więc będziesz musiał ustawić klasę za pomocąsetLoggerClass
  3. Nie musisz definiować __init__dla loggera klasy, jeśli nic 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 ustawić go jako domyślny rejestratorgetLogger

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

Trzeba będzie setFormatter, setHandleri setLevel(TRACE)na handleri na logsam faktycznie SE Ten niski poziom śladu

Bryce Guinta
źródło
3

To zadziałało dla mnie:

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 lambda / funcName został naprawiony w logger._log, jak wskazał @marqueed. Wydaje mi się, że użycie lambdy wygląda na nieco czystszą, ale wadą jest to, że nie może przyjmować argumentów słów kluczowych. Sam nigdy z tego nie korzystałem, więc nie ma co.

  UWAGA: szkoła się kończy lato! koleś
  KRYTYCZNA konfiguracja: nie znaleziono pliku.
Gringo Suave
źródło
2

Z mojego doświadczenia wynika, że ​​jest to pełne rozwiązanie problemu operacji ... aby uniknąć postrzegania "lambda" jako funkcji, w której emitowany jest komunikat, 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ą rejestratora, ale myślę, że podstawowa idea jest taka sama (użyj _log).

markiza
źródło
Myślę, że to nie działa. Czy nie potrzebujesz loggerjako pierwszego argumentu log_at_my_log_level?
Paweł
Tak, myślę, że prawdopodobnie tak. Ta odpowiedź została zaczerpnięta z kodu, który rozwiązuje nieco inny problem.
Marqueed
2

Dodatek do przykładu Mad Physicists, 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)
Frederik Holljen
źródło
1

Bazując na 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 coś takiego:

new_log_levels = {
    # level_num is in logging.INFO section, that's why it 21, 22, etc..
    "FOO":      21,
    "BAR":      22,
}
groshevpavel
źródło
0

Jako alternatywę dla dodania dodatkowej metody do klasy Logger polecam użycie Logger.log(level, msg)metody.

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')
schlamar
źródło
0

Jestem zmieszany; przynajmniej z Pythonem 3.5 po prostu 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")
    

wynik:

DEBUG: root: y1

TRACE: root: y2

gerardw
źródło
1
To nie pozwala ci zrobić tego, logger.trace('hi')co moim zdaniem jest głównym celem
Ultimation
-3

W przypadku, gdyby ktoś chciał zautomatyzować sposób dynamicznego dodawania nowego poziomu logowania do modułu logowania (lub jego kopii), utworzyłem tę funkcję, rozszerzają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
Vasilis Lemonidis
źródło
1
Eval inside exec. Łał.
Mad Physicist
2
..... nie wiem, co sprawiło, że to zrobiłem .... po tylu miesiącach z radością zamieniłbym to stwierdzenie na setattrzamiast tego ...
Vasilis Lemonidis