Come aggiungere un loglevel personalizzato alla funzione di registrazione di Python


116

Vorrei avere loglevel TRACE (5) per la mia applicazione, poiché non credo debug()sia sufficiente. Inoltre log(5, msg)non è quello che voglio. Come posso aggiungere un loglevel personalizzato a un logger Python?

Ho un mylogger.pycon il seguente contenuto:

import logging

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

Nel mio codice lo uso nel modo seguente:

class ExampleClass(object):
    from mylogger import log

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

Adesso vorrei chiamare self.log.trace("foo bar")

Grazie in anticipo per il vostro aiuto.

Modifica (8 dicembre 2016): ho cambiato la risposta accettata in pfa che è, IMHO, un'ottima soluzione basata sull'ottima proposta di Eric S.

Risposte:


171

@Eric S.

La risposta di Eric S. è eccellente, ma ho imparato dalla sperimentazione che ciò causerà sempre la stampa dei messaggi registrati al nuovo livello di debug, indipendentemente da ciò su cui è impostato il livello di log. Quindi, se si crea un nuovo numero di livello 9, se si chiama setLevel(50), i messaggi di livello inferiore verranno stampati erroneamente.

Per evitare che ciò accada, è necessaria un'altra riga all'interno della funzione "debugv" per verificare se il livello di registrazione in questione è effettivamente abilitato.

Esempio corretto che controlla se il livello di registrazione è abilitato:

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

Se si guarda il codice per class Loggerin logging.__init__.pyper Python 2.7, questo è ciò che tutte le funzioni di protocollo standard di do (.critical, .debug, etc.).

Apparentemente non posso pubblicare risposte alle risposte degli altri per mancanza di reputazione ... spero che Eric aggiorni il suo post se lo vede. =)


7
Questa è la risposta migliore perché controlla correttamente il livello di log.
Colonel Panic

2
Sicuramente molto più informativo della risposta attuale.
Mad Physicist,

4
@pfa Che ne dici di aggiungere in logging.DEBUG_LEVEL_NUM = 9modo da poter accedere a quel livello di debug ovunque importi il logger nel tuo codice?
edgarstack

4
Sicuramente invece DEBUG_LEVEL_NUM = 9dovresti definirlo logging.DEBUG_LEVEL_NUM = 9. In questo modo sarai in grado di usare log_instance.setLevel(logging.DEBUG_LEVEL_NUM)lo stesso modo in cui usi il giusto know logging.DEBUGologging.INFO
maQ

Questa risposta è stata molto utile. Grazie pfa e EricS. Vorrei suggerire che per completezza siano incluse altre due affermazioni: logging.DEBUGV = DEBUG_LEVELV_NUMe logging.__all__ += ['DEBUGV'] La seconda non è molto importante ma la prima è necessaria se hai del codice che regola dinamicamente il livello di registrazione e vuoi essere in grado di fare qualcosa come if verbose: logger.setLevel(logging.DEBUGV)`
Keith Hanlan

63

Ho preso la risposta "evita di vedere lambda" e ho dovuto modificare il punto in cui veniva aggiunto log_at_my_log_level. Anch'io ho visto il problema che Paul ha fatto "Non penso che funzioni. Non hai bisogno di logger come primo argomento in log_at_my_log_level?" Questo ha funzionato per 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

7
+1 anche. Un approccio elegante e ha funzionato perfettamente. Una nota importante: è necessario eseguire questa operazione solo una volta, in un singolo modulo, e funzionerà per tutti i moduli . Non è nemmeno necessario importare il modulo "setup". Quindi buttalo in un pacco __init__.pye sii felice: D
MestreLion

4
@Eric S. Dovresti dare un'occhiata a questa risposta: stackoverflow.com/a/13638084/600110
Sam Mussmann

1
Sono d'accordo con @SamMussmann. Mi mancava quella risposta perché questa era la risposta più votata.
Colonel Panic

@Eric S. Perché hai bisogno di argomenti senza *? Se lo faccio, ottengo TypeError: not all arguments converted during string formattingma funziona bene con *. (Python 3.4.3). È un problema della versione di Python o qualcosa che mi manca?
Peter

Questa risposta non funziona per me. Il tentativo di eseguire un "logging.debugv" restituisce un erroreAttributeError: module 'logging' has no attribute 'debugv'
Alex

51

Combinando tutte le risposte esistenti con un sacco di esperienza di utilizzo, penso di aver escogitato un elenco di tutte le cose che devono essere fatte per garantire un utilizzo completo del nuovo livello. I passaggi seguenti presuppongono che tu stia aggiungendo un nuovo livello TRACEcon valore logging.DEBUG - 5 == 5:

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') deve essere invocato per ottenere il nuovo livello registrato internamente in modo che possa essere referenziato per nome.
  2. Il nuovo livello deve essere aggiunto come attributo loggingse stesso per consistenza: logging.TRACE = logging.DEBUG - 5.
  3. Un metodo chiamato tracedeve essere aggiunto al loggingmodulo. Si dovrebbe comportarsi proprio come debug, infoecc
  4. Un metodo chiamato tracedeve essere aggiunto alla classe logger attualmente configurata. Poiché non è garantito al 100% logging.Logger, usa logging.getLoggerClass()invece.

Tutti i passaggi sono illustrati nel metodo seguente:

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)

Ordina le risposte per Oldeste apprezzerai che questa è la migliore risposta di tutte!
Serge Stroobandt

Grazie. Ho fatto un bel po 'di lavoro mettendo insieme qualcosa di simile e questo QA è stato molto utile, quindi ho provato ad aggiungere qualcosa.
Mad Physicist

1
@PeterDolan. Fammi sapere se hai problemi con questo. Nella mia cassetta degli attrezzi personale ho una versione estesa che ti consente di configurare come gestire le definizioni dei livelli in conflitto. Mi è venuto in mente una volta perché mi piace aggiungere un livello TRACE, e così fa uno dei componenti della sfinge.
Mad Physicist

1
È la mancanza di asterisco davanti argsnella logForLevelrealizzazione intenzionale / richiesta?
Chris L. Barnes il

1
@Tunisia. Non è intenzionale. Grazie per la cattura.
Mad Physicist il

40

Questa domanda è piuttosto vecchia, ma ho appena affrontato lo stesso argomento e ho trovato un modo simile a quelli già citati che mi sembra un po 'più pulito. Questo è stato testato su 3.4, quindi non sono sicuro che i metodi utilizzati esistano nelle versioni precedenti:

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)

1
Questa è IMHO la migliore risposta, poiché evita il patching delle scimmie. Cosa gete setLoggerClassesattamente e perché sono necessari?
Marco Sulla

3
@MarcoSulla Sono documentati come parte del modulo di registrazione di Python. La sottoclasse dinamica, presumo, viene utilizzata nel caso in cui qualcuno volesse il proprio llogger durante l'utilizzo di questa libreria. Questo MyLogger diventerebbe quindi una sottoclasse della mia classe, combinando i due.
CrackerJack9

Ciò è molto simile alla soluzione presentata in questa discussione sull'opportunità di aggiungere un TRACElivello alla libreria di registrazione predefinita. +1
IMP1

18

Chi ha iniziato la cattiva pratica di utilizzare metodi interni ( self._log) e perché ogni risposta è basata su questo ?! La soluzione pitonica sarebbe quella di usare self.loginvece in modo da non dover pasticciare con nessuna roba interna:

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

18
È necessario usare _log () invece di log () per evitare di introdurre un livello aggiuntivo nello stack di chiamate. Se si usa log (), l'introduzione dello stack frame aggiuntivo fa sì che diversi attributi LogRecord (funcName, lineno, filename, pathname, ...) puntino alla funzione di debug invece che al chiamante effettivo. Questo probabilmente non è il risultato desiderato.
Rivy

5
Da quando non è consentito chiamare i metodi interni di una classe? Solo perché la funzione è definita al di fuori della classe non significa che sia un metodo esterno.
OozeMeister

3
Questo metodo non solo altera inutilmente la traccia dello stack, ma non controlla nemmeno che venga registrato il livello corretto.
Mad Physicist,

Sento, quello che dice @schlamar è giusto, ma la ragione contraria ha ottenuto lo stesso numero di voti. Allora cosa usare?
Sumit Murari

1
Perché un metodo non dovrebbe utilizzare un metodo interno?
Gringo Suave

9

Trovo più semplice creare un nuovo attributo per l'oggetto logger che passa la funzione log (). Penso che il modulo logger fornisca addLevelName () e log () proprio per questo motivo. Quindi non sono necessarie sottoclassi o nuovi metodi.

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

adesso

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

dovrebbe funzionare come previsto.


Questo non avrebbe un piccolo calo in termini di prestazioni rispetto alla sottoclasse? Con questo approccio, ogni volta che alcuni richiedono un logger, dovranno effettuare la chiamata setattr. Probabilmente li raggrupperesti insieme in una classe personalizzata, ma comunque quel setattr deve essere chiamato su ogni logger creato, giusto?
Matthew Lund

@Zbigniew sotto ha indicato che questo non ha funzionato, il che penso sia dovuto al fatto che il tuo logger deve effettuare la sua chiamata a _log, no log.
marqueed

9

Anche se abbiamo già molte risposte corrette, il seguente è secondo me più pitonico:

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)

Se si desidera utilizzare mypysul codice, si consiglia di aggiungere # type: ignoreper eliminare gli avvisi dall'aggiunta di attributi.


1
Sembra fantastico, ma l'ultima riga è confusa. Non dovrebbe essere logging.trace = partial(logging.log, logging.TRACE) # type: ignore?
Sergey Nudnov

@SergeyNudnov grazie per averlo sottolineato, l'ho risolto. È stato un errore da parte mia, ho appena copiato dal mio codice e apparentemente ho incasinato la pulizia.
DerWeh

8

Penso che dovrai sottoclassare la Loggerclasse e aggiungere un metodo chiamato traceche sostanzialmente chiama Logger.logcon un livello inferiore a DEBUG. Non l'ho provato, ma questo è ciò che indicano i documenti .


3
E probabilmente vorrai sostituire logging.getLoggerper restituire la tua sottoclasse invece della classe incorporata.
S.Lott

4
@ S.Lott - In realtà (almeno con la versione attuale di Python, forse non era il caso nel 2010) devi usare setLoggerClass(MyClass)e poi chiamare getLogger()normalmente ...
mac

IMO, questa è di gran lunga la risposta migliore (e più pitonica), e se avessi potuto dargli più +1, l'avrei fatto. È semplice da eseguire, tuttavia il codice di esempio sarebbe stato carino. MrGreen
Doug R.

@ DougR.Thanks ma come ho detto, non l'ho provato. :)
Noufal Ibrahim

6

Suggerimenti per la creazione di un logger personalizzato:

  1. Non usare _log, usa log(non devi controllare isEnabledFor)
  2. il modulo di registrazione dovrebbe essere quello che crea l'istanza del logger personalizzato poiché fa un po 'di magia getLogger, quindi dovrai impostare la classe tramitesetLoggerClass
  3. Non è necessario definire __init__per il logger, class se non si memorizza nulla
# Lower than debug which is 10
TRACE = 5
class MyLogger(logging.Logger):
    def trace(self, msg, *args, **kwargs):
        self.log(TRACE, msg, *args, **kwargs)

Quando si chiama questo logger, usare setLoggerClass(MyLogger)per renderlo il logger predefinito dagetLogger

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

Avrai bisogno di setFormatter, setHandlere setLevel(TRACE)su handlere su logse stesso per vedere effettivamente questa traccia di basso livello


3

Questo ha funzionato per 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.')

Il problema lambda / funcName è stato risolto con logger._log come sottolineato da @marqueed. Penso che l'uso di lambda sembri un po 'più pulito, ma lo svantaggio è che non può accettare argomenti di parole chiave. Non l'ho mai usato io stesso, quindi nessun problema.

  NOTA setup: la scuola è finita per l'estate! tipo
  Installazione FATALE: file non trovato.

2

Nella mia esperienza, questa è la soluzione completa del problema dell'operazione ... per evitare di vedere "lambda" come la funzione in cui viene emesso il messaggio, vai più a fondo:

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

Non ho mai provato a lavorare con una classe logger autonoma, ma penso che l'idea di base sia la stessa (usa _log).


Non credo che funzioni. Non ti serve loggercome primo argomento in log_at_my_log_level?
Paul

Sì, penso che probabilmente lo faresti. Questa risposta è stata adattata dal codice che risolve un problema leggermente diverso.
tendone

2

Aggiunta all'esempio di Mad Physicists per ottenere il nome del file e il numero di riga corretti:

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

1

basato sulla risposta appuntata, ho scritto un piccolo metodo che crea automaticamente nuovi livelli di registrazione

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 potrebbe essere così:

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

0

In alternativa all'aggiunta di un metodo extra alla classe Logger, consiglierei di utilizzare il Logger.log(level, msg)metodo.

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

Non ho capito bene; con python 3.5, almeno, funziona:

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

produzione:

DEBUG: root: y1

TRACE: root: y2


1
Questo non ti consente di fare quello logger.trace('hi')che credo sia l'obiettivo principale
Ultimation

-3

Nel caso in cui qualcuno desideri un modo automatizzato per aggiungere un nuovo livello di registrazione al modulo di registrazione (o una copia di esso) dinamicamente, ho creato questa funzione, espandendo la risposta di @ 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

1
Eval all'interno di exec. Wow.
Fisico pazzo

2
..... non so cosa mi abbia spinto a fare questo .... dopo così tanti mesi scambierei felicemente questa affermazione con una setattrinvece ...
Vasilis Lemonidis
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.