PEP 282 – Ein Protokollsystem
- Autor:
- Vinay Sajip <vinay_sajip at red-dove.com>, Trent Mick <trentm at activestate.com>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 04-Feb-2002
- Python-Version:
- 2.3
- Post-History:
Zusammenfassung
Dieses PEP beschreibt ein vorgeschlagenes Protokollpaket für die Standardbibliothek von Python.
Grundsätzlich besteht das System darin, dass der Benutzer ein oder mehrere Logger-Objekte erstellt, auf denen Methoden aufgerufen werden, um Debugging-Hinweise, allgemeine Informationen, Warnungen, Fehler usw. zu protokollieren. Verschiedene Protokollierungs-'Ebenen' können verwendet werden, um wichtige Meldungen von weniger wichtigen zu unterscheiden.
Eine Registrierung von benannten Singleton-Logger-Objekten wird gepflegt, damit
- verschiedene logische Protokollierungsströme (oder 'Kanäle') existieren (z. B. einer für 'zope.zodb'-Sachen und ein anderer für 'mywebsite'-spezifische Sachen)
- man keine Referenzen auf Logger-Objekte weitergeben muss.
Das System ist zur Laufzeit konfigurierbar. Dieser Konfigurationsmechanismus erlaubt es, die Art und das Ausmaß der durchgeführten Protokollierung anzupassen, ohne die Anwendung selbst zu berühren.
Motivation
Wenn ein einheitlicher Protokollierungsmechanismus in die Standardbibliothek aufgenommen wird, 1) werden Protokollierungen mit größerer Wahrscheinlichkeit 'gut' durchgeführt, und 2) können mehrere Bibliotheken in größere Anwendungen integriert werden, die sinnvoll und kohärent protokolliert werden können.
Einflüsse
Dieser Vorschlag wurde nach der Untersuchung der folgenden Protokollpakete zusammengestellt
Einfaches Beispiel
Dies zeigt ein sehr einfaches Beispiel, wie das Protokollpaket verwendet werden kann, um einfache Protokollausgaben auf stderr zu erzeugen.
--------- mymodule.py -------------------------------
import logging
log = logging.getLogger("MyModule")
def doIt():
log.debug("Doin' stuff...")
#do stuff...
raise TypeError, "Bogus type error for testing"
-----------------------------------------------------
--------- myapp.py ----------------------------------
import mymodule, logging
logging.basicConfig()
log = logging.getLogger("MyApp")
log.info("Starting my app")
try:
mymodule.doIt()
except Exception, e:
log.exception("There was a problem.")
log.info("Ending my app")
-----------------------------------------------------
$ python myapp.py
INFO:MyApp: Starting my app
DEBUG:MyModule: Doin' stuff...
ERROR:MyApp: There was a problem.
Traceback (most recent call last):
File "myapp.py", line 9, in ?
mymodule.doIt()
File "mymodule.py", line 7, in doIt
raise TypeError, "Bogus type error for testing"
TypeError: Bogus type error for testing
INFO:MyApp: Ending my app
Das obige Beispiel zeigt das Standardausgabeformat. Alle Aspekte des Ausgabeformats sollten konfigurierbar sein, so dass Sie Ausgaben wie diese haben könnten
2002-04-19 07:56:58,174 MyModule DEBUG - Doin' stuff...
or just
Doin' stuff...
Kontrollfluss
Anwendungen rufen Logger-Objekte für die Protokollierung auf. Logger sind in einem hierarchischen Namensraum organisiert und untergeordnete Logger erben einige Protokollierungseigenschaften von ihren Eltern im Namensraum.
Logger-Namen passen in einen "Punkt-Namensraum", wobei Punkte Unter-Namensräume angeben. Der Namensraum der Logger-Objekte entspricht daher einer einzelnen Baumdatenstruktur.
""ist die Wurzel des Namensraums"Zope"wäre ein Kindknoten der Wurzel"Zope.ZODB"wäre ein Kindknoten von"Zope"
Diese Logger-Objekte erstellen LogRecord-Objekte, die zur Ausgabe an Handler-Objekte übergeben werden. Sowohl Logger als auch Handler können Protokollierungs-Ebenen und (optional) Filter verwenden, um zu entscheiden, ob sie an einem bestimmten LogRecord interessiert sind. Wenn es notwendig ist, einen LogRecord extern auszugeben, kann ein Handler (optional) einen Formatter verwenden, um die Meldung zu lokalisieren und zu formatieren, bevor sie an einen I/O-Stream gesendet wird.
Jeder Logger verfolgt eine Reihe von Ausgabe-Handlern. Standardmäßig senden alle Logger ihre Ausgaben auch an alle Handler ihrer Vorfahren-Logger. Logger können jedoch auch so konfiguriert werden, dass sie Handler weiter oben im Baum ignorieren.
Die APIs sind so strukturiert, dass Aufrufe auf den Logger-APIs günstig sind, wenn die Protokollierung deaktiviert ist. Wenn die Protokollierung für eine bestimmte Protokollierungsstufe deaktiviert ist, kann der Logger einen günstigen Vergleichstest durchführen und zurückkehren. Wenn die Protokollierung für eine bestimmte Protokollierungsstufe aktiviert ist, ist der Logger immer noch darauf bedacht, Kosten zu minimieren, bevor er den LogRecord an die Handler weitergibt. Insbesondere werden Lokalisierung und Formatierung (die relativ teuer sind) verzögert, bis der Handler sie anfordert.
Die gesamte Logger-Hierarchie kann auch eine zugeordnete Ebene haben, die Vorrang vor den Ebenen einzelner Logger hat. Dies geschieht über eine Modul-Funktion
def disable(lvl):
"""
Do not generate any LogRecords for requests with a severity less
than 'lvl'.
"""
...
Ebenen
Die Protokollierungsstufen, in zunehmender Reihenfolge der Wichtigkeit, sind
- DEBUG
- INFO
- WARN
- ERROR
- CRITICAL
Der Begriff CRITICAL wird gegenüber FATAL bevorzugt, der von log4j verwendet wird. Die Ebenen sind konzeptionell dieselben - die eines schwerwiegenden oder sehr schwerwiegenden Fehlers. FATAL impliziert jedoch den Tod, der in Python eine ausgelöste und nicht abgefangene Ausnahme, einen Traceback und ein Beenden impliziert. Da das Protokollmodul ein solches Ergebnis aus einem Protokolleintrag auf FATAL-Ebene nicht erzwingt, ist es sinnvoll, CRITICAL gegenüber FATAL zu bevorzugen.
Dies sind nur Integer-Konstanten, die einen einfachen Vergleich der Wichtigkeit ermöglichen. Erfahrungsgemäß kann es zu viele Ebenen verwirrend sein, da sie zu subjektiven Interpretationen führen, welche Ebene für eine bestimmte Protokollanfrage angewendet werden sollte.
Obwohl die obigen Ebenen dringend empfohlen werden, sollte das Protokollsystem nicht vorschreibend sein. Benutzer können ihre eigenen Ebenen definieren, sowie die textuelle Darstellung beliebiger Ebenen. Benutzerdefinierte Ebenen müssen jedoch die Einschränkungen befolgen, dass sie alle positive ganze Zahlen sind und dass sie in der Reihenfolge zunehmender Schwere zunehmen.
Benutzerdefinierte Protokollierungsstufen werden über zwei Modul-Funktionen unterstützt
def getLevelName(lvl):
"""Return the text for level 'lvl'."""
...
def addLevelName(lvl, lvlName):
"""
Add the level 'lvl' with associated text 'levelName', or
set the textual representation of existing level 'lvl' to be
'lvlName'."""
...
Logger
Jeder Logger-Objekt verfolgt eine Protokollierungsstufe (oder einen Schwellenwert), an der er interessiert ist, und verwirft Protokollierungsanfragen unterhalb dieser Stufe.
Eine Instanz der Klasse Manager verwaltet den hierarchischen Namensraum von benannten Logger-Objekten. Generationen werden durch punktgetrennte Namen bezeichnet: Logger "foo" ist das Elternteil der Logger "foo.bar" und "foo.baz".
Die Instanz der Manager-Klasse ist ein Singleton und wird den Benutzern nicht direkt ausgesetzt, die mit ihr über verschiedene Modul-Funktionen interagieren.
Die allgemeine Protokollierungsmethode ist
class Logger:
def log(self, lvl, msg, *args, **kwargs):
"""Log 'str(msg) % args' at logging level 'lvl'."""
...
Es werden jedoch Komfortfunktionen für jede Protokollierungsstufe definiert
class Logger:
def debug(self, msg, *args, **kwargs): ...
def info(self, msg, *args, **kwargs): ...
def warn(self, msg, *args, **kwargs): ...
def error(self, msg, *args, **kwargs): ...
def critical(self, msg, *args, **kwargs): ...
Nur ein Schlüsselwortargument wird derzeit erkannt – "exc_info". Wenn wahr, möchte der Aufrufer Ausnahmeinformationen in der Protokollausgabe haben. Dieser Mechanismus ist nur erforderlich, wenn Ausnahmeinformationen auf **jeder** Protokollierungsstufe bereitgestellt werden müssen. In dem häufigeren Fall, in dem Ausnahmeinformationen nur bei Fehlern zum Protokoll hinzugefügt werden müssen, d. h. auf ERROR-Ebene, wird eine weitere Komfortmethode bereitgestellt
class Logger:
def exception(self, msg, *args): ...
Dies sollte nur im Kontext eines Ausnahmebehandlers aufgerufen werden und ist der bevorzugte Weg, um den Wunsch nach Ausnahmeinformationen im Protokoll zu signalisieren. Die anderen Komfortmethoden sind dazu bestimmt, mit exc_info nur in der ungewöhnlichen Situation aufgerufen zu werden, in der Sie beispielsweise Ausnahmeinformationen im Kontext einer INFO-Meldung bereitstellen möchten.
Das obige "msg"-Argument ist normalerweise eine Formatzeichenkette; es kann jedoch jedes Objekt x sein, für das str(x) die Formatzeichenkette zurückgibt. Dies erleichtert beispielsweise die Verwendung eines Objekts, das eine lokalitätsspezifische Meldung für eine internationalisierte/lokalisierte Anwendung abruft, möglicherweise unter Verwendung des Standard-gettext-Moduls. Ein einfaches Beispiel
class Message:
"""Represents a message"""
def __init__(self, id):
"""Initialize with the message ID"""
def __str__(self):
"""Return an appropriate localized message text"""
...
logger.info(Message("abc"), ...)
Das Sammeln und Formatieren von Daten für eine Protokollnachricht kann teuer sein und eine Verschwendung, wenn der Logger die Nachricht ohnehin verworfen hätte. Um zu sehen, ob eine Anfrage vom Logger angenommen wird, kann die Methode isEnabledFor() verwendet werden
class Logger:
def isEnabledFor(self, lvl):
"""
Return true if requests at level 'lvl' will NOT be
discarded.
"""
...
anstelle dieser teuren und möglicherweise verschwenderischen DOM-zu-XML-Konvertierung
...
hamletStr = hamletDom.toxml()
log.info(hamletStr)
...
kann man dies tun
if log.isEnabledFor(logging.INFO):
hamletStr = hamletDom.toxml()
log.info(hamletStr)
Wenn neue Logger erstellt werden, werden sie mit einer Ebene initialisiert, die "keine Ebene" bedeutet. Eine Ebene kann explizit mit der Methode setLevel() gesetzt werden
class Logger:
def setLevel(self, lvl): ...
Wenn die Ebene eines Loggers nicht gesetzt ist, konsultiert das System alle seine Vorfahren und geht die Hierarchie hinauf, bis eine explizit gesetzte Ebene gefunden wird. Diese wird als "effektive Ebene" des Loggers betrachtet und kann über die Methode getEffectiveLevel() abgefragt werden.
def getEffectiveLevel(self): ...
Logger werden niemals direkt instanziiert. Stattdessen wird eine Modul-Funktion verwendet
def getLogger(name=None): ...
Wenn kein Name angegeben wird, wird der Root-Logger zurückgegeben. Andernfalls wird, wenn ein Logger mit diesem Namen existiert, dieser zurückgegeben. Wenn nicht, wird ein neuer Logger initialisiert und zurückgegeben. Hier ist "name" synonym mit "Kanalname".
Benutzer können eine benutzerdefinierte Unterklasse von Logger angeben, die vom System beim Instanziieren neuer Logger verwendet wird
def setLoggerClass(klass): ...
Die übergebene Klasse sollte eine Unterklasse von Logger sein, und ihre __init__-Methode sollte Logger.__init__ aufrufen.
Handler
Handler sind dafür verantwortlich, etwas Nützliches mit einem gegebenen LogRecord zu tun. Die folgenden Kern-Handler werden implementiert
StreamHandler: Ein Handler zum Schreiben in ein dateiähnliches Objekt.FileHandler: Ein Handler zum Schreiben in eine einzelne Datei oder eine Reihe von rotierenden Dateien.SocketHandler: Ein Handler zum Schreiben in entfernte TCP-Ports.DatagramHandler: Ein Handler zum Schreiben in UDP-Sockets für kostengünstige Protokollierung. Jeff Bauer hatte bereits ein solches System [5].MemoryHandler: Ein Handler, der Protokollaufzeichnungen im Speicher puffert, bis der Puffer voll ist oder eine bestimmte Bedingung eintritt [1].SMTPHandler: Ein Handler zum Senden an E-Mail-Adressen über SMTP.SysLogHandler: Ein Handler zum Schreiben in Unix syslog über UDP.NTEventLogHandler: Ein Handler zum Schreiben in Ereignisprotokolle unter Windows NT, 2000 und XP.HTTPHandler: Ein Handler zum Schreiben an einen Webserver mit GET- oder POST-Semantik.
Handlern können auch Ebenen zugewiesen werden, indem die Methode setLevel() verwendet wird
def setLevel(self, lvl): ...
Der FileHandler kann so eingerichtet werden, dass er eine rotierende Reihe von Protokolldateien erstellt. In diesem Fall wird der dem Konstruktor übergebene Dateiname als "Basis"-Dateiname verwendet. Zusätzliche Dateinamen für die Rotation werden durch Anhängen von .1, .2 usw. an den Basisdateinamen erstellt, bis zu einem Maximum, das beim Anfordern des Umbruchs angegeben wird. Die Methode setRollover wird verwendet, um eine maximale Größe für eine Protokolldatei und eine maximale Anzahl von Sicherungsdateien in der Rotation anzugeben.
def setRollover(maxBytes, backupCount): ...
Wenn maxBytes als Null angegeben wird, erfolgt niemals ein Umbruch und die Protokolldatei wächst unbegrenzt. Wenn eine nicht-null Größe angegeben wird, tritt ein Umbruch ein, wenn diese Größe überschritten zu werden droht. Die Umbruchmethode stellt sicher, dass der Basisdateiname immer der aktuellste ist, .1 der nächstaktuellste, .2 der nächstaktuellste danach und so weiter.
Es gibt viele zusätzliche Handler, die in den Test-/Beispielskripten bereitgestellt werden, die mit [6] geliefert werden – zum Beispiel XMLHandler und SOAPHandler.
LogRecords
Ein LogRecord fungiert als Auffangbehälter für Informationen über ein Protokollierungsereignis. Es ist kaum mehr als ein Wörterbuch, obwohl es eine Methode getMessage definiert, die eine Meldung mit optionalen Laufargumenten zusammenführt.
Formatierer
Ein Formatter ist dafür verantwortlich, einen LogRecord in eine Zeichenfolgendarstellung umzuwandeln. Ein Handler kann seinen Formatter aufrufen, bevor er eine Aufzeichnung schreibt. Die folgenden Kern-Formatter werden implementiert
Formatter: Bietet printf-ähnliche Formatierung unter Verwendung des %-Operators.BufferingFormatter: Bietet Formatierung für mehrere Meldungen mit Unterstützung für Kopf- und Fußzeilenformatierung.
Formatter werden Handlern zugeordnet, indem setFormatter() auf einem Handler aufgerufen wird
def setFormatter(self, form): ...
Formatter verwenden den %-Operator, um die Protokollnachricht zu formatieren. Die Formatzeichenkette sollte %(name)x enthalten, und das Attribut-Dictionary des LogRecord wird verwendet, um nachrichtenbezogene Daten zu erhalten. Die folgenden Attribute werden bereitgestellt
%(name)s |
Name des Loggers (Protokollkanal) |
%(levelno)s |
Numerische Protokollierungsstufe der Meldung (DEBUG, INFO, WARN, ERROR, CRITICAL) |
%(levelname)s |
Textuelle Protokollierungsstufe der Meldung ("DEBUG", "INFO", "WARN", "ERROR", "CRITICAL") |
%(pathname)s |
Vollständiger Pfad zum Quellcode, von dem der Protokollaufruf ausging (falls verfügbar) |
%(filename)s |
Dateiname aus dem Pfad |
%(module)s |
Modul, von dem der Protokollaufruf ausging |
%(lineno)d |
Quellzeilennummer, bei der der Protokollaufruf ausging (falls verfügbar) |
%(created)f |
Zeitpunkt, zu dem der LogRecord erstellt wurde (Rückgabewert von time.time()) |
%(asctime)s |
Textuelle Zeit, zu der der LogRecord erstellt wurde |
%(msecs)d |
Millisekundenanteil der Erstellungszeit |
%(relativeCreated)d |
Zeit in Millisekunden, zu der der LogRecord erstellt wurde, relativ zur Zeit, als das Protokollmodul geladen wurde (typischerweise zum Zeitpunkt des Anwendungsstarts) |
%(thread)d |
Thread-ID (falls verfügbar) |
%(message)s |
Das Ergebnis von record.getMessage(), berechnet kurz bevor der Record ausgegeben wird |
Wenn ein Formatter feststellt, dass die Formatzeichenkette "(asctime)s" enthält, wird die Erstellungszeit in das asctime-Attribut des LogRecord formatiert. Um Flexibilität bei der Formatierung von Daten zu ermöglichen, werden Formatter mit einer Formatzeichenkette für die Meldung als Ganzes und einer separaten Formatzeichenkette für Datum/Zeit initialisiert. Die Datum/Zeit-Formatzeichenkette sollte im time.strftime-Format vorliegen. Der Standardwert für das Nachrichtenformat ist "%(message)s". Das Standard-Datum/Zeit-Format ist ISO8601.
Der Formatter verwendet ein Klassenattribut "converter", um anzugeben, wie eine Zeit von Sekunden in ein Tupel umgewandelt werden soll. Standardmäßig ist der Wert von "converter" "time.localtime". Bei Bedarf kann ein anderer Konverter (z. B. "time.gmtime") für eine einzelne Formatter-Instanz festgelegt oder das Klassenattribut geändert werden, um alle Formatter-Instanzen zu beeinflussen.
Filter
Wenn die ebenenbasierte Filterung nicht ausreicht, kann ein Filter von einem Logger oder Handler aufgerufen werden, um zu entscheiden, ob ein LogRecord ausgegeben werden soll. Logger und Handler können mehrere Filter installiert haben, und jeder von ihnen kann ein LogRecord von der Ausgabe ausschließen.
class Filter:
def filter(self, record):
"""
Return a value indicating true if the record is to be
processed. Possibly modify the record, if deemed
appropriate by the filter.
"""
Das Standardverhalten erlaubt die Initialisierung eines Filters mit einem Logger-Namen. Dies lässt nur Ereignisse durch, die mit dem benannten Logger oder einem seiner Kinder generiert wurden. Ein Filter, der mit "A.B" initialisiert wurde, lässt beispielsweise Ereignisse zu, die von den Loggern "A.B", "A.B.C", "A.B.C.D", "A.B.D" usw. protokolliert wurden, aber nicht "A.BB", "B.A.B" usw. Wenn er mit einer leeren Zeichenkette initialisiert wird, werden alle Ereignisse vom Filter durchgelassen. Dieses Filterverhalten ist nützlich, wenn man sich auf einen bestimmten Bereich einer Anwendung konzentrieren möchte; der Fokus kann einfach durch Ändern eines Filters, der an den Root-Logger angehängt ist, geändert werden.
Es gibt viele Beispiele für Filter in [6].
Konfiguration
Der Hauptvorteil eines Protokollierungssystems wie diesem ist, dass man steuern kann, wie viel und welche Protokollausgaben man von einer Anwendung erhält, ohne den Quellcode dieser Anwendung zu ändern. Daher muss die Konfiguration, obwohl sie über die Protokollierungs-API erfolgen kann, auch möglich sein, die Protokollierungskonfiguration zu ändern, ohne eine Anwendung überhaupt zu ändern. Für langlaufende Programme wie Zope sollte es möglich sein, die Protokollierungskonfiguration während des laufenden Programms zu ändern.
Die Konfiguration umfasst Folgendes
- Welche Protokollierungsstufe ein Logger oder Handler interessiert.
- Welche Handler an welche Logger angehängt werden sollen.
- Welche Filter an welche Handler und Logger angehängt werden sollen.
- Festlegen von Attributen, die für bestimmte Handler und Filter spezifisch sind.
Im Allgemeinen wird jede Anwendung ihre eigenen Anforderungen daran haben, wie ein Benutzer die Protokollausgabe konfigurieren kann. Jede Anwendung wird jedoch die erforderliche Konfiguration über einen Standardmechanismus an das Protokollsystem weitergeben.
Die einfachste Konfiguration ist die eines einzelnen Handlers, der nach stderr schreibt und an den Root-Logger angehängt ist. Diese Konfiguration wird durch Aufrufen der Funktion basicConfig() eingerichtet, sobald das Protokollmodul importiert wurde.
def basicConfig(): ...
Für anspruchsvollere Konfigurationen macht dieses PEP aus folgenden Gründen keine spezifischen Vorschläge
- Ein spezifischer Vorschlag könnte als vorschreibend angesehen werden.
- Ohne die Vorteile breiter praktischer Erfahrung in der Python-Community gibt es keine Möglichkeit zu wissen, ob ein bestimmter Konfigurationsansatz gut ist. Das kann nicht wirklich geschehen, bis das Protokollmodul verwendet wird, und das bedeutet, bis **nachdem** Python 2.3 ausgeliefert wurde.
- Es besteht die Wahrscheinlichkeit, dass verschiedene Arten von Anwendungen unterschiedliche Konfigurationsansätze erfordern, so dass kein "Einheitsansatz" passt.
Die Referenzimplementierung [6] verfügt über ein funktionierendes Konfigurationsdateiformat, das zur Verifizierung des Konzepts und als möglicher Alternativvorschlag implementiert ist. Es ist möglich, dass separate Erweiterungsmodule, die nicht Teil der Kern-Python-Distribution sind, für die Protokollierungskonfiguration und -anzeige, ergänzende Handler und andere Funktionen erstellt werden, die für die Mehrheit der Community nicht von Interesse sind.
Threadsicherheit
Das Protokollsystem sollte die Thread-sichere Operation unterstützen, ohne dass vom Benutzer besondere Maßnahmen ergriffen werden müssen.
Modulweite Funktionen
Um die Verwendung des Protokollierungsmechanismus in kurzen Skripten und kleinen Anwendungen zu unterstützen, werden Modul-Funktionen debug(), info(), warn(), error(), critical() und exception() bereitgestellt. Diese funktionieren genauso wie die entsprechend benannten Methoden von Logger – tatsächlich delegieren sie an die entsprechenden Methoden des Root-Loggers. Eine weitere Annehmlichkeit, die diese Funktionen bieten, ist, dass, wenn keine Konfiguration vorgenommen wurde, basicConfig() automatisch aufgerufen wird.
Beim Beenden der Anwendung können alle Handler durch Aufrufen der Funktion geleert werden
def shutdown(): ...
Dies leert und schließt alle Handler.
Implementierung
Die Referenzimplementierung ist Vinay Sajips Logging-Modul [6].
Packaging
Die Referenzimplementierung ist als ein einziges Modul implementiert. Dies bietet die einfachste Schnittstelle – alles, was Benutzer tun müssen, ist "import logging", und sie sind in der Lage, alle verfügbaren Funktionalitäten zu nutzen.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0282.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT