Come posso aggiungere un campo personalizzato alla stringa del formato del registro Python?


96

La mia stringa di formato corrente è:

formatter = logging.Formatter('%(asctime)s : %(message)s')

e voglio aggiungere un nuovo campo chiamato app_nameche avrà un valore diverso in ogni script che contiene questo formattatore.

import logging
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.addHandler(syslog)

Ma non sono sicuro di come passare quel app_namevalore al logger per interpolare nella stringa di formato. Ovviamente posso farlo apparire nel messaggio di registro passandolo ogni volta, ma questo è disordinato.

Ho provato:

logging.info('Log message', app_name='myapp')
logging.info('Log message', {'app_name', 'myapp'})
logging.info('Log message', 'myapp')

ma nessuno funziona.


Vuoi davvero passare questo messaggio a ogni logchiamata? Se è così, guarda i documenti in cui dice "Questa funzionalità può essere utilizzata per iniettare i tuoi valori in un LogRecord ..." Ma questo sembra un ottimo caso per usarlo logger = logging.getLogger('myapp')e inserirlo nella logger.infochiamata.
abarnert

il logging di Python può già farlo afaik. se si utilizza un diverso loggeroggetto in ogni app, è possibile rendere ognuno utilizzare un nome diverso da un'istanza tuoi loggers in questo modo: logger = logging.getLogger(myAppName). nota che __name__è il nome del modulo python, quindi se ogni app è il proprio modulo python, funzionerebbe anche questo.
Florian Castellane

Risposte:


135

È possibile utilizzare un LoggerAdapter in modo da non dover passare le informazioni aggiuntive ad ogni chiamata di registrazione:

import logging
extra = {'app_name':'Super App'}

logger = logging.getLogger(__name__)
syslog = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(syslog)

logger = logging.LoggerAdapter(logger, extra)
logger.info('The sky is so blue')

log (qualcosa di simile)

2013-07-09 17:39:33,596 Super App : The sky is so blue

I filtri possono essere utilizzati anche per aggiungere informazioni contestuali.

import logging

class AppFilter(logging.Filter):
    def filter(self, record):
        record.app_name = 'Super App'
        return True

logger = logging.getLogger(__name__)
logger.addFilter(AppFilter())
syslog = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(syslog)

logger.info('The sky is so blue')

produce un record di registro simile.


3
Come possiamo specificarlo in un config.inifile? Desidero aggiungere il nome host corrente socket.gethostname().
Laurent LAPORTE

Ho questo campione che non funziona per me. import uuid uniqueId = str(uuid.uuid4()) extra = {"u_id" : uniqueId} RotatingHandler = RotatingFileHandler(LOG_FILENAME,encoding='utf-8',maxBytes=maxSize, backupCount=batchSize) logger.basicConfig(handlers=[RotatingHandler],level=logLevel.upper(),format='%(levelname)s %(u_id)s %(funcName)s %(asctime)s %(message)s ',datefmt='%m/%d/%Y %I:%M:%S %p') logger = logger.LoggerAdapter(logger=logger, extra=extra)
Hayat

È possibile aggiungere un campo "level" che è uguale a "levelname"? Vedi: Come posso rinominare "levelname" in "level" nei messaggi di log di Python?
Martin Thoma

2
Posso solo passare una serie di informazioni extra. Qualcosa di simile: "Si è verificato un errore per l'ID dipendente 1029382" Senza creare alcun dizionario.
shreesh katti

51

Devi passare il dict come parametro a extra per farlo in questo modo.

logging.info('Log message', extra={'app_name': 'myapp'})

Prova:

>>> import logging
>>> logging.basicConfig(format="%(foo)s - %(message)s")
>>> logging.warning('test', extra={'foo': 'bar'})
bar - test 

Inoltre, come nota, se provi a registrare un messaggio senza passare il dict, allora fallirà.

>>> logging.warning('test')
Traceback (most recent call last):
  File "/usr/lib/python2.7/logging/__init__.py", line 846, in emit
    msg = self.format(record)
  File "/usr/lib/python2.7/logging/__init__.py", line 723, in format
    return fmt.format(record)
  File "/usr/lib/python2.7/logging/__init__.py", line 467, in format
    s = self._fmt % record.__dict__
KeyError: 'foo'
Logged from file <stdin>, line 1

Funzionerà anche per logging.info()? Non è riuscito l'ultima volta che ho provato. : /
Prakhar Mohan Srivastava

2
Mi piace la risposta @ mr2ert. Puoi dare un valore predefinito al campo extra estendendo la logging.Formatterclasse: class CustomFormatter (logging.Formatter): def format (self, record): if not hasattr (record, 'foo'): record.foo = 'default_foo' return super (CustomFormatter, self.format (record) h = loggin.StreamHandler () h.setFormatter (CustomFormatter ('% (foo) s% (message) s') logger = logging.getLogger ('bar') logger.addHandler ( h) logger.error ('hey!', extra = {'foo': 'FOO'}) logger.error ('hey!')
loutre

Questo metodo è più veloce ma è necessario aggiungere le righe extra su ogni messaggio di registro che è facile da dimenticare e soggetto a errori. La sostituzione delle chiamate super () è più complicata della risposta di unutbu.
pevogam

@Prakhar Mohan Srivastava Sì Funzionerà bene anche per logging.info (). Quale messaggio di errore stai ricevendo?
shreesh katti

Posso solo passare una serie di informazioni extra. Qualcosa del genere: "Si è verificato un errore per l'ID dipendente 1029382" Senza creare alcun dizionario e passare le chiavi
shreesh katti

25

Python3

A partire da Python3.2 ora puoi usare LogRecordFactory

>>> import logging
>>> logging.basicConfig(format="%(custom_attribute)s - %(message)s")
>>> old_factory = logging.getLogRecordFactory()
>>> def record_factory(*args, **kwargs):
        record = old_factory(*args, **kwargs)
        record.custom_attribute = "my-attr"
        return record

>>> logging.setLogRecordFactory(record_factory)
>>> logging.info("hello")
my-attr - hello

Ovviamente record_factorypuò essere personalizzato per essere qualsiasi chiamabile e il valore dicustom_attribute potrebbe essere aggiornato se si mantiene un riferimento alla chiamata di fabbrica.

Perché è meglio che usare adattatori / filtri?

  • Non è necessario passare il logger all'applicazione
  • In realtà funziona con librerie di terze parti che utilizzano il proprio logger (semplicemente chiamando logger = logging.getLogger(..) ) ora avrebbero lo stesso formato di registro. (questo non è il caso di filtri / adattatori in cui è necessario utilizzare lo stesso oggetto logger)
  • Puoi impilare / concatenare più fabbriche

C'è qualche alternativa a Python 2.7?
karolch

Non con gli stessi vantaggi, con 2.7 dovresti andare con adattatori o filtri.
Ahmad

5
Questa è la migliore risposta di python3 al giorno d'oggi
Stéphane

Secondo docs.python.org/3/howto/logging-cookbook.html : questo modello consente a diverse librerie di concatenare le fabbriche e, a condizione che non sovrascrivono gli attributi l'una dell'altra o sovrascrivono involontariamente gli attributi forniti come standard, lì non dovrebbero esserci sorprese. Tuttavia, si dovrebbe tenere presente che ogni collegamento nella catena aggiunge un overhead di runtime a tutte le operazioni di registrazione e la tecnica dovrebbe essere utilizzata solo quando l'uso di un filtro non fornisce il risultato desiderato.
steve0hh

1
@ steve0hh uno dei risultati chiave desiderati è la capacità di registrare le informazioni contestuali in diverse librerie / moduli, cosa che potrebbe essere ottenuta solo in questo modo. Nella maggior parte dei casi, le librerie non dovrebbero toccare la configurazione del logger, è responsabilità dell'applicazione padre.
Ahmad

10

Un altro modo è creare un LoggerAdapter personalizzato. Ciò è particolarmente utile quando non è possibile modificare il formato OPPURE se il formato è condiviso con un codice che non invia la chiave univoca (nel tuo caso app_name ):

class LoggerAdapter(logging.LoggerAdapter):
    def __init__(self, logger, prefix):
        super(LoggerAdapter, self).__init__(logger, {})
        self.prefix = prefix

    def process(self, msg, kwargs):
        return '[%s] %s' % (self.prefix, msg), kwargs

E nel tuo codice, dovresti creare e inizializzare il tuo logger come al solito:

    logger = logging.getLogger(__name__)
    # Add any custom handlers, formatters for this logger
    myHandler = logging.StreamHandler()
    myFormatter = logging.Formatter('%(asctime)s %(message)s')
    myHandler.setFormatter(myFormatter)
    logger.addHandler(myHandler)
    logger.setLevel(logging.INFO)

Infine, creerai l'adattatore wrapper per aggiungere un prefisso secondo necessità:

    logger = LoggerAdapter(logger, 'myapp')
    logger.info('The world bores you when you are cool.')

L'output sarà simile a questo:

2013-07-09 17:39:33,596 [myapp] The world bores you when you are cool.

1

Ho trovato questa domanda SO dopo averla implementata da solo. Spero che aiuti qualcuno. Nel codice seguente, induco una chiave aggiuntiva chiamata claim_idnel formato logger. Registrerà il claim_id ogni volta che è presente una claim_idchiave nell'ambiente. Nel mio caso d'uso, avevo bisogno di registrare queste informazioni per una funzione AWS Lambda.

import logging
import os

LOG_FORMAT = '%(asctime)s %(name)s %(levelname)s %(funcName)s %(lineno)s ClaimID: %(claim_id)s: %(message)s'


class AppLogger(logging.Logger):

    # Override all levels similarly - only info overriden here

    def info(self, msg, *args, **kwargs):
        return super(AppLogger, self).info(msg, extra={"claim_id": os.getenv("claim_id", "")})


def get_logger(name):
    """ This function sets log level and log format and then returns the instance of logger"""
    logging.setLoggerClass(AppLogger)
    logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)
    return logger


LOGGER = get_logger(__name__)

LOGGER.info("Hey")
os.environ["claim_id"] = "12334"
LOGGER.info("Hey")

Gist: https://gist.github.com/ramanujam/306f2e4e1506f302504fb67abef50652


1

La risposta accettata non registrava il formato nel file di log, mentre il formato si rifletteva nell'output di sys. In alternativa ho utilizzato un approccio più semplice e ho lavorato come;

logging.basicConfig(filename="mylogfile.test",
                    filemode="w+",
                    format='%(asctime)s: ' +app_name+': %(message)s ',
                    level=logging.DEBUG)


0

Usando la risposta di mr2ert, ho trovato questa comoda soluzione (anche se immagino che non sia consigliato): sovrascrivi i metodi di registrazione incorporati per accettare l'argomento personalizzato e creare il extradizionario all'interno dei metodi:

import logging

class CustomLogger(logging.Logger):

   def debug(self, msg, foo, *args, **kwargs):
       extra = {'foo': foo}

       if self.isEnabledFor(logging.DEBUG):
            self._log(logging.DEBUG, msg, args, extra=extra, **kwargs)

   *repeat for info, warning, etc*

logger = CustomLogger('CustomLogger', logging.DEBUG)
formatter = logging.Formatter('%(asctime)s [%(foo)s] %(message)s') 
handler = logging.StreamHandler()
handler.setFormatter(formatter) 
logger.addHandler(handler)

logger.debug('test', 'bar')

Produzione:

2019-03-02 20:06:51,998 [bar] test

Questa è la funzione incorporata per riferimento:

def debug(self, msg, *args, **kwargs):
    """
    Log 'msg % args' with severity 'DEBUG'.

    To pass exception information, use the keyword argument exc_info with
    a true value, e.g.

    logger.debug("Houston, we have a %s", "thorny problem", exc_info=1)
    """
    if self.isEnabledFor(DEBUG):
        self._log(DEBUG, msg, args, **kwargs)

0

registrazione delle importazioni;

class LogFilter (logging.Filter):

def __init__(self, code):
    self.code = code

def filter(self, record):
    record.app_code = self.code
    return True

logging.basicConfig (format = '[% (asctime) s:% (levelname) s] :: [% (module) s ->% (name) s] - APP_CODE:% (app_code) s - MSG:% (messaggio )S');

logger di classe:

def __init__(self, className):
    self.logger = logging.getLogger(className)
    self.logger.setLevel(logging.ERROR)

@staticmethod
def getLogger(className):
    return Logger(className)

def logMessage(self, level, code, msg):
    self.logger.addFilter(LogFilter(code))

    if level == 'WARN':        
        self.logger.warning(msg)
    elif level == 'ERROR':
        self.logger.error(msg)
    else:
        self.logger.info(msg)

classe Test: logger = Logger.getLogger ('Test')

if __name__=='__main__':
    logger.logMessage('ERROR','123','This is an error')

Questa implementazione sarà molto inefficiente.
blakev
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.