Dynamische Änderung der Protokollebene ohne Neustart der Anwendung

70

Ist es möglich, die Protokollebene mit fileConfig in Python zu ändern, ohne die Anwendung neu zu starten? Wenn dies nicht über fileConfig erreicht werden kann, gibt es eine andere Möglichkeit, das gleiche Ergebnis zu erzielen?

Update: Dies war für eine Anwendung, die auf einem Server ausgeführt wird. Ich wollte, dass Systemadministratoren eine Konfigurationsdatei ändern können, die zur Laufzeit von der Anwendung ausgewählt wird, und die Protokollstufe dynamisch ändern. Ich habe zu dieser Zeit mit gevent gearbeitet, daher habe ich meinen Code als eine der Antworten hinzugefügt, die inotify verwendet, um Änderungen an der Konfigurationsdatei auszuwählen.

opensourcegeek
quelle
2
Jeder, der Threads und Timer verwendet, um die Protokollierungsstufe zurückzusetzen. Dafür wurden Signale erfunden. Sie können einen Prozess mit "SIGHUP" (der Standard bei * nix) signalisieren, um die Konfiguration neu zu laden. In Python können Sie einen Signalhandler installieren, der dies abfängt (Ereignislaufwerk) und somit eine Konfiguration neu lädt oder zurücksetzt.
Ben DeMott
2
Ich bin damit einverstanden, Signale zu verwenden. Schlagen Sie vor, SIGUSR1 oder SIGUSR2 für diesen Zweck anstelle von SIGHUP zu verwenden. Letzteres wird gesendet, wenn das steuernde Terminal eines Prozesses getrennt wird.
Mike Ellis
1
SIGUSR1, SIGUSR2sind nur für Unix-ähnliche Systeme verfügbar. Sie können verwenden logging.config.listen(), um nach neuen Konfigurationen zu suchen ( docs.python.org/3/library/… )
Yoonghm

Antworten:

116

fileConfigist ein Mechanismus zum Konfigurieren der Protokollebene für Sie basierend auf einer Datei. Sie können es jederzeit in Ihrem Programm dynamisch ändern.

Rufen Sie .setLevel()das Protokollierungsobjekt auf, für das Sie die Protokollstufe ändern möchten. Normalerweise würden Sie das auf der Wurzel tun:

logging.getLogger().setLevel(logging.DEBUG)
Martijn Pieters
quelle
1
Siehe auch die Antwort von @sfinkens (und meinen Kommentar dort) für den Fall, dass Sie nur die setLevel für einen bestimmten Handler ändern möchten.
Starman
25

Zusätzlich zur akzeptierten Antwort: Abhängig davon, wie Sie den Logger initialisiert haben, müssen Sie möglicherweise auch die Handler des Loggers aktualisieren:

import logging

level = logging.DEBUG
logger = logging.getLogger()
logger.setLevel(level)
for handler in logger.handlers:
    handler.setLevel(level)
sfinkens
quelle
1
Nützlich: Wenn Sie eine dictConfig verwendet haben, um Ihre Logger, Handler usw. anzugeben, und Sie nur Level für einen bestimmten Handler festlegen möchten, können Sie die Funktion .get_name () in einer Schleife verwenden, die hier von @sfinkens gezeigt wird, und make Sicher, Sie ändern nur die Stufe der gewünschten.
Starman
Warum sollten Handler ein logLevel benötigen? Warum kann ihr Level außerdem unabhängig von dem Logger eingestellt werden, dem sie gehören? Das ist furchtbar verwirrend und schlechtes Design.
CodeKid
1
@CodeKid Was ist, wenn Sie sich in einer Datei auf DEBUG-Ebene, aber auf einer anderen Ebene in der Konsole anmelden möchten? Würden Sie nicht zwei Handler mit unterschiedlichen Ebenen anbringen?
Supermitch
8

Wenn Sie die Antwort von sfinken und den nachfolgenden Kommentar von Starman erweitern, können Sie auch den Typ des Handlers überprüfen, der auf einen bestimmten Outputter abzielt - zum Beispiel:

import logging
logger = logging.getLogger()
for handler in logger.handlers:
    if isinstance(handler, type(logging.StreamHandler())):
        handler.setLevel(logging.DEBUG)
        logger.debug('Debug logging enabled')
DrOffler
quelle
Genau das, was ich brauchte, um die Debug-Protokollierung automatisch zu aktivieren, wenn ich ohne das Flag -o ausgeführt werde
takanuva15
7

Es ist sicherlich möglich, die fileConfig()Protokollierungskonfiguration im laufenden Betrieb zu ändern, obwohl für einfache Änderungen ein programmatischer Ansatz geeignet sein könnte, wie in der Antwort von Martijn Pieters vorgeschlagen. Die Protokollierung bietet sogar einen Socket-Server, der mithilfe der listen()/ stopListening()APIs auf Konfigurationsänderungen wartet, wie hier dokumentiert . Sie verwenden die Protokollierung, um einen bestimmten Port abzuhören

t = logging.config.listen(PORT_NUMBER)
t.start()

und um nicht mehr zuzuhören, rufen Sie an

logging.config.stopListening()

Um Daten an den Server zu senden, können Sie z

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', PORT_NUMBER))
with open(CONFIG_FILE) as f:
    data_to_send = f.read()
s.send(struct.pack('>L', len(data_to_send)))
s.send(data_to_send)
s.close()

Aktualisieren: Aufgrund von Abwärtskompatibilitätsbeschränkungen fileConfig()bedeutet die interne Implementierung des Aufrufs, dass Sie disable_existing_loggers=Falseim Aufruf keine Angaben machen können , wodurch diese Funktion in bestimmten Szenarien weniger nützlich ist. Sie können dieselbe API verwenden, um eine JSON-Datei mithilfe des dictConfig-Schemas zu senden, wodurch eine bessere Kontrolle über die Neukonfiguration ermöglicht wird. Dies erfordert Python 2.7 / 3.2 oder höher (wo dictConfig()hinzugefügt wurde). Oder Sie können den stdlib-Code verwenden, um Ihren eigenen Listener zu implementieren, der auf die gleiche Weise funktioniert, aber auf Ihre spezifischen Anforderungen zugeschnitten ist.

Vinay Sajip
quelle
1
Wow, ich habe diese Antwort heute früher gesehen, aber das Update nicht bemerkt. Ich hatte mit genau diesem Problem zu kämpfen. Es wurde ein Fehler gemeldet, um die Option hinzuzufügen. bugs.python.org/issue26533 Jetzt habe ich das Problem auf hinterhältige Weise
umgangen
python def my_fileConfig(fname, defaults=None, disable_existing_loggers=False): """ This does _NOTHING_ but call fileConfig with disable_existing_loggers set to False instead of true. It is intended to monkey patcn it out, so that the config listener can be used without disabling all non-root loggers. """ orig_fileConfig(fname, defaults, disable_existing_loggers) orig_fileConfig = logging.config.fileConfig # HACK: Patch out fileConfig for my version. logging.config.fileConfig = my_fileConfig logging.config.fileConfig(config_path)
Gdogg
3

Dies könnte das sein, wonach Sie suchen:

import logging
logging.getLogger().setLevel(logging.INFO)

Beachten Sie, dass getLogger()aufgerufen ohne Argumente den Root-Logger zurückgibt.

R. Max
quelle
2

Ich habe mich schließlich dafür entschieden, mit inotify und gevent nach dem Dateischreibvorgang zu suchen. Sobald ich weiß, dass die Datei geändert wurde, lege ich die Ebene für jeden Logger fest, den ich basierend auf der Konfiguration habe.

import gevent
import gevent_inotifyx as inotify
from gevent.queue import Queue

class FileChangeEventProducer(gevent.Greenlet):
    def __init__(self, fd, queue):
        gevent.Greenlet.__init__(self)
        self.fd = fd
        self.queue = queue

    def _run(self):
        while True:
            events = inotify.get_events(self.fd)
            for event in events:
                self.queue.put(event)
                gevent.sleep(0)


class FileChangeEventConsumer(gevent.Greenlet):
    def __init__(self, queue, callBack):
        gevent.Greenlet.__init__(self)
        self.queue = queue
        self.callback = callBack

    def _run(self):
        while True:
            _ = self.queue.get()
            self.callback()
            gevent.sleep(0)


class GeventManagedFileChangeNotifier:
    def __init__(self, fileLocation, callBack):
        self.fileLocation = fileLocation
        self.callBack = callBack
        self.queue = Queue()
        self.fd = inotify.init()
        self.wd = inotify.add_watch(self.fd, self.fileLocation, inotify.IN_CLOSE_WRITE)


    def start(self):
        producer = FileChangeEventProducer(self.fd, self.queue)
        producer.start()
        consumer = FileChangeEventConsumer(self.queue, self.callBack)
        consumer.start()
        return (producer, consumer)

Der obige Code wird wie folgt verwendet:

    def _setUpLoggingConfigFileChangeNotifier(self):
        loggingFileNameWithFullPath = self._getFullPathForLoggingConfig()
        self.gFsNotifier = GeventManagedFileChangeNotifier(loggingFileNameWithFullPath, self._onLogConfigChanged)
        self.fsEventProducer, self.fsEventConsumer = self.gFsNotifier.start()


    def _onLogConfigChanged(self):
        self.rootLogger.info('Log file config has changed - examining the changes')
        newLoggingConfig = Config(self.resourcesDirectory, [self.loggingConfigFileName]).config.get('LOG')
        self.logHandler.onLoggingConfigChanged(newLoggingConfig)

Sobald ich die neue Protokolldateikonfiguration habe, kann ich die richtige Protokollierungsstufe für jeden Protokollierer von config verkabeln. Ich wollte nur die Antwort teilen und es könnte jemandem helfen, wenn er versucht, sie mit gevent zu verwenden.

opensourcegeek
quelle
0

Abhängig von Ihrer App müssen Sie zuerst einen Weg finden, um diese Datei neu zu laden oder die Protokollebene basierend auf Ihrer eigenen Konfigurationsdatei während der Ausführung zurückzusetzen.

Am einfachsten wäre es, einen Timer zu verwenden. Verwenden Sie dazu entweder Threading oder lassen Sie Ihr Async-Framework dies tun (falls Sie eines verwenden; es wird normalerweise implementiert).

Threading verwenden.Timer:

import threading
import time


def reset_level():
    # you can reload your own config file or use logging.config.fileConfig here
    print 'Something else'
    pass


t = threading.Timer(10, reset_level)
t.start()

while True:
    # your app code
    print 'Test'
    time.sleep(2)

Ausgabe:

Test
Test
Test
Test
Test
Something else
Test
Test

Update: Bitte überprüfen Sie die von Martijn Pieters vorgeschlagene Lösung.

Mihai
quelle
Ich denke nicht, dass dies die Frage vollständig beantwortet.
Jace Browning
Ich habe die obige Lösung verwendet und das Problem, mit dem ich konfrontiert bin (auf rhel5 mit Python 2.4.3 und unter Verwendung von ConcurrentLogHandler), ist, dass bei jedem Neuladen der Konfigurationsdatei ein Dateihandle hinzugefügt wird und nach einer Weile der Ausführung ein Ausnahme "Zu viele offene Dateien". Also stimme ich Travis Bear zu, dass die Antwort von Martijn Pieters die akzeptierte sein sollte.
Fabian76