---
title: "Strukturiertes Logging mit Log-Format und Log-Leveln, die du nach Feld durchsuchst"
description: "Strukturiertes Logging legt jeden Wert in ein eigenes Feld statt in einen Satz. Wir haben 1 Bestellung auf 3 Arten an LogBrew gesendet und zeigen, welche du über order_id wiederfindest."
canonical: "https://logbrew.co/de/guides/structured-logging"
last_updated: "2026-11-10"
---

# Strukturiertes Logging mit Log-Format und Log-Leveln, die du nach Feld durchsuchst

Strukturiertes Logging legt jeden Wert in ein eigenes Feld statt in einen Satz. Wir haben 1 Bestellung auf 3 Arten an LogBrew gesendet und zeigen, welche du über `order_id` wiederfindest.

Getestet am 10. November 2026 mit Python 3.13.5 und logbrew-sdk 0.1.29

Von [Furkan Erday](https://github.com/furkanerday). Furkan Erday hat LogBrew gegründet. Er entwickelt LogBrew jeden Tag mit Claude Code und Codex.

Canonical: https://logbrew.co/de/guides/structured-logging

## Welche Log-Zeile du über `order_id` wiederfindest

| Wo die ID stand | Textsuche | Feldfilter |
| --- | --- | --- |
| Im Nachrichtentext | Gefunden | Nicht gefunden |
| In `extra={}`-Feldern | Nicht gefunden | Gefunden |
| In einem Dict innerhalb von `extra={}` | Nicht gefunden | Nicht gefunden |

Wir haben 1 Bestellung auf 3 Arten durch den Python-Handler geschickt und dann danach gesucht. Eine volle Tasse heißt, die Suche hat die Zeile gefunden.

## Ein Log-Format wählen, das du filtern kannst

### Die Meldung fest lassen, die Werte in Felder legen

Ein Feld behält seinen Namen und seinen Typ. Ein Wert im Nachrichtentext ist nur ein Teilstring. In Python übergibst du die Felder in `extra={}`, und die Meldung bleibt für jede Bestellung gleich.

```bash
log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

### JSON-Zeilen schreiben, wenn ein Programm das Log liest

Eine `Formatter`-Unterklasse macht aus jedem Record 1 JSON-Objekt pro Zeile, mit den Standardfeldern und jedem `extra`-Wert. Ein Log-Collector parst die Zeile ohne Regex.

```bash
import json
import logging

RECORD_FIELDS = set(vars(logging.LogRecord("", 0, "", 0, "", (), None))) | {"message", "asctime"}


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        line = {
            "time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        line.update({k: v for k, v in vars(record).items() if k not in RECORD_FIELDS})
        if record.exc_info:
            line["exception"] = self.formatException(record.exc_info)
        return json.dumps(line)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
log.warning("keg below 10%", extra={"keg": "ipa", "percent": 8})
```

Ausgabe

```text
{"time": "2026-11-10T08:34:04+0300", "level": "INFO", "logger": "taproom", "message": "order paid", "order_id": "ord_42", "amount_cents": 1999}
{"time": "2026-11-10T08:34:04+0300", "level": "WARNING", "logger": "taproom", "message": "keg below 10%", "keg": "ipa", "percent": 8}
```

### Die Felder an LogBrew senden

`LogBrewLoggingHandler` sendet jeden Record mit seinen `extra`-Werten als Felder. Wir haben dieselbe Bestellung auf 3 Arten gesendet, im Nachrichtentext, als `extra`-Felder und als Dict innerhalb von `extra`.

```bash
import logging
import os

from logbrew_sdk import HttpTransport, LogBrewClient, LogBrewLoggingHandler

client = LogBrewClient.create(api_key=os.environ["LOGBREW_SERVER_API_KEY"])
transport = HttpTransport()
log = logging.getLogger("taproom")
log.addHandler(LogBrewLoggingHandler(client, transport, metadata={"service": "taproom"}))
log.setLevel(logging.INFO)

logging.getLogger("taproom.text").info("order %s paid, %d cents", "ord_42", 1999)
logging.getLogger("taproom.fields").info(
    "order paid", extra={"order_id": "ord_42", "amount_cents": 1999}
)
logging.getLogger("taproom.nested").info(
    "order paid", extra={"order": {"id": "ord_42", "amount_cents": 1999}}
)

response = client.flush(transport)
print("HTTP", response.status_code, "accepted", response.accepted_events, "logs")
```

Ausgabe

```text
HTTP 200 accepted 3 logs
```

### Die Zeile über ein Feld wiederfinden

`GET /api/logs` nimmt `attribute_filters` mit Feldpfad, Typ, Operator und Wert an. Der Filter auf `order_id` fand nur die Zeile, die mit `extra` gesendet wurde. Das Dict innerhalb von `extra` wurde verworfen, weil der Handler nur Strings, Zahlen, Booleans und `None` behält.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/order_id","type":"string","operator":"eq","value":"ord_42"}]' \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

Ausgabe

```text
{"message":"order paid","order_id":"ord_42"}
```

## Log-Level in jedem Dienst gleich verwenden

### In Produktion 4 Level verwenden

`info` für das, was passiert ist, `warning` für das, was schiefging, aber behandelt wurde, `error` für eine fehlgeschlagene Anfrage oder einen fehlgeschlagenen Job und `critical` für das, was den Dienst stoppt. Behalte `debug` für lokale Läufe. Der LogBrew-Handler sendet DEBUG-Records als `info`.

### Den Level einmal setzen, beim Start

Lies den Level aus der Umgebung, damit 1 Host auf DEBUG laufen kann, während die übrigen auf INFO bleiben. In Python erledigt das `logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))`.

### Zahlen als Zahlen loggen

`amount_cents: 1999` behält seinen Typ, also trifft der Filter `amount_cents >= 1000` ihn. Wir haben auch `amount_cents: "1999"` als Text gesendet, und der Zahlenfilter hat diese Zeile übersprungen.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/amount_cents","type":"number","operator":"gte","value":1000}]' \
  | jq -c '.[] | {message, amount_cents: .attributes.metadata.amount_cents}'
```

Ausgabe

```text
{"message":"order paid","amount_cents":1999}
```

## Was wir beim Testen gesehen haben

Wir haben 1 Bestellung auf 3 Arten durch den Python-Handler geschickt und dann mit der LogBrew-API danach gesucht. Die Textsuche `search=ord_42` traf 1 der 3 Zeilen, die mit der ID im Nachrichtentext.

Der Filter `order_id = ord_42` traf 1 Zeile, die mit `extra={"order_id": "ord_42"}` gesendete. Der Filter `amount_cents >= 1000` traf dieselbe Zeile, weil der Handler 1999 als Zahl behalten hat.

Die mit `extra={"order": {...}}` gesendete Zeile traf nichts. Der Handler sendet Strings, Zahlen, Booleans und `None` aus `extra` und überspringt ein Dict oder eine Liste.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode "search=ord_42" \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

Ausgabe

```text
{"message":"order ord_42 paid, 1999 cents","order_id":null}
```

## Klartext vs. JSON als Log-Format

Ein reines `%(message)s`-Format verwirft jeden `extra`-Wert. Die Zeile unten endet in `INFO order paid` ohne Bestell-ID, also findet später niemand die Bestellung.

Eine JSON-Zeile behält die Felder und ihre Typen, und ein Log-Collector parst sie ohne Regex. Ein Mensch liest sie weniger leicht, also halte die Meldung kurz und die Feldnamen stabil.

Python hat keinen eingebauten JSON-Formatter. python/cpython#77691 hat 2018 einen angefragt und wurde geschlossen, weil die Doku schon zeigt, wie man einen schreibt. Der 15-zeilige `Formatter` oben ist dieses Rezept.

```bash
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

Ausgabe

```text
2026-11-10 08:34:04,955 INFO order paid
```

## Was LogBrew aufzeichnet

Jedes Log behält Level, Logger-Name und jedes primitive Feld aus `extra`, plus die Felder in `metadata={}` auf dem Handler. Die API filtert nach Feldpfad, Typ und Operator. Text nimmt `eq`, `ne`, `contains` und `starts_with`, Zahlen nehmen `gt`, `gte`, `lt` und `lte`.

LogBrew indiziert kein Feld, dessen Name `password`, `secret`, `token`, `cookie` oder `authorization` enthält, also trifft ein Filter es nie. Halte Secrets trotzdem aus Log-Zeilen heraus.

Ein Log, das innerhalb eines Trace gesendet wird, trägt die Trace- und Span-IDs, also führt ein Filter nach `order_id` zu der Anfrage, die die Zeile erzeugt hat.

## Häufige Fragen

### Was ist strukturiertes Logging?

Logging, bei dem jeder Wert ein benanntes Feld mit Typ ist, als JSON oder als Felder auf einem Log-Record, statt Wörter in einem Satz. Ein Suchtool filtert dann nach dem Feld, nicht nach Teilstring.

### Welche Log-Level gibt es, in welcher Reihenfolge?

DEBUG, INFO, WARNING, ERROR, CRITICAL, mit den Zahlen 10 bis 50 in Python. Ein Logger auf INFO lässt INFO und höher durch und verwirft DEBUG.

### Soll ich in JSON oder in Klartext loggen?

JSON, wenn ein Programm die Zeilen liest, etwa ein Collector, eine Datei oder ein Suchtool. Klartext, wenn nur ein Mensch das Terminal liest. Behalte in beiden dieselben Felder.

### Wie füge ich einer Python-Log-Zeile Felder hinzu?

Übergib `extra={"order_id": "ord_42"}` an den Log-Aufruf. Der Standard-Formatter ignoriert die Extra-Felder. Ein JSON-Formatter oder der LogBrew-Handler behält sie.

### Kann ich Logs in LogBrew nach Feld durchsuchen?

Ja. `GET /api/logs` nimmt bis zu 4 `attribute_filters` an, jeder mit Feldpfad, Typ und einem Operator wie `eq` oder `gte`.

## Filtere deine Logs nach jedem Feld

LogBrew behält jedes Feld, das du loggst, und findet die Zeile über das Feld wieder, neben dem Trace und dem Issue, zu dem sie gehört. Starte mit dem großzügigen kostenlosen Plan, ohne Kreditkarte.

[Kostenlos starten](https://logbrew.co/de)

## Quellen

- [Python docs, Logging Cookbook](https://docs.python.org/3/howto/logging-cookbook.html)
- [Python docs, LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
- [python/cpython#77691](https://github.com/python/cpython/issues/77691)

## Weitere Anleitungen für Agenten und Python

- [MCP-Server in Claude Code und Codex hinzufügen](https://logbrew.co/de/guides/mcp-server-claude-code-codex)
- [AGENTS.md in Claude Code und Codex nutzen](https://logbrew.co/de/guides/agents-md)
- [Claude-Fehler 529 overloaded und 429 Rate Limit beheben](https://logbrew.co/de/guides/claude-overloaded-rate-limit-error)
- [Python-Logging einrichten mit den richtigen Log-Leveln und Exception-Erfassung](https://logbrew.co/de/guides/python-logging)
- [Fehler beheben in Claude Code, Codex, JavaScript und Python](https://logbrew.co/de/guides)
