Comment ajouter un champ personnalisé à la chaîne de format de journal Python?

91

Ma chaîne de format actuelle est:

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

et je veux ajouter un nouveau champ appelé app_namequi aura une valeur différente dans chaque script qui contient ce formateur.

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

Mais je ne sais pas comment transmettre cette app_namevaleur à l'enregistreur pour interpoler dans la chaîne de format. Je peux évidemment le faire apparaître dans le message de journal en le passant à chaque fois, mais c'est compliqué.

J'ai essayé:

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

mais aucun ne fonctionne.

nickponline
la source
Voulez-vous vraiment transmettre cela à chaque logappel? Si c'est le cas, regardez la documentation où il est dit "Cette fonctionnalité peut être utilisée pour injecter vos propres valeurs dans un LogRecord ..." Mais cela semble être un cas de choix pour l'utiliser logger = logging.getLogger('myapp')et le faire cuire dans l' logger.infoappel.
abarnert le
la journalisation python peut déjà faire cela afaik. si vous utilisez un autre loggerobjet dans chaque application, vous pouvez faire de chacun un nom différent par instanciation vos loggers comme ceci: logger = logging.getLogger(myAppName). notez qu'il __name__s'agit du nom du module python, donc si chaque application est son propre module python, cela fonctionnerait également.
Florian Castellane

Réponses:

131

Vous pouvez utiliser un LoggerAdapter pour ne pas avoir à transmettre les informations supplémentaires à chaque appel de journalisation:

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

journaux (quelque chose comme)

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

Les filtres peuvent également être utilisés pour ajouter des informations contextuelles.

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

produit un enregistrement de journal similaire.

unutbu
la source
3
Comment pouvons-nous spécifier cela dans un config.inifichier? Je souhaite ajouter le nom d'hôte actuel socket.gethostname().
Laurent LAPORTE
J'ai cet échantillon ne fonctionne pas pour moi. 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
Est-il possible d'ajouter un champ "level" égal à "levelname"? Voir: Comment puis-je renommer «levelname» en «level» dans les messages de journal Python?
Martin Thoma
2
Puis-je simplement passer une chaîne d'informations supplémentaires. Quelque chose comme ceci: "Une erreur s'est produite pour l'ID d'employé 1029382" Sans créer de dictionnaire.
shreesh katti
50

Vous devez passer le dict en tant que paramètre à extra pour le faire de cette façon.

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

Preuve:

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

De plus, si vous essayez de consigner un message sans passer le dict, cela échouera.

>>> 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
mr2ert
la source
Cela fonctionnera-t-il également pour logging.info()? Cela a échoué la dernière fois que j'ai essayé. : /
Prakhar Mohan Srivastava
2
J'aime la réponse @ mr2ert. Vous pouvez donner une valeur par défaut au champ supplémentaire en étendant la logging.Formatterclasse: class CustomFormatter (logging.Formatter): def format (self, record): sinon hasattr (record, 'foo'): record.foo = 'default_foo' return super (CustomFormatter, self.format (enregistrement) 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
Cette méthode est plus rapide mais vous devez ajouter des lignes supplémentaires sur chaque message de journal, ce qui est facile à oublier et sujet aux erreurs. Le remplacement des appels super () est plus compliqué que la réponse de unutbu.
pevogam
@Prakhar Mohan Srivastava Oui, cela fonctionnera également pour logging.info (). Quel message d'erreur obtenez-vous?
shreesh katti
Puis-je simplement passer une chaîne d'informations supplémentaires. Quelque chose comme ceci: "Une erreur s'est produite pour l'ID d'employé 1029382" Sans créer de dictionnaire et passer les clés
shreesh katti
23

Python3

Depuis Python3.2, vous pouvez désormais utiliser 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

Bien sûr, il record_factorypeut être personnalisé pour être appelable et la valeur de custom_attributepourrait être mise à jour si vous conservez une référence à la fabrique appelable.

Pourquoi est-ce mieux que d'utiliser des adaptateurs / filtres?

  • Vous n'avez pas besoin de passer votre enregistreur autour de l'application
  • Il fonctionne en fait avec des bibliothèques tierces qui utilisent leur propre enregistreur (en appelant simplement logger = logging.getLogger(..)) auraient maintenant le même format de journal. (ce n'est pas le cas avec les filtres / adaptateurs où vous devez utiliser le même objet enregistreur)
  • Vous pouvez empiler / chaîner plusieurs usines
Ahmad
la source
Existe-t-il une alternative à python 2.7?
karolch
Pas avec les mêmes avantages, avec 2.7, vous devrez utiliser des adaptateurs ou des filtres.
Ahmad le
5
C'est la meilleure réponse python3 de nos jours
Stéphane
Selon docs.python.org/3/howto/logging-cookbook.html : ce modèle permet à différentes bibliothèques de chaîner des usines ensemble, et tant qu'elles n'écrasent pas les attributs de l'autre ou n'écrasent pas involontairement les attributs fournis en standard, il ne devrait pas être des surprises. Cependant, il convient de garder à l'esprit que chaque maillon de la chaîne ajoute une surcharge d'exécution à toutes les opérations de journalisation, et la technique ne doit être utilisée que lorsque l'utilisation d'un filtre ne fournit pas le résultat souhaité.
steve0hh
1
@ steve0hh l'un des principaux résultats souhaités est la possibilité de consigner des informations contextuelles dans différentes bibliothèques / modules, ce qui ne pourrait être obtenu qu'en utilisant cette méthode. Dans la plupart des cas, les bibliothèques ne doivent pas toucher à la configuration de l'enregistreur, c'est la responsabilité de l'application parente.
Ahmad
9

Une autre méthode consiste à créer un LoggerAdapter personnalisé. Ceci est particulièrement utile lorsque vous ne pouvez pas modifier le format OU si votre format est partagé avec du code qui n'envoie pas la clé unique (dans votre cas 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

Et dans votre code, vous créez et initialisez votre enregistreur comme d'habitude:

    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)

Enfin, vous créerez l'adaptateur wrapper pour ajouter un préfixe si nécessaire:

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

La sortie ressemblera à ceci:

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

J'ai trouvé cette question SO après l'avoir mise en œuvre moi-même. J'espère que ça aide quelqu'un. Dans le code ci-dessous, j'induit une clé supplémentaire appelée claim_idau format enregistreur. Il enregistrera le claim_id chaque fois qu'une claim_idclé est présente dans l'environnement. Dans mon cas d'utilisation, j'avais besoin de consigner ces informations pour une fonction 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

rhn89
la source
0

En utilisant la réponse de mr2ert, j'ai trouvé cette solution confortable (bien que je suppose que ce n'est pas recommandé) - Remplacez les méthodes de journalisation intégrées pour accepter l'argument personnalisé et créer le extradictionnaire à l'intérieur des méthodes:

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

Production:

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

Voici la fonction intégrée pour référence:

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)
Yaniv K.
la source
0

journalisation des importations;

classe 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:% (message ) s ');

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

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

if __name__=='__main__':
    logger.logMessage('ERROR','123','This is an error')
utilisateur3672617
la source
Cette mise en œuvre sera très inefficace.
blakev