Zum Inhalt springen
aviral gupta

// I4.6 · ca. 40 Min. · Aufbau

Logging und ein eigenes Log-grep

Nach dieser Lektion loggen Sie aus eigenen Programmen mit Loggern, Leveln, Handlern und Formattern, und Sie haben loggrep gebaut, ein Kommandozeilenwerkzeug, das Logdateien mit re, logging und argparse durchsucht.

Lektion 6 von 6 in I4 Die Standardbibliothek für echte Programme

Ende des Moduls

Danach können Sie

  • Über benannte Logger loggen und Level wählen, sodass nur Meldungen ab einer bestimmten Schwere erscheinen
  • Logging mit basicConfig einrichten: Level, Format und Stream
  • Einträge mit Handlern an mehrere Ziele schicken, jeden mit eigenem Level und Formatter
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen mit regulären Ausdrücken, die das Log-grep brauchen wird. Was gibt das aus?

    import re
    
    print(re.match(r"(\w+) (\w+)", "ERROR db").group(2))
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen. Nichts konfiguriert hier das Logging. Was erscheint auf dem Bildschirm?

    import logging
    
    logging.info("starting")
    logging.warning("disk almost full")
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie das Level, sodass info-Meldungen erscheinen, debug-Meldungen aber nicht.

    logging.basicConfig(level=logging.____, stream=sys.stdout, format="%(levelname)s %(message)s")
    logging.basicConfig(level=logging., stream=sys.stdout, format="%(levelname)s %(message)s")
  4. Üben · Aufgabe 4 von 7

    Ein eigenes Format. Was gibt das aus?

    import logging
    import sys
    
    logging.basicConfig(stream=sys.stdout, format="%(name)s [%(levelname)s] %(message)s")
    logging.getLogger("shop.cart").error("empty cart")
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Teil von logging seine Aufgabe zu.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Der Root-Logger hat einen Handler, der Logger app auch. Was gibt das aus?

    import logging
    import sys
    
    logging.basicConfig(stream=sys.stdout, format="root: %(message)s")
    log = logging.getLogger("app")
    log.addHandler(logging.StreamHandler(sys.stdout))
    log.warning("disk full")
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Geben Sie dem Logger backup zwei Handler: Einer gibt INFO und höher auf sys.stdout als "LEVEL: Meldung" aus, der andere schreibt nur Fehler nach errors.log, als "Name LEVEL Meldung". Loggen Sie eine debug-, eine info- und eine error-Meldung und geben Sie dann den Inhalt von errors.log aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Ein Shop, der auf den Bildschirm und in eine Datei loggt

Das Modul loggt über seinen eigenen Logger, shop. basicConfig schickt INFO und höher in einem eigenen Format auf den Bildschirm; stream=sys.stdout macht das Log hier nur als normale Ausgabe sichtbar. Ein zweiter Handler am Root-Logger schreibt Fehler, und nur Fehler, in anderem Format nach errors.log. Die debug-Meldung fällt weg, weil das Level INFO ist.

main.py

import logging
import sys

log = logging.getLogger("shop")


def checkout(cart: dict[str, int]) -> int:
    log.debug("cart has %d items", len(cart))
    if not cart:
        log.warning("empty cart, nothing to do")
        return 0
    total = sum(cart.values())
    log.info("checkout total %d cents", total)
    return total


logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(name)s: %(message)s", stream=sys.stdout)

# A second handler: errors only, into a file, with its own format.
errors = logging.FileHandler("errors.log", encoding="utf-8")
errors.setLevel(logging.ERROR)
errors.setFormatter(logging.Formatter("%(name)s|%(levelname)s|%(message)s"))
logging.getLogger().addHandler(errors)

checkout({"bread": 250, "coffee": 890})
checkout({})
log.error("payment service unreachable")
errors.close()

print("errors.log:")
with open("errors.log", encoding="utf-8") as f:
    print(f.read(), end="")

Ausführen mit

python main.py

Ausgabe

INFO    shop: checkout total 1140 cents
WARNING shop: empty cart, nothing to do
ERROR   shop: payment service unreachable
errors.log:
shop|ERROR|payment service unreachable
  • Die debug-Zeile fehlt: basicConfig hat das Level auf INFO gesetzt.
  • %(levelname)-7s füllt das Level auf sieben Zeichen auf, sodass die Meldungen bündig stehen.
  • Der Logger shop hat keinen eigenen Handler: Seine Einträge wandern zu den zwei Handlern des Root.
  • errors.log erhielt nur den Fehler, weil sein Handler das Level ERROR hat.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 3

Bauschritt 1: eine Logzeile lesen

Das Log-grep liest Zeilen wie 2026-09-29 10:17:12 ERROR db: connection lost. Schreiben Sie parse_line(line), das (Zeit, Level, Meldung) liefert, oder None für eine Zeile in anderer Form, etwa eine Traceback-Zeile. Schreiben Sie level_ok(level, minimum), das True ist, wenn level mindestens so schwer wiegt wie minimum. Der Startcode trennt an Leerzeichen und vergleicht Levelnamen als Text, wobei "ERROR" < "WARNING" gilt.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Ein regulärer Ausdruck mit drei Gruppen passt: Datum und Uhrzeit, ein großgeschriebenes Wort und der Rest. Nutzen Sie fullmatch, nach rstrip("\n").

  2. Hinweis 2

    logging.getLevelNamesMapping() bildet "DEBUG" auf 10 bis "CRITICAL" auf 50 ab. Vergleichen Sie diese Zahlen.

  3. Hinweis 3

    LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)"); geben Sie None zurück, wenn LINE.fullmatch(...) None ist.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import logging
import re

LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")


def parse_line(line: str) -> tuple[str, str, str] | None:
    match = LINE.fullmatch(line.rstrip("\n"))
    if match is None:
        return None
    return match.group(1), match.group(2), match.group(3)


def level_ok(level: str, minimum: str) -> bool:
    numbers = logging.getLevelNamesMapping()
    return numbers.get(level, 0) >= numbers[minimum]


if __name__ == "__main__":
    print(parse_line("2026-09-29 10:17:12 ERROR db: connection lost\n"))
    print(parse_line("Traceback (most recent call last):\n"))
    print(level_ok("ERROR", "WARNING"), level_ok("INFO", "WARNING"))
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

def parse_line(line: str) -> tuple[str, str, str] | None:
    date, time, level, message = line.split(" ", 3)
    return f"{date} {time}", level, message


def level_ok(level: str, minimum: str) -> bool:
    return level >= minimum


if __name__ == "__main__":
    print(parse_line("2026-09-29 10:17:12 ERROR db: connection lost\n"))
    print(parse_line("Traceback (most recent call last):\n"))
    print(level_ok("ERROR", "WARNING"), level_ok("INFO", "WARNING"))

test_main.py

from main import level_ok, parse_line


def test_parse_line():
    """Eine Logzeile zerfällt in Zeit, Level und Meldung"""
    got = parse_line("2026-09-29 10:17:12 ERROR db: connection lost\n")
    want = ("2026-09-29 10:17:12", "ERROR", "db: connection lost")
    assert got == want, f"parse_line lieferte {got!r}"


def test_not_a_log_line():
    """Eine Traceback-Zeile oder eine leere Zeile ergibt None"""
    for line in ["Traceback (most recent call last):\n", "\n", "  File \"app.py\", line 3\n"]:
        got = parse_line(line)
        assert got is None, f"für {line!r} lieferte parse_line {got!r}, erwartet war None"


def test_level_ok():
    """ERROR besteht ein Minimum WARNING, INFO nicht"""
    assert level_ok("ERROR", "WARNING") is True, "level_ok('ERROR', 'WARNING') sollte True sein"
    assert level_ok("INFO", "WARNING") is False, "level_ok('INFO', 'WARNING') sollte False sein"


def test_level_ok_edges():
    """Das Minimum selbst besteht, CRITICAL besteht alles"""
    assert level_ok("DEBUG", "DEBUG") is True, "level_ok('DEBUG', 'DEBUG') sollte True sein"
    assert level_ok("CRITICAL", "ERROR") is True, "level_ok('CRITICAL', 'ERROR') sollte True sein"
    assert level_ok("DEBUG", "INFO") is False, "level_ok('DEBUG', 'INFO') sollte False sein"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 3

Bauschritt 2: die Zeilen durchsuchen

Schritt 1 steht fertig im Startcode. Schreiben Sie grep(lines, pattern, minimum="DEBUG", ignore_case=False): Die Funktion liefert die Logzeilen ohne "\n", deren Level minimum besteht und deren Meldung zum regulären Ausdruck pattern passt. ignore_case=True ignoriert Groß- und Kleinschreibung. Zeilen, die keine Logzeilen sind, werden übersprungen und jeweils mit log.debug("line %d skipped: not a log line", number) gemeldet, gezählt ab 1. Führen Sie es aus, um app.log (zweiter Tab) zu durchsuchen.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Kompilieren Sie das Muster einmal mit re.compile(pattern, flags), wobei flags re.IGNORECASE oder 0 ist.

  2. Hinweis 2

    Laufen Sie mit enumerate(lines, start=1), dann haben Sie die Zeilennummer für die debug-Meldung.

  3. Hinweis 3

    Für jede Zeile: parse_line; bei None loggen und continue; sonst die Zeile behalten, wenn level_ok(level, minimum) und regex.search(message) gelten.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import logging
import re
from collections.abc import Iterable

log = logging.getLogger("loggrep")

LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")


def parse_line(line: str) -> tuple[str, str, str] | None:
    match = LINE.fullmatch(line.rstrip("\n"))
    if match is None:
        return None
    return match.group(1), match.group(2), match.group(3)


def level_ok(level: str, minimum: str) -> bool:
    numbers = logging.getLevelNamesMapping()
    return numbers.get(level, 0) >= numbers[minimum]


def grep(lines: Iterable[str], pattern: str, minimum: str = "DEBUG", ignore_case: bool = False) -> list[str]:
    regex = re.compile(pattern, re.IGNORECASE if ignore_case else 0)
    found = []
    for number, line in enumerate(lines, start=1):
        parsed = parse_line(line)
        if parsed is None:
            log.debug("line %d skipped: not a log line", number)
            continue
        stamp, level, message = parsed
        if level_ok(level, minimum) and regex.search(message):
            found.append(line.rstrip("\n"))
    return found


if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
    with open("app.log", encoding="utf-8") as f:
        for line in grep(f, r"timeout|lost", minimum="INFO", ignore_case=True):
            print(line)
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import logging
import re
from collections.abc import Iterable

log = logging.getLogger("loggrep")

LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")


def parse_line(line: str) -> tuple[str, str, str] | None:
    match = LINE.fullmatch(line.rstrip("\n"))
    if match is None:
        return None
    return match.group(1), match.group(2), match.group(3)


def level_ok(level: str, minimum: str) -> bool:
    numbers = logging.getLevelNamesMapping()
    return numbers.get(level, 0) >= numbers[minimum]


def grep(lines: Iterable[str], pattern: str, minimum: str = "DEBUG", ignore_case: bool = False) -> list[str]:
    return [line.rstrip("\n") for line in lines if pattern in line]


if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
    with open("app.log", encoding="utf-8") as f:
        for line in grep(f, r"timeout|lost", minimum="INFO", ignore_case=True):
            print(line)

test_main.py

import unittest

from main import grep

LOG = [
    "2026-09-29 10:15:02 INFO web: server started on port 8000\n",
    "2026-09-29 10:16:03 WARNING db: slow query took 2.4 s\n",
    "Traceback (most recent call last):\n",
    "2026-09-29 10:17:12 ERROR db: connection lost\n",
    "2026-09-29 10:18:55 ERROR web: request Timeout for /api/orders\n",
]


def test_regex():
    """Das Muster ist ein regulärer Ausdruck, gesucht in der Meldung"""
    got = grep(LOG, r"lost|slow")
    want = ["2026-09-29 10:16:03 WARNING db: slow query took 2.4 s", "2026-09-29 10:17:12 ERROR db: connection lost"]
    assert got == want, f"grep(LOG, 'lost|slow') lieferte {got!r}"


def test_minimum_level():
    """minimum='ERROR' lässt die WARNING-Zeile weg"""
    got = grep(LOG, "db:", minimum="ERROR")
    assert got == ["2026-09-29 10:17:12 ERROR db: connection lost"], f"mit minimum='ERROR' lieferte grep {got!r}"


def test_ignore_case():
    """ignore_case=True findet Timeout für das Muster timeout"""
    assert grep(LOG, "timeout") == [], "ohne ignore_case sollte timeout nicht zu Timeout passen"
    got = grep(LOG, "timeout", ignore_case=True)
    assert got == ["2026-09-29 10:18:55 ERROR web: request Timeout for /api/orders"], f"mit ignore_case=True lieferte grep {got!r}"


def test_skipped_line_is_logged():
    """Die Traceback-Zeile wird mit einer DEBUG-Meldung am Logger loggrep übersprungen"""
    with unittest.TestCase().assertLogs("loggrep", level="DEBUG") as logs:
        grep(LOG, "Traceback")
    assert logs.output == ["DEBUG:loggrep:line 3 skipped: not a log line"], f"der Logger loggrep zeichnete {logs.output!r} auf"

app.log

2026-09-29 10:15:02 INFO web: server started on port 8000
2026-09-29 10:15:40 DEBUG db: pool size 5
2026-09-29 10:16:03 WARNING db: slow query took 2.4 s
2026-09-29 10:17:12 ERROR db: connection lost
Traceback (most recent call last):
2026-09-29 10:17:13 INFO db: reconnected after timeout
2026-09-29 10:18:55 ERROR web: request timeout for /api/orders

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 3 von 3

Bauschritt 3: die Kommandozeile

Vollenden Sie loggrep mit main(argv=None) -> int: python main.py MUSTER DATEI [DATEI ...] [--level LEVEL] [-i] [-v]. Geben Sie jeden Treffer als "Datei: Zeile" aus. --level nimmt DEBUG bis CRITICAL; -v stellt Logging mit basicConfig auf DEBUG, sonst auf WARNING. Eine fehlende Datei wird mit log.error("%s: no such file", name) geloggt, die übrigen Dateien werden trotzdem durchsucht. Rückgabe: 0 bei einem Treffer, 1 ohne Treffer, 2 nach einer fehlenden Datei. Führen Sie es auf Ihrem Rechner aus, etwa python main.py timeout app.log -i -v.

Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Fügen Sie --level mit choices=LEVELS und default="DEBUG" hinzu, dazu -i und -v mit action="store_true".

  2. Hinweis 2

    Rufen Sie logging.basicConfig(level=..., format="%(levelname)s: %(message)s") direkt nach parse_args auf; es schreibt nach stderr und vermischt sich nie mit den Treffern.

  3. Hinweis 3

    Umschließen Sie das open mit try/except FileNotFoundError, merken Sie sich failed = True und machen Sie mit continue weiter. Am Ende: 2 bei failed, sonst 0 bei Treffern, sonst 1.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import argparse
import logging
import re
import sys
from collections.abc import Iterable

log = logging.getLogger("loggrep")

LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")
LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]


def parse_line(line: str) -> tuple[str, str, str] | None:
    match = LINE.fullmatch(line.rstrip("\n"))
    if match is None:
        return None
    return match.group(1), match.group(2), match.group(3)


def level_ok(level: str, minimum: str) -> bool:
    numbers = logging.getLevelNamesMapping()
    return numbers.get(level, 0) >= numbers[minimum]


def grep(lines: Iterable[str], pattern: str, minimum: str = "DEBUG", ignore_case: bool = False) -> list[str]:
    regex = re.compile(pattern, re.IGNORECASE if ignore_case else 0)
    found = []
    for number, line in enumerate(lines, start=1):
        parsed = parse_line(line)
        if parsed is None:
            log.debug("line %d skipped: not a log line", number)
            continue
        stamp, level, message = parsed
        if level_ok(level, minimum) and regex.search(message):
            found.append(line.rstrip("\n"))
    return found


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(prog="loggrep", description="Search log files.")
    parser.add_argument("pattern", help="regular expression to search the messages for")
    parser.add_argument("files", nargs="+", help="log files to search")
    parser.add_argument("--level", default="DEBUG", choices=LEVELS, help="lowest level to show")
    parser.add_argument("-i", "--ignore-case", action="store_true")
    parser.add_argument("-v", "--verbose", action="store_true", help="log what loggrep does")
    args = parser.parse_args(argv)
    logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING, format="%(levelname)s: %(message)s")

    matched = False
    failed = False
    for name in args.files:
        try:
            with open(name, encoding="utf-8") as f:
                found = grep(f, args.pattern, args.level, args.ignore_case)
        except FileNotFoundError:
            log.error("%s: no such file", name)
            failed = True
            continue
        for line in found:
            print(f"{name}: {line}")
        matched = matched or bool(found)
    if failed:
        return 2
    return 0 if matched else 1


if __name__ == "__main__":
    sys.exit(main())
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import argparse
import logging
import re
import sys
from collections.abc import Iterable

log = logging.getLogger("loggrep")

LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")
LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]


def parse_line(line: str) -> tuple[str, str, str] | None:
    match = LINE.fullmatch(line.rstrip("\n"))
    if match is None:
        return None
    return match.group(1), match.group(2), match.group(3)


def level_ok(level: str, minimum: str) -> bool:
    numbers = logging.getLevelNamesMapping()
    return numbers.get(level, 0) >= numbers[minimum]


def grep(lines: Iterable[str], pattern: str, minimum: str = "DEBUG", ignore_case: bool = False) -> list[str]:
    regex = re.compile(pattern, re.IGNORECASE if ignore_case else 0)
    found = []
    for number, line in enumerate(lines, start=1):
        parsed = parse_line(line)
        if parsed is None:
            log.debug("line %d skipped: not a log line", number)
            continue
        stamp, level, message = parsed
        if level_ok(level, minimum) and regex.search(message):
            found.append(line.rstrip("\n"))
    return found


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(prog="loggrep", description="Search log files.")
    parser.add_argument("pattern", help="regular expression to search the messages for")
    parser.add_argument("files", nargs="+", help="log files to search")
    args = parser.parse_args(argv)
    for name in args.files:
        with open(name, encoding="utf-8") as f:
            for line in grep(f, args.pattern):
                print(f"{name}: {line}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

test_main.py

import contextlib
import io
import unittest

from main import main

LOG = (
    "2026-09-29 10:16:03 WARNING db: slow query took 2.4 s\n"
    "2026-09-29 10:17:12 ERROR db: connection lost\n"
    "2026-09-29 10:17:13 INFO db: reconnected after timeout\n"
)


def run(argv):
    with open("t_app.log", "w", encoding="utf-8") as f:
        f.write(LOG)
    out = io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
        try:
            status = main(argv)
        except SystemExit as stop:
            status = stop.code
    return status, out.getvalue().splitlines()


def test_match():
    """Treffer werden mit dem Dateinamen ausgegeben, Status 0"""
    status, lines = run(["db", "t_app.log", "--level", "WARNING"])
    want = ["t_app.log: 2026-09-29 10:16:03 WARNING db: slow query took 2.4 s", "t_app.log: 2026-09-29 10:17:12 ERROR db: connection lost"]
    assert (status, lines) == (0, want), f"main lieferte {status!r} und gab {lines!r} aus"


def test_no_match():
    """Ohne Treffer: Status 1 und keine Ausgabe"""
    status, lines = run(["disk", "t_app.log"])
    assert (status, lines) == (1, []), f"main lieferte {status!r} und gab {lines!r} aus"


def test_ignore_case():
    """-i findet das kleingeschriebene timeout für TIMEOUT"""
    status, lines = run(["-i", "TIMEOUT", "t_app.log"])
    assert status == 0 and len(lines) == 1, f"mit -i lieferte main {status!r} und gab {lines!r} aus"


def test_missing_file():
    """Eine fehlende Datei wird als Fehler an loggrep geloggt, Status 2"""
    with unittest.TestCase().assertLogs("loggrep", level="ERROR") as logs:
        status, lines = run(["db", "missing.log", "t_app.log"])
    assert status == 2, f"mit einer fehlenden Datei lieferte main {status!r}, erwartet war 2"
    assert logs.output == ["ERROR:loggrep:missing.log: no such file"], f"der Logger loggrep zeichnete {logs.output!r} auf"
    assert len(lines) == 3, f"die andere Datei sollte trotzdem durchsucht werden, aber main gab {lines!r} aus"

app.log

2026-09-29 10:15:02 INFO web: server started on port 8000
2026-09-29 10:15:40 DEBUG db: pool size 5
2026-09-29 10:16:03 WARNING db: slow query took 2.4 s
2026-09-29 10:17:12 ERROR db: connection lost
Traceback (most recent call last):
2026-09-29 10:17:13 INFO db: reconnected after timeout
2026-09-29 10:18:55 ERROR web: request timeout for /api/orders

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Ein Levelname in Kleinbuchstaben

import logging

logging.getLogger("app").setLevel("info")

Was Python ausgibt

ValueError: Unknown level: 'info'

Warum, und die Lösung

setLevel und basicConfig(level=...) nehmen einen Levelnamen nur großgeschrieben an, "INFO", denn so sind die Level registriert. Besser ist die Konstante logging.INFO: Ein Tippfehler dort ist sofort ein AttributeError statt eines verwirrenden ValueError.

Ein Layout im Stil von str.format in einem Formatter

import logging
import sys

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("{levelname}: {message}"))

Was Python ausgibt

ValueError: Invalid format '{levelname}: {message}' for '%' style

Warum, und die Lösung

Standardmäßig nutzen ein Formatter und basicConfig(format=...) Felder im %-Stil: "%(levelname)s: %(message)s". Geschweifte Klammern funktionieren nur, wenn Sie zusätzlich style="{" übergeben. Die Prüfung läuft beim Erzeugen des Formatters, der Fehler zeigt sich also sofort.

Eine Logdatei in einem Ordner, den es nicht gibt

import logging

handler = logging.FileHandler("logs/app.log")
logging.getLogger("app").addHandler(handler)

Was Python ausgibt

No such file or directory

Warum, und die Lösung

FileHandler legt die Datei an, aber nicht den Ordner darum, und löst deshalb FileNotFoundError aus. Legen Sie den Ordner zuerst an: Path("logs").mkdir(exist_ok=True), mit from pathlib import Path.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Logger und Level

Ein Logger ist das, womit Ihr Code spricht. Holen Sie sich einen pro Modul mit log = logging.getLogger(__name__); derselbe Name liefert immer dasselbe Objekt. Jede Meldung hat ein Level: debug, info, warning, error oder critical, von 10 bis 50. Ein Logger mit setLevel(logging.WARNING) verwirft alles unter WARNING, und ohne jede Konfiguration erscheinen nur WARNING und höher. Übergeben Sie Werte als Argumente: log.info("%d files copied", count). logging setzt sie erst ein, wenn die Meldung wirklich geschrieben wird.

basicConfig

logging.basicConfig richtet den Root-Logger einmal ein, am Anfang des Programms: level= legt das niedrigste gezeigte Level fest, format= das Layout, etwa "%(levelname)s %(name)s: %(message)s", und stream= oder filename= das Ziel. Ohne stream= schreibt es nach sys.stderr, sodass Logmeldungen nicht in die eigentliche Ausgabe des Programms geraten. basicConfig tut nichts, wenn der Root-Logger schon Handler hat; ein zweiter Aufruf wird also stillschweigend ignoriert. Konfigurieren Sie Logging im Hauptteil des Programms, nie in einem Modul, das anderer Code importiert.

Handler und Formatter

Ein Handler entscheidet, wohin ein Eintrag geht: StreamHandler in einen Stream wie sys.stderr, FileHandler in eine Datei. Ein Formatter entscheidet, wie er aussieht; Sie hängen ihn mit handler.setFormatter an. Ein Handler hat ein eigenes Level, sodass ein Logger INFO und höher ausgibt, während eine Datei nur Fehler erhält. Einträge wandern auch zu den Handlern der Eltern-Logger: ein Handler an "app" und einer am Root geben eine Meldung zweimal aus. Setzen Sie am Logger propagate = False, oder hängen Sie Handler nur an den Root.

Quellen

Zuletzt geprüft am 29. September 2026