159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
import logging
|
|
import pickle
|
|
import socket
|
|
import struct
|
|
import threading
|
|
from datetime import datetime
|
|
|
|
from textual.app import App, ComposeResult
|
|
from textual.containers import Vertical
|
|
from textual.widgets import Footer, Header, Input, RichLog, Checkbox
|
|
|
|
# Mapping von Log-Level-Namen zu Farben für die Anzeige
|
|
LEVEL_STYLES = {
|
|
"CRITICAL": "bold white on red",
|
|
"ERROR": "bold red",
|
|
"WARNING": "bold yellow",
|
|
"INFO": "bold green",
|
|
"DEBUG": "bold blue",
|
|
}
|
|
|
|
|
|
class LogReceiver(threading.Thread):
|
|
"""Ein Thread, der auf eingehende Log-Nachrichten lauscht."""
|
|
|
|
def __init__(self, app, host="", port=19996):
|
|
super().__init__()
|
|
self.app = app
|
|
self.host = host
|
|
self.port = port
|
|
self.daemon = True
|
|
|
|
def run(self):
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind((self.host, self.port))
|
|
s.listen()
|
|
while True:
|
|
conn, _ = s.accept()
|
|
with conn:
|
|
while True:
|
|
chunk = conn.recv(4)
|
|
if len(chunk) < 4:
|
|
break
|
|
slen = struct.unpack(">L", chunk)[0]
|
|
chunk = conn.recv(slen)
|
|
while len(chunk) < slen:
|
|
chunk = chunk + conn.recv(slen - len(chunk))
|
|
|
|
obj = pickle.loads(chunk)
|
|
log_record = logging.makeLogRecord(obj)
|
|
|
|
self.app.call_from_thread(self.app.add_log, log_record)
|
|
except Exception as e:
|
|
logging.basicConfig(filename="server_error.log", level=logging.DEBUG)
|
|
logging.error(f"LogReceiver-Fehler: {e}", exc_info=True)
|
|
|
|
|
|
class LogViewerApp(App):
|
|
"""Eine Textual-App zum Anzeigen und Filtern von Python-Logs."""
|
|
|
|
CSS_PATH = "style.tcss"
|
|
BINDINGS = [("q", "quit", "Quit")]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
#
|
|
self.all_logs = []
|
|
self.module_filter = ""
|
|
self.level_filter = ""
|
|
self.force_critical = True
|
|
self.force_error = True
|
|
self.force_warning = False
|
|
self.log_display = RichLog(highlight=True, markup=True)
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Erstellt die Widgets für die App."""
|
|
yield Header(name="Python Log Viewer")
|
|
with Vertical(id="app-grid"):
|
|
yield self.log_display
|
|
with Vertical(id="bottom-bar"):
|
|
yield Input(placeholder="module filter...", id="module_filter")
|
|
yield Input(placeholder="level filter...", id="level_filter")
|
|
yield Checkbox("CRITICAL", self.force_critical, id="force_critical")
|
|
yield Checkbox("ERROR", self.force_error, id="force_error")
|
|
yield Checkbox("WARNING", self.force_warning, id="force_warning")
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
"""Startet den Log-Empfänger-Thread."""
|
|
log_receiver = LogReceiver(self)
|
|
log_receiver.start()
|
|
|
|
def add_log(self, record: logging.LogRecord) -> None:
|
|
"""Fügt einen neuen Log-Eintrag hinzu und aktualisiert die Anzeige."""
|
|
self.all_logs.append(record)
|
|
self._apply_filters_to_log(record)
|
|
|
|
def _force(self, lvl: str) -> bool:
|
|
rv = False
|
|
rv |= lvl == 'critical' and self.force_critical
|
|
rv |= lvl == 'error' and self.force_error
|
|
rv |= lvl == 'warning' and self.force_warning
|
|
return rv
|
|
|
|
def _apply_filters_to_log(self, record: logging.LogRecord):
|
|
"""Prüft einen einzelnen Log-Eintrag gegen die Filter und zeigt ihn ggf. an."""
|
|
module_match = False
|
|
for module_filter in self.module_filter.split(","):
|
|
module_match |= module_filter.lower() in record.name.lower()
|
|
|
|
level_match = False
|
|
for level_filter in self.level_filter.split(","):
|
|
level_match |= level_filter.lower() in record.levelname.lower()
|
|
|
|
if self._force(record.levelname.lower()) or (module_match and level_match):
|
|
level_style = LEVEL_STYLES.get(record.levelname, "white")
|
|
|
|
timestamp_str = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
|
|
asctime = f"{timestamp_str},{int(record.msecs):03d}"
|
|
|
|
message = (
|
|
f"[[dim]{asctime}[/dim]] "
|
|
f"[{level_style}]{record.levelname:<8}[/{level_style}] "
|
|
f"[bold]{record.name}[/bold] - "
|
|
f"{record.getMessage()}"
|
|
)
|
|
|
|
self.log_display.write(message)
|
|
|
|
def _update_display(self):
|
|
"""Löscht die Anzeige und rendert alle Logs basierend auf den aktuellen Filtern neu."""
|
|
self.log_display.clear()
|
|
for record in self.all_logs:
|
|
self._apply_filters_to_log(record)
|
|
|
|
def on_input_changed(self, message: Input.Changed) -> None:
|
|
"""Aktualisiert die Filter und die Anzeige, wenn der Benutzer tippt."""
|
|
if message.input.id == "module_filter":
|
|
self.module_filter = message.value
|
|
elif message.input.id == "level_filter":
|
|
self.level_filter = message.value
|
|
|
|
self._update_display()
|
|
|
|
def on_checkbox_changed(self, message: Checkbox.Changed) -> None:
|
|
if message.checkbox.id == "force_critical":
|
|
self.force_critical = message.value
|
|
elif message.checkbox.id == "force_error":
|
|
self.force_error = message.value
|
|
elif message.checkbox.id == "force_warning":
|
|
self.force_warning = message.value
|
|
|
|
self._update_display()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = LogViewerApp()
|
|
app.run()
|