logging.config Python Tutorial Teil 27

Bisher haben wir immer nur das basic logging von Python benutzt. Mit logging.config steht uns aber ein sehr viel umfangreicheres und komfortablerer Weg zur Verfügung.

Python Logo (CC-BY-SA The people from the Tango! project / Wikipedia)
Python Logo (CC-BY-SA The people from the Tango! project / Wikipedia)
1Python Programmierkurs
2Python: Methoden
3Kontrollstrukturen
4Strings in Python
5Container
6Objekte in Python
7Module
8Exceptions in Python
9Typkonvertierung
10Python und Dateien
11Datum und Zeit mit Python verarbeiten
12Multithreading
13Netzwerk in Python
14Logging in Python
15GPIO
16Automatische Tests
17Datenbanken mit Python
18Python: Generatoren und List Comprehension
19Python: Webseiten mit Flask
20Python virtuelle Umgebungen
21Interrupts & Signale
22NumPy
23Matplotlib
24match
25Reguläre Ausdrücke
26lambda Funktionen
27logging.config
28Decorators

logging.config

logging.config ist ein Modul, dass Python mitliefert. Mit seiner Hilfe kannst du Filter und Handler sehr viel präziser konfigurieren als mit dem basic logging, welches dich auf Console- und Dateilogging beschränkt.

Zunächst wollen wir uns mal anschauen, was im Hintergrund passiert, wenn du einen Logmeldung absetzt.

Workflow

opensource.com
workflowCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Logging Workflow CC-BY-SA 4.0 opensource.com

Nachdem logger.info() aufgerufen wurde, wird zuerst geprüft, ob der Loglevel auf INFO eingestellt ist, sonst wird gar nicht weitergemacht. Zunächst wird im weiteren Prozess ein LogRecord erstellt. Dieser enthält alle Informationen, die zu einem Logeintrag gehören (Zeitstempel, Text, Funktionsname usw.) Dieser wird dann an alle konfigurierten Handler weitergereicht, die ihrerseits zunächst den Loglevel überprüfen. Du kannst also für verschiedene Loglevel unterschiedliche Handler benutzen. Jeder Handler kann den LogRecord nach seinen Bedürfnissen filtern. Danach wird der LogRecord für die Verarbeitung formatiert und verarbeitet.

Nachdem alle Handler durchlaufen wurden, wird ggf. ein übergeordneter Handler aufgerufen. Dieser sogenannte root-Logger soll verhindern, dass Meldungen verloren gehen.

Praxisbeispiele

Zunächst wollen wir das bereits bekannte Beispiel aus Tutorial Teil 14 mit logging.config nachbauen:

# File: testlogging.py

import logging
from logging import config

# Logging Konfiguration definieren.

log_config = {
    "version":1,
# root-Logger
    "root":{
        "handlers" : ["console",'file'],
        "level": "DEBUG"
    },
    "handlers":{
#Handler für Console
        "console":{
            "formatter": "std_out",
            "class": "logging.StreamHandler",
            "level": "DEBUG"
        },
# Handler für Logdatei
        "file":{
            "formatter":"std_out",
            "class":"logging.FileHandler",
            "level":"DEBUG",
            "filename":"raspithek.log"
        },
    },
      
# Format einer Logzeile
    "formatters":{
        "std_out": {
            "format": "%(asctime)s : %(levelname)s : %(module)s : %(funcName)s : %(lineno)d : (Process Details : (%(process)d, %(processName)s), Thread Details : (%(thread)d, %(threadName)s))\nLog : %(message)s",
            "datefmt":"%d-%m-%Y %I:%M:%S"
        }
    },
}

# Config setzen
config.dictConfig(log_config)

# Logging testen
def testlogging():
  print('testlogging() aufgerufen.')
  logging.debug('DEBUG/Druckdaten werden  gesendet.')
  logging.info('INFO/Es werden 2 Seiten gedruckt')
  logging.warning('WARN/nicht genügend Papier im Drucker')
  logging.error('ERROR/Drucker nicht gefunden.')
  logging.critical('CRITICAL/Drucker brennt')

if __name__ == '__main__':
  testlogging()

Die Konfiguration des Logging geschieht am einfachsten mit einem Dictionary. In dicht_config werden alle Konfigurationsparameter zusammengefasst. Wir konfigurieren hier für den Root Logger die Handler für Console und File. Diese benutzen von Python gelieferte Klassen zur Verarbeitung des LogRecord. Beide Handler benutzen den selben Formatter std_out, um die Logzeile zu erzeugen.

Mit config.dictConfig(log_config) wird das Dictionary mit der Konfiguration an das Logging-System übergeben und gesetzt.

Was hier etwas unschön ist, ist, dass die Konfiguration im Pythoncode liegt, aber darum kümmern wir uns gleich. Jetzt testen wir das Programm erstmal mit python testlogging.py

Die Ausgabe von testloggingconfig.py ist der des Originalprogramms sehr ähnlich.
Olli Graf - raspithek.de
testloggingconfigCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Die Ausgabe von testloggingconfig.py ist der des Originalprogramms sehr ähnlich.

Filter

Jetzt wollen wir einen eigenen Filter ins Logging konfigurieren, der nur die Zeilen loggt, die das Wort „Druck“ in beliebiger Groß-/Kleinschreibung enthält.

Aber zunächst lagern wir die Logging Config in eine separate Datei log_config.json aus. Das Dictionary von oben kannst einfach in diese Datei kopieren.

{
    "version":1,
    "filters": {
        "drucker":{
            "()": "__main__.DruckFilter",
            "params": "noshow"
        }
    },
    "formatters":{
        "std_out":{
            "format": "%(asctime)s : %(levelname)s : %(module)s : %(funcName)s : %(lineno)d : (Process Details : (%(process)d, %(processName)s), Thread Details : (%(thread)d, %(threadName)s))\nLog : %(message)s",
            "datefmt":"%d-%m-%Y %I:%M:%S"
        }
    },
    "handlers":{
        "console":{
            "formatter": "std_out",
            "class": "logging.StreamHandler",
            "level": "DEBUG",
            "filters": ["drucker"]
        },
        "file":{
            "formatter":"std_out",
            "class":"logging.FileHandler",
            "level":"DEBUG",
            "filename" : "raspithek.log"
        }
    },
    "root":{
        "handlers":["console","file"],
        "level": "DEBUG"
    }
      
}

Neu ist hier nur die Sektion „filters“, die den „drucker“ Filter konfiguriert. Diesen Filter benutzen wir im „console“ Handler. Unser modifiziertes Programm enthält jetzt eine Klasse, mit dem DruckFilter. Außerdem wird die Logging Config als JSON Datei geladen.

#Datei: filterlogging.py
from logging import config
import json

# Filter definieren
class DruckFilter(logging.Filter):
  
  def __init__(self, params=None):
    self.param = params
    print('Druckfilter initialisiert')

  def filter(self, record:logging.LogRecord)-> bool| logging.LogRecord:

# nur Text, der das Wort Druck enthält soll geloggt werden.
    return record.getMessage().lower().find('druck') > -1


# Config aus JSON Datei laden und setzen

with open('log_config.json') as file_config:
  config.dictConfig(json.load(file_config))


# Logging testen
def testlogging():
  print('testlogging() aufgerufen.')
  logging.debug('Diese Zeile wird ausgefiltert.')
  logging.debug('DEBUG/Druckdaten werden  gesendet.')
  logging.info('INFO/Es werden 2 Seiten gedruckt')
  logging.debug('Diese Zeile wird auch ausgefiltert.')
  logging.warning('WARN/nicht genügend Papier im Drucker')
  logging.error('ERROR/Drucker nicht gefunden.')
  logging.critical('CRITICAL/Drucker brennt')

if __name__ == '__main__':
  testlogging()

Die Klasse DruckFilter leitet von logging.Filter ab und besitzt neben der __init__() Methode nur noch die Methode filter(self, record). Letztere bekommt den LogRecord übergeben und gibt ein True zurück, wenn der LogRecord weiterverarbeitet werden soll.

Wenn du das Programm startest, wirst du sehen, du sehen, dass die beiden Logzeilen, die den String „druck“ nicht enthalten, auch nicht in der Ausgabe erscheinen.

Der Filter lässt zwei Zeilen nicht auf der Console erscheinen, in der Logdatei stehen sie aber.
Olli Graf -raspithek.de
filterloggingCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Der Filter lässt zwei Zeilen nicht auf der Console erscheinen, in der Logdatei stehen sie aber.

JSON Formatter

Jack-o'-lantern (CC-BY-SA 3.0 Petar Milošević / Wikipedia)
Petar Milošević
Jack-o‘-lanternCreative Commons Attribution-ShareAlike 4.0 International License . loading=
Jack-o‘-lantern (CC-BY-SA 3.0 Petar Milošević / Wikipedia)

Ich will dir jetzt noch zeigen, wie du das Logfile im JSON Format schreiben kannst, wenn du es ggf. automatisiert weiterverarbeiten musst. Da dieser Post am 31.10. erscheinen soll, habe ich es etwas an diesen Tag angepasst. Es soll die kostümierten Wesen loggen, die zu Halloween an dir Tür kommen, um Süßigkeiten (Treat) zu erbitten. Das Programm wählt zufällig einen Treat aus der Liste aus und bestimmt eine Menge zwischen 0 und 20. Da ein Unsichtbarer keine Spuren im Logfile hinterlassen will, wird sein LogRecord durch den HalloweenFilter herausgefiltert. Der JsonFormatter sorgt für ein JSON kompatibles Logfile.

import logging
from logging import config
import json
import random
import JSONFormatter

#JSON Formatter
ATTR_TO_JSON = ['created', 'filename', 'funcName', 'levelname', 'lineno', 'module', 'msecs', 'msg', 'name', 'pathname', 'process', 'processName', 'relativeCreated', 'thread', 'threadName']
class JsonFormatter:
    def format(self, record):
        obj = {attr: getattr(record, attr)
                  for attr in ATTR_TO_JSON}
        return json.dumps(obj, indent=4)

#CustomFilter

class HalloweenFilter(logging.Filter):
  
  def __init__(self, params=None):
    self.param = params
    print('Halloweenfilter initialisiert')

  def filter(self, record:logging.LogRecord)-> bool| logging.LogRecord:

# Der Unsichtbare soll keine Spuren im Logfile hinterlassen.
    return record.getMessage().lower().find('unsichtbar') == -1

visitors = ['Werwolf','Gespenst','Hexe','Unsichtbarer','Vampir','Dämon','Gorilla','Monster','Mumie']

treats = ['Bonbons','Twinkies','Lollis','Schokoladen','Kuchen', 'Äpfel']

secrets = random.SystemRandom()

with open('halloween_log_conf.json') as file_config:
  config.dictConfig(json.load(file_config))
for v in visitors:
  logging.info(f'An der Tür klopft eine gruselige Gestalt: {v}')
  number = secrets._randbelow(20)
  treat = secrets.choice(treats)
  logging.info(f'Du gibst ihr {number} {treat} und sie zieht weiter')

Ein eigener Formatter ist im Prinzip nicht viel anders als ein Filter zu implementieren. Die Methode format() bekommt einen LogRecord und gibt den formatierten Text als String zurück. In der Liste ATTR_TO_JSON sind alle Attribute enthalten, die ins Logfile geschrieben werden sollen.Über eine List Comprehension werden diese in obj aufgebaut und danach mit json.dumps() in einen JSON-String umgewandelt.

Olli Graf - raspithek.de
halloween-logCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Ausgabe von halloween.py, Auf der Console taucht der Unsichtbare auf, im Logfile nicht.

Ein LogRecord im File sieht so aus:

{
    "created": 1728902304.6991022,
    "filename": "halloween.py",
    "funcName": "<module>",
    "levelname": "INFO",
    "lineno": 40,
    "module": "halloween",
    "msecs": 699.0,
    "msg": "Du gibst ihr 6 Lollis und sie zieht weiter",
    "name": "root",
    "pathname": "/home/pi/git/pythonkurs/teil27/halloween.py",
    "process": 3078,
    "processName": "MainProcess",
    "relativeCreated": 48.51651191711426,
    "thread": 4152119328,
    "threadName": "MainThread"
}

DBHandler

Zum Abschluss dieses Post will ich dir noch zeigen, wie du mit einem eigenen Handler direkt in die Datenbank loggen kannst. Dazu benutze ich die Datenbank pidb, dir ich im Kapitel 17 angelegt hatte. Zunächst benötigen wir die db.ini mit den Verbindungsparametern:

[pidb]
host=database
user=piuser
password=<passwort>
database=pidb

Unser DBHandler soll automatisch die Tabelle logs in der Maria DB anlegen und für jeden LogRecord eine Tabellenzeile schreiben. Als Grundlage nehme ich hier das Programm testlogging.py und baue dort die DBHandler Klasse ein.

# Datei dbhandler.py
import mysql.connector
import datetime
import configparser
import logging
from logging import config
import json




class DBHandler(logging.Handler):
    def __init__(self):
        logging.Handler.__init__(self)
        # Datenbank Konfiguration lesen
        confparser = configparser.ConfigParser()
        confparser.read('db.ini')
        # Verbindung zur DB herstellen
        self.connection= mysql.connector.connect(host=confparser['pidb']['host'],user = confparser['pidb']['user'],password = confparser['pidb']['password'],database = confparser['pidb']['database'])
        # Tabelle anlegen, falls noch keine existiert.
        self.connection.cursor().execute('''CREATE TABLE IF NOT EXISTS logs (
                                id INTEGER PRIMARY KEY AUTO_INCREMENT,
                                zeit VARCHAR(50),
                                level VARCHAR(8),
                                message VARCHAR(40),
                                logger_name VARCHAR(25),
                                filename VARCHAR(60),
                                line INTEGER)''')
        self.connection.commit()

    # Bei Löschung der Klasseninstanz, Connection schliessen
    def __del__(self):
     if self.connection != None:
       self.connection.close()
       self.connection = None

    # LogRecord in die DB schreiben
    def emit(self, record):
        try:
            # Log-Daten in ein Tupel extrahieren
            log_entry = (datetime.datetime.utcnow().isoformat(),
                         record.levelname,
                         record.getMessage(),
                         record.name,
                         record.pathname,
                         record.lineno)

            # Log in die Datenbank einfügen
            cursor = self.connection.cursor()
            cursor.execute('''INSERT INTO logs (zeit, level, message, logger_name, filename, line) 
                              VALUES (%s, %s, %s, %s, %s, %s)''', log_entry)
            self.connection.commit()
            cursor.close()

        except Exception as e:
            # im Fehlerfall den LogRecord nach "oben" weiterreichen.
            self.handleError(record)

# Config aus JSON Datei laden und setzen
with open('handler_config.json') as file_config:
  config.dictConfig(json.load(file_config))
# Logging testen
def testlogging():
  print('testlogging() aufgerufen.')
  logging.debug('Diese Zeile wird ausgefiltert.')
  logging.debug('DEBUG/Druckdaten werden  gesendet.')
  logging.info('INFO/Es werden 2 Seiten gedruckt')
  logging.debug('Diese Zeile wird auch ausgefiltert.')
  logging.warning('WARN/nicht genügend Papier im Drucker')
  logging.error('ERROR/Drucker nicht gefunden.')
  logging.critical('CRITICAL/Drucker brennt')

if __name__ == '__main__':
  testlogging()

Konfiguriert wird der Handler in dieser Datei

# Datei: handler_config.json
{
    "version":1,
    "formatters":{
        "std_out":{
            "format": "%(asctime)s : %(levelname)s : %(module)s : %(funcName)s : %(lineno)d : (Process Details : (%(process)d, %(processName)s), Thread Details : (%(thread)d, %(threadName)s))\nLog : %(message)s",
            "datefmt":"%d-%m-%Y %I:%M:%S"
        }
    },
    "handlers":{
        "console":{
            "formatter": "std_out",
            "class": "logging.StreamHandler",
            "level": "DEBUG"
        },
        "db":{
            "formatter": "std_out",
            "()": "__main__.DBHandler",
            "level": "DEBUG"
        },
        "file":{
            "formatter":"std_out",
            "class":"logging.FileHandler",
            "level":"DEBUG",
            "filename" : "raspithek.log"
        }
    },
    "root":{
        "handlers":["console","file","db"],
        "level": "DEBUG"
    }
      
}

In der __init__() Methode legt der Handler automatisch eine neue Datenbanktabelle an. In emit() nehmen wir den LogRecord entgegen und erzeuge die Tabellenzeile. Die Ausgabe auf der Console unterscheidet sich nicht vom obigen Beispiel, auch in der Logdatei stehen die gleichen Werte.

Interessant ist, was in der Datenbank passiert ist:

Olli Graf -raspithek.de
dbhandlerCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Der DBHandler hat alle LogRecord in die Datenbanktabelle geschrieben.

Fazit

logging.config bietet umfangreiche Möglichkeiten, das Logging zu verbessern. Du solltest allerdings im Hinterkopf behalten, dass dies nur auf Anwendungsebene sinnvoll ist. Es eignet sich nicht für das Logging in Modulen.

Und wie immer am Schluss der Hinweis auf das Git Repository. Das war’s für heute. Happy Halloween 🎃

Schreibe einen Kommentar

Creative Commons License
Except where otherwise noted, the content on this site is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Olli Graf - raspithek.de
WordPress Cookie Hinweis von Real Cookie Banner