| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- # -*- coding: utf-8 -*-
- """
- Log-Formatierer für verschiedene Output-Formate.
- Bietet JSON, Text und Compact Formatierung für strukturierte Logs.
- """
- from __future__ import annotations
- import json
- import logging
- import traceback
- from abc import ABC, abstractmethod
- from datetime import datetime
- from typing import Any
- from trixy_core.utils.logging.context import get_context, get_correlation_id
- class BaseFormatter(logging.Formatter, ABC):
- """
- Abstrakte Basisklasse für Log-Formatierer.
- Definiert die gemeinsame Schnittstelle für alle Formatierer
- und bietet Hilfsmethoden für die Verarbeitung von Log-Records.
- """
- # Standard-Zeitformat
- DEFAULT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
- DEFAULT_MSEC_FORMAT = "%s.%03d"
- def __init__(
- self,
- time_format: str | None = None,
- include_context: bool = True,
- include_correlation_id: bool = True,
- ) -> None:
- """
- Initialisiert den Formatierer.
- Args:
- time_format: Optionales Zeitformat (strftime-kompatibel).
- include_context: Ob der Log-Kontext eingefügt werden soll.
- include_correlation_id: Ob die Korrelations-ID eingefügt werden soll.
- """
- super().__init__()
- self.time_format = time_format or self.DEFAULT_TIME_FORMAT
- self.include_context = include_context
- self.include_correlation_id = include_correlation_id
- def format_timestamp(self, record: logging.LogRecord) -> str:
- """
- Formatiert den Zeitstempel eines Log-Records.
- Args:
- record: Der Log-Record.
- Returns:
- Formatierter Zeitstempel als ISO-8601-String.
- """
- ct = datetime.fromtimestamp(record.created)
- return ct.strftime(self.time_format) + f".{int(record.msecs):03d}"
- def format_exception(self, record: logging.LogRecord) -> str | None:
- """
- Formatiert Exception-Informationen.
- Args:
- record: Der Log-Record.
- Returns:
- Formatierter Exception-Traceback oder None.
- """
- if record.exc_info:
- return "".join(traceback.format_exception(*record.exc_info))
- return None
- def get_extra_fields(self, record: logging.LogRecord) -> dict[str, Any]:
- """
- Extrahiert zusätzliche Felder aus dem Log-Record.
- Args:
- record: Der Log-Record.
- Returns:
- Dictionary mit Extra-Feldern.
- """
- # Standard-Attribute, die nicht als Extra gelten
- standard_attrs = {
- "name", "msg", "args", "created", "filename", "funcName",
- "levelname", "levelno", "lineno", "module", "msecs",
- "pathname", "process", "processName", "relativeCreated",
- "stack_info", "exc_info", "exc_text", "thread", "threadName",
- "message", "asctime",
- }
- extra = {}
- for key, value in record.__dict__.items():
- if key not in standard_attrs and not key.startswith("_"):
- extra[key] = value
- return extra
- @abstractmethod
- def format(self, record: logging.LogRecord) -> str:
- """
- Formatiert einen Log-Record.
- Args:
- record: Der zu formatierende Log-Record.
- Returns:
- Formatierter Log-String.
- """
- pass
- class JSONFormatter(BaseFormatter):
- """
- JSON-Formatierer für maschinenlesbare Log-Ausgabe.
- Ideal für Log-Aggregation in Systemen wie ELK, Splunk oder CloudWatch.
- """
- def __init__(
- self,
- time_format: str | None = None,
- include_context: bool = True,
- include_correlation_id: bool = True,
- indent: int | None = None,
- ensure_ascii: bool = False,
- include_stacktrace: bool = True,
- ) -> None:
- """
- Initialisiert den JSON-Formatierer.
- Args:
- time_format: Optionales Zeitformat.
- include_context: Ob der Log-Kontext eingefügt werden soll.
- include_correlation_id: Ob die Korrelations-ID eingefügt werden soll.
- indent: Einrückung für Pretty-Print (None für kompakt).
- ensure_ascii: Ob Nicht-ASCII-Zeichen escaped werden sollen.
- include_stacktrace: Ob Stacktraces inkludiert werden sollen.
- """
- super().__init__(time_format, include_context, include_correlation_id)
- self.indent = indent
- self.ensure_ascii = ensure_ascii
- self.include_stacktrace = include_stacktrace
- def format(self, record: logging.LogRecord) -> str:
- """
- Formatiert einen Log-Record als JSON.
- Args:
- record: Der zu formatierende Log-Record.
- Returns:
- JSON-String mit Log-Daten.
- """
- log_data: dict[str, Any] = {
- "timestamp": self.format_timestamp(record),
- "level": record.levelname,
- "logger": record.name,
- "message": record.getMessage(),
- }
- # Korrelations-ID hinzufügen
- if self.include_correlation_id:
- log_data["correlation_id"] = get_correlation_id()
- # Quell-Informationen
- log_data["source"] = {
- "file": record.filename,
- "line": record.lineno,
- "function": record.funcName,
- "module": record.module,
- }
- # Process/Thread-Informationen
- log_data["process"] = {
- "id": record.process,
- "name": record.processName,
- }
- log_data["thread"] = {
- "id": record.thread,
- "name": record.threadName,
- }
- # Kontext hinzufügen
- if self.include_context:
- context = get_context()
- if context:
- log_data["context"] = context
- # Extra-Felder hinzufügen
- extra = self.get_extra_fields(record)
- if extra:
- log_data["extra"] = extra
- # Exception-Informationen
- if self.include_stacktrace:
- exception = self.format_exception(record)
- if exception:
- log_data["exception"] = exception
- return json.dumps(
- log_data,
- indent=self.indent,
- ensure_ascii=self.ensure_ascii,
- default=str,
- )
- class TextFormatter(BaseFormatter):
- """
- Klassischer Text-Formatierer für menschenlesbare Log-Ausgabe.
- Enthält optional farbige Ausgabe für Terminal-Nutzung.
- """
- # ANSI-Farbcodes
- COLORS = {
- "DEBUG": "\033[36m", # Cyan
- "INFO": "\033[32m", # Grün
- "WARNING": "\033[33m", # Gelb
- "ERROR": "\033[31m", # Rot
- "CRITICAL": "\033[35m", # Magenta
- }
- RESET = "\033[0m"
- BOLD = "\033[1m"
- DIM = "\033[2m"
- def __init__(
- self,
- time_format: str | None = None,
- include_context: bool = True,
- include_correlation_id: bool = True,
- use_colors: bool = True,
- show_source: bool = True,
- format_string: str | None = None,
- ) -> None:
- """
- Initialisiert den Text-Formatierer.
- Args:
- time_format: Optionales Zeitformat.
- include_context: Ob der Log-Kontext eingefügt werden soll.
- include_correlation_id: Ob die Korrelations-ID eingefügt werden soll.
- use_colors: Ob ANSI-Farben verwendet werden sollen.
- show_source: Ob Datei/Zeile angezeigt werden soll.
- format_string: Optionales eigenes Format-Template.
- """
- super().__init__(time_format, include_context, include_correlation_id)
- self.use_colors = use_colors
- self.show_source = show_source
- self.format_string = format_string
- def _colorize(self, text: str, level: str) -> str:
- """Fügt Farbcodes basierend auf dem Log-Level hinzu."""
- if not self.use_colors:
- return text
- color = self.COLORS.get(level, "")
- return f"{color}{text}{self.RESET}"
- def format(self, record: logging.LogRecord) -> str:
- """
- Formatiert einen Log-Record als lesbaren Text.
- Args:
- record: Der zu formatierende Log-Record.
- Returns:
- Formatierter Text-String.
- """
- timestamp = self.format_timestamp(record)
- level = record.levelname.ljust(8)
- message = record.getMessage()
- # Basis-Format
- parts = [
- f"{self.DIM if self.use_colors else ''}{timestamp}{self.RESET if self.use_colors else ''}",
- self._colorize(level, record.levelname),
- ]
- # Korrelations-ID
- if self.include_correlation_id:
- cid = get_correlation_id()[:8] # Verkürzt für Lesbarkeit
- if self.use_colors:
- parts.append(f"{self.DIM}[{cid}]{self.RESET}")
- else:
- parts.append(f"[{cid}]")
- # Logger-Name
- parts.append(f"[{record.name}]")
- # Nachricht
- parts.append(message)
- # Quell-Information
- if self.show_source:
- source = f"({record.filename}:{record.lineno})"
- if self.use_colors:
- parts.append(f"{self.DIM}{source}{self.RESET}")
- else:
- parts.append(source)
- output = " ".join(parts)
- # Kontext hinzufügen
- if self.include_context:
- context = get_context()
- if context:
- ctx_str = " ".join(f"{k}={v}" for k, v in context.items())
- if self.use_colors:
- output += f"\n {self.DIM}Context: {ctx_str}{self.RESET}"
- else:
- output += f"\n Context: {ctx_str}"
- # Extra-Felder
- extra = self.get_extra_fields(record)
- if extra:
- extra_str = " ".join(f"{k}={v}" for k, v in extra.items())
- if self.use_colors:
- output += f"\n {self.DIM}Extra: {extra_str}{self.RESET}"
- else:
- output += f"\n Extra: {extra_str}"
- # Exception
- exception = self.format_exception(record)
- if exception:
- if self.use_colors:
- output += f"\n{self.COLORS['ERROR']}{exception}{self.RESET}"
- else:
- output += f"\n{exception}"
- return output
- class CompactFormatter(BaseFormatter):
- """
- Kompakter Formatierer für minimale Log-Ausgabe.
- Ideal für High-Volume-Logging wo Speicherplatz kritisch ist.
- """
- # Einbuchstabige Level-Codes
- LEVEL_CODES = {
- "DEBUG": "D",
- "INFO": "I",
- "WARNING": "W",
- "ERROR": "E",
- "CRITICAL": "C",
- }
- def __init__(
- self,
- time_format: str | None = None,
- include_context: bool = False,
- include_correlation_id: bool = True,
- separator: str = "|",
- ) -> None:
- """
- Initialisiert den Compact-Formatierer.
- Args:
- time_format: Optionales Zeitformat.
- include_context: Ob der Log-Kontext eingefügt werden soll.
- include_correlation_id: Ob die Korrelations-ID eingefügt werden soll.
- separator: Trennzeichen zwischen Feldern.
- """
- super().__init__(time_format, include_context, include_correlation_id)
- self.separator = separator
- def format(self, record: logging.LogRecord) -> str:
- """
- Formatiert einen Log-Record im Kompakt-Format.
- Args:
- record: Der zu formatierende Log-Record.
- Returns:
- Kompakter Log-String.
- """
- parts = [
- self.format_timestamp(record),
- self.LEVEL_CODES.get(record.levelname, "?"),
- ]
- if self.include_correlation_id:
- parts.append(get_correlation_id()[:8])
- parts.append(record.name)
- parts.append(record.getMessage())
- output = self.separator.join(parts)
- # Exception als zusätzliche Zeile
- exception = self.format_exception(record)
- if exception:
- # Nur erste Zeile des Tracebacks
- first_line = exception.strip().split("\n")[-1]
- output += f"{self.separator}EXC:{first_line}"
- return output
- class KeyValueFormatter(BaseFormatter):
- """
- Key-Value-Formatierer für einfache Parsing-Kompatibilität.
- Ausgabe im Format: key1=value1 key2=value2
- """
- def __init__(
- self,
- time_format: str | None = None,
- include_context: bool = True,
- include_correlation_id: bool = True,
- quote_strings: bool = True,
- ) -> None:
- """
- Initialisiert den Key-Value-Formatierer.
- Args:
- time_format: Optionales Zeitformat.
- include_context: Ob der Log-Kontext eingefügt werden soll.
- include_correlation_id: Ob die Korrelations-ID eingefügt werden soll.
- quote_strings: Ob String-Werte in Anführungszeichen gesetzt werden.
- """
- super().__init__(time_format, include_context, include_correlation_id)
- self.quote_strings = quote_strings
- def _format_value(self, value: Any) -> str:
- """Formatiert einen Wert für die Ausgabe."""
- if isinstance(value, str):
- # Escape Anführungszeichen und füge Quotes hinzu
- escaped = value.replace('"', '\\"')
- return f'"{escaped}"' if self.quote_strings else escaped
- return str(value)
- def format(self, record: logging.LogRecord) -> str:
- """
- Formatiert einen Log-Record als Key-Value-Paare.
- Args:
- record: Der zu formatierende Log-Record.
- Returns:
- Key-Value-formatierter String.
- """
- pairs = [
- f"timestamp={self._format_value(self.format_timestamp(record))}",
- f"level={record.levelname}",
- f"logger={record.name}",
- f"message={self._format_value(record.getMessage())}",
- ]
- if self.include_correlation_id:
- pairs.append(f"correlation_id={get_correlation_id()}")
- pairs.extend([
- f"file={record.filename}",
- f"line={record.lineno}",
- f"function={record.funcName}",
- ])
- # Kontext hinzufügen
- if self.include_context:
- context = get_context()
- for key, value in context.items():
- pairs.append(f"ctx_{key}={self._format_value(value)}")
- # Extra-Felder
- extra = self.get_extra_fields(record)
- for key, value in extra.items():
- pairs.append(f"{key}={self._format_value(value)}")
- # Exception
- exception = self.format_exception(record)
- if exception:
- # Nur letzte Zeile (die Exception selbst)
- last_line = exception.strip().split("\n")[-1]
- pairs.append(f"exception={self._format_value(last_line)}")
- return " ".join(pairs)
|