Wie füge ich der Python-Protokollformatzeichenfolge ein benutzerdefiniertes Feld hinzu?

90

Meine aktuelle Formatzeichenfolge lautet:

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

und ich möchte ein neues Feld mit dem Namen hinzufügen, app_namedas in jedem Skript, das diesen Formatierer enthält, einen anderen Wert hat.

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

Ich bin mir jedoch nicht sicher, wie ich diesen app_nameWert an den Logger übergeben soll, um ihn in die Formatzeichenfolge zu interpolieren. Ich kann es natürlich dazu bringen, in der Protokollnachricht zu erscheinen, indem ich es jedes Mal weitergebe, aber das ist chaotisch.

Ich habe es versucht:

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

aber keine funktioniert.

Nickponline
quelle
Möchten Sie dies wirklich an jeden logAnruf weitergeben? Wenn ja, schauen Sie sich die Dokumente an, in denen steht: "Diese Funktionalität kann verwendet werden, um Ihre eigenen Werte in einen LogRecord einzufügen ...". Dies scheint jedoch ein Hauptfall für die Verwendung logger = logging.getLogger('myapp')und Einbringung in den logger.infoAufruf zu sein.
Abarnert
Die Python-Protokollierung kann dies bereits tun. Wenn Sie loggerin jeder App ein anderes Objekt verwenden, können Sie dafür sorgen, dass jedes Objekt einen anderen Namen verwendet, indem Sie Ihr Objekt loggerwie folgt instanziieren : logger = logging.getLogger(myAppName). Beachten Sie, dass dies __name__der Name des Python-Moduls ist. Wenn also jede App ein eigenes Python-Modul ist, funktioniert dies ebenfalls.
Florian Castellane

Antworten:

130

Sie können einen LoggerAdapter verwenden, damit Sie die zusätzlichen Informationen nicht bei jedem Protokollierungsaufruf übergeben müssen:

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

Protokolle (so etwas wie)

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

Filter können auch zum Hinzufügen von Kontextinformationen verwendet werden.

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

erzeugt einen ähnlichen Protokolldatensatz.

unutbu
quelle
3
Wie können wir das in einer config.iniDatei angeben ? Ich möchte den aktuellen Hostnamen hinzufügen socket.gethostname().
Laurent LAPORTE
Ich habe dieses Beispiel nicht für mich arbeiten. 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
Ist es möglich, ein Feld "level" hinzuzufügen, das gleich "levelname" ist? Siehe: Wie kann ich "Levelname" in Python-Protokollnachrichten in "Level" umbenennen?
Martin Thoma
2
Kann ich einfach eine Reihe von Extra-Informationen übergeben? Etwa so: "Fehler bei Mitarbeiter-ID 1029382 aufgetreten" Ohne Wörterbuch zu erstellen.
Shreesh Katti
50

Sie müssen das Diktat als Parameter an extra übergeben, um dies auf diese Weise zu tun.

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

Beweis:

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

Wenn Sie versuchen, eine Nachricht zu protokollieren, ohne das Diktat zu übergeben, schlägt dies fehl.

>>> 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
quelle
Funktioniert das auch für logging.info()? Es ist fehlgeschlagen, als ich es zuletzt versucht habe. : /
Prakhar Mohan Srivastava
2
Ich mag @ mr2ert Antwort. Sie können dem zusätzlichen Feld einen Standardwert logging.Formatterzuweisen, indem Sie die Klasse: class CustomFormatter (logging.Formatter): def format (self, record): Wenn nicht hasattr (record, 'foo'): record.foo = 'default_foo' return erweitern 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
Diese Methode ist schneller, aber Sie müssen die zusätzlichen Zeilen zu jeder Protokollnachricht hinzufügen, die leicht zu vergessen und fehleranfällig sind. Das Ersetzen der super () -Aufrufe ist chaotischer als die Antwort von unutbu.
Pevogam
@Prakhar Mohan Srivastava Ja, es funktioniert auch gut für logging.info (). Welche Fehlermeldung erhalten Sie?
Shreesh Katti
Kann ich einfach eine Reihe von Extra-Informationen übergeben? So etwas wie das: "Fehler für Mitarbeiter-ID 1029382 aufgetreten" Ohne ein Wörterbuch zu erstellen und die Schlüssel zu übergeben
Shreesh Katti
23

Python3

Ab Python3.2 können Sie jetzt LogRecordFactory verwenden

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

Natürlich record_factorykann es so angepasst werden, dass es aufrufbar ist, und der Wert von custom_attributekann aktualisiert werden, wenn Sie einen Verweis auf das werkseitig aufrufbare Objekt behalten.

Warum ist das besser als die Verwendung von Adaptern / Filtern?

  • Sie müssen Ihren Logger nicht an die Anwendung weitergeben
  • Es funktioniert tatsächlich mit Bibliotheken von Drittanbietern, die ihren eigenen Logger verwenden (durch einfaches Aufrufen logger = logging.getLogger(..)) und jetzt dasselbe Protokollformat haben. (Dies ist nicht der Fall bei Filtern / Adaptern, bei denen Sie dasselbe Logger-Objekt verwenden müssen.)
  • Sie können mehrere Fabriken stapeln / verketten
Ahmad
quelle
Gibt es eine Alternative für Python 2.7?
Karolch
Nicht mit den gleichen Vorteilen, mit 2.7 müssten Sie sich für Adapter oder Filter entscheiden.
Ahmad
5
Dies ist heutzutage die beste Antwort für Python3
Stéphane,
Laut docs.python.org/3/howto/logging-cookbook.html : Dieses Muster ermöglicht es verschiedenen Bibliotheken, Fabriken miteinander zu verketten , und solange sie die Attribute des anderen nicht überschreiben oder die standardmäßig bereitgestellten Attribute dort unbeabsichtigt überschreiben sollte keine Überraschungen sein. Es sollte jedoch berücksichtigt werden, dass jedes Glied in der Kette allen Protokollierungsvorgängen Laufzeitaufwand hinzufügt, und die Technik sollte nur verwendet werden, wenn die Verwendung eines Filters nicht das gewünschte Ergebnis liefert.
steve0hh
1
@ steve0hh Eines der wichtigsten gewünschten Ergebnisse ist die Fähigkeit, Kontextinformationen über verschiedene Bibliotheken / Module hinweg zu protokollieren, was nur auf diese Weise erreicht werden konnte. In den meisten Fällen sollten Bibliotheken die Logger-Konfiguration nicht berühren. Es liegt in der Verantwortung der übergeordneten Anwendung.
Ahmad
9

Eine andere Möglichkeit besteht darin, einen benutzerdefinierten LoggerAdapter zu erstellen. Dies ist besonders nützlich, wenn Sie das Format nicht ändern können ODER wenn Ihr Format für Code freigegeben ist, der den eindeutigen Schlüssel nicht sendet (in Ihrem Fall Anwendungsname ):

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

Und in Ihrem Code würden Sie Ihren Logger wie gewohnt erstellen und initialisieren:

    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)

Schließlich würden Sie den Wrapper-Adapter erstellen, um nach Bedarf ein Präfix hinzuzufügen:

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

Die Ausgabe sieht ungefähr so ​​aus:

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

Ich habe diese SO-Frage gefunden, nachdem ich sie selbst implementiert habe. Hoffe es hilft jemandem. Im folgenden Code induziere ich einen zusätzlichen Schlüssel, der claim_idim Logger-Format aufgerufen wird . Die Claim_ID wird immer dann protokolliert, wenn claim_idin der Umgebung ein Schlüssel vorhanden ist. In meinem Anwendungsfall musste ich diese Informationen für eine AWS Lambda-Funktion protokollieren.

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

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

rhn89
quelle
0

Mit der Antwort von mr2ert habe ich diese komfortable Lösung gefunden (obwohl ich denke, dass dies nicht empfohlen wird) - Überschreiben Sie die integrierten Protokollierungsmethoden, um das benutzerdefinierte Argument zu akzeptieren und das extraWörterbuch innerhalb der Methoden zu erstellen :

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

Ausgabe:

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

Dies ist die eingebaute Referenzfunktion:

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.
quelle
0

Protokollierung importieren;

Klasse 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] :: [% (modul) s ->% (name) s] - APP_CODE:% (app_code) s - MSG:% (message ) s ');

Klasse Logger:

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)

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

if __name__=='__main__':
    logger.logMessage('ERROR','123','This is an error')
user3672617
quelle
Diese Implementierung wird sehr ineffizient sein.
Blakev