it-swarm.com.de

Hinzufügen eines benutzerdefinierten Loglevel zu Pythons Protokollierungsfunktion

Ich hätte gerne Loglevel TRACE (5) für meine Bewerbung, da debug() meiner Meinung nach nicht ausreicht. Außerdem ist log(5, msg) nicht das, was ich will. Wie kann ich einem Python-Logger ein benutzerdefiniertes Loglevel hinzufügen?

Ich habe einen mylogger.py mit folgendem Inhalt:

import logging

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

In meinem Code verwende ich es folgendermaßen:

class ExampleClass(object):
    from mylogger import log

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

Jetzt möchte ich self.log.trace("foo bar") anrufen.

Vielen Dank im Voraus für Ihre Hilfe.

Edit (8. Dezember 2016): Ich habe die akzeptierte Antwort in pfa's geändert. Dies ist IMHO, eine ausgezeichnete Lösung, die auf dem sehr guten Vorschlag von Eric S basiert.

84
tuergeist

@Eric S.

Die Antwort von Eric S. ist hervorragend, aber ich habe durch Experimentieren gelernt, dass dies immer dazu führt, dass Nachrichten, die auf der neuen Debug-Ebene protokolliert wurden, gedruckt werden - unabhängig davon, auf welche Protokollebene sie eingestellt sind. Wenn Sie also eine neue Ebenennummer von 9 erstellen, wenn Sie setLevel (50) aufrufen, werden die Meldungen der unteren Ebene fälschlicherweise gedruckt. Um dies zu verhindern, benötigen Sie eine weitere Zeile in der Funktion "debugv", um zu prüfen, ob die betreffende Protokollierungsstufe tatsächlich aktiviert ist.

Behobenes Beispiel, das überprüft, ob die Protokollierungsstufe aktiviert ist:

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

Wenn Sie sich den Code für class Logger in logging.__init__.py für Python 2.7 ansehen, tun dies alle Standardprotokollfunktionen (.critical, .debug usw.).

Ich kann anscheinend keine Antworten auf die Antworten anderer posten, da er keinen guten Ruf hat ... hoffentlich wird Eric seinen Beitrag aktualisieren, wenn er das sieht. =)

134
pfa

Ich nahm die Antwort "Vermeiden Sie, Lambda zu sehen" und musste ändern, wo der log_at_my_log_level hinzugefügt wurde. Ich sah auch das Problem, das Paul tat: "Ich glaube nicht, dass das funktioniert. Benötigen Sie nicht Logger als erstes Argument in log_at_my_log_level?" Das hat bei mir funktioniert

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
58
Eric S.

Diese Frage ist ziemlich alt, aber ich habe mich gerade mit demselben Thema befasst und einen Weg gefunden, der den bereits erwähnten ähnlich ist, der mir ein wenig sauberer erscheint. Dies wurde mit 3.4 getestet. Ich bin mir nicht sicher, ob die verwendeten Methoden in älteren Versionen vorhanden sind:

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)
32
Wisperwind

Wenn ich alle vorhandenen Antworten mit einer Reihe von Nutzungserfahrungen kombiniere, denke ich, dass ich eine Liste mit allen Dingen erstellt habe, die erforderlich sind, um eine vollständig nahtlose Verwendung der neuen Ebene zu gewährleisten. Die folgenden Schritte setzen voraus, dass Sie eine neue Ebene TRACE mit dem Wert logging.DEBUG - 5 == 5 hinzufügen:

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') muss aufgerufen werden, damit die neue Ebene intern registriert wird, damit sie nach Name referenziert werden kann.
  2. Die neue Ebene muss zur Konsistenz in logging selbst als Attribut hinzugefügt werden: logging.TRACE = logging.DEBUG - 5.
  3. Dem trace-Modul muss eine Methode namens logging hinzugefügt werden. Es sollte sich genau wie debug, info usw. verhalten.
  4. Eine Methode mit dem Namen trace muss der aktuell konfigurierten Logger-Klasse hinzugefügt werden. Da dies nicht zu 100% logging.Logger garantiert ist, verwenden Sie stattdessen logging.getLoggerClass().

Alle Schritte werden in der folgenden Methode veranschaulicht:

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)
28
Mad Physicist

Wer hat mit der schlechten Praxis der Verwendung interner Methoden (self._log) begonnen und warum basiert jede Antwort darauf ?! Die Pythonic-Lösung würde stattdessen self.log verwenden, damit Sie sich nicht mit irgendwelchen internen Dingen herumschlagen müssen:

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

Ich finde es einfacher, ein neues Attribut für das Logger-Objekt zu erstellen, das die Funktion log () übergibt. Ich denke, dass das Logger-Modul aus genau diesem Grund addLevelName () und log () enthält. Somit sind keine Unterklassen oder neue Methode erforderlich. 

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

jetzt

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

sollte wie erwartet funktionieren.

9
LtPinback

Ich denke, Sie müssen die Klasse Logger subclassieren und eine Methode namens trace hinzufügen, die im Grunde Logger.log mit einer niedrigeren Stufe als DEBUG aufruft. Ich habe es nicht ausprobiert, aber dies ist, was die docs anzeigen. 

8
Noufal Ibrahim

Tipps zum Erstellen eines benutzerdefinierten Loggers:

  1. Verwenden Sie nicht _log, verwenden Sie log (Sie müssen isEnabledFor nicht überprüfen)
  2. das Protokollierungsmodul sollte die Instanz sein, die eine Instanz des benutzerdefinierten Protokollierers erstellt, da es in getLogger etwas Magie ausübt. Daher müssen Sie die Klasse über setLoggerClass festlegen.
  3. Sie müssen nicht __init__ für den Logger, class, definieren, wenn Sie nichts speichern
# Lower than debug which is 10
TRACE = 5
class MyLogger(logging.Logger):
    def trace(self, msg, *args, **kwargs):
        self.log(TRACE, msg, *args, **kwargs)

Wenn Sie diesen Logger aufrufen, verwenden Sie setLoggerClass(MyLogger), um ihn zum Standard-Logger von getLogger zu machen.

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

Sie müssen setFormatter, setHandler und setLevel(TRACE) in der handler und in der log selbst angeben, um diese Ablaufverfolgung auf niedriger Ebene tatsächlich zu sehen

4
Bryce Guinta

Das hat für mich funktioniert:

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

Das Problem mit Lambda/funcName wurde mit logger._log behoben, wie @marqueed darauf hinweist. Ich denke, dass die Verwendung von Lambda etwas sauberer aussieht, aber der Nachteil ist, dass es keine Schlüsselwortargumente zulässt. Ich habe das selbst noch nie benutzt, also kein Biggie.

 HINWEIS: Die Schule ist für den Sommer aus! Kumpel
 FATAL-Setup: Datei nicht gefunden .
3
Gringo Suave

Meiner Erfahrung nach ist dies die vollständige Lösung des Problems der Op ... um zu vermeiden, "Lambda" als Funktion zu sehen, in der die Nachricht ausgegeben wird, gehen Sie tiefer:

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

Ich habe noch nie versucht, mit einer eigenständigen Logger-Klasse zu arbeiten, aber ich denke, dass die Grundidee die gleiche ist (_log verwenden).

2
marqueed

Ergänzung zu Mad Physicists Beispiel, um Dateinamen und Zeilennummer korrekt zu erhalten

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

Während wir bereits viele richtige Antworten haben, ist das Folgende meiner Meinung nach eher Pythonic:

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)

Wenn Sie mypy in Ihrem Code verwenden möchten, wird empfohlen, # type: ignore hinzuzufügen, um zu verhindern, dass Warnungen Attribute hinzufügen.

2
DerWeh

basierend auf festgehaltener antwort.... ich schrieb eine kleine methode, die automatisch neue logging-ebenen erstellt

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.__= 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 kann smth so machen:

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

Als Alternative zum Hinzufügen einer zusätzlichen Methode zur Logger-Klasse würde ich empfehlen, die Logger.log(level, msg)-Methode zu verwenden.

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
schlamar

Ich bin verwirrt; Zumindest mit Python 3.5 funktioniert es einfach:

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

ausgabe:

DEBUG: root: y1 

TRACE: root: y2

0
gerardw