Initial loggy implementation

This commit is contained in:
Dirk Alders 2025-07-18 21:29:18 +02:00
parent ac4a2cef27
commit 50c2a2d33d
6 changed files with 202 additions and 0 deletions

6
loggy Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
#
BASEPATH=$(dirname $0)
$BASEPATH/venv/bin/python $BASEPATH/loggy.py

135
loggy.py Normal file
View File

@ -0,0 +1,135 @@
import logging
import pickle
import socket
import struct
import threading
from datetime import datetime # <--- HINZUGEFÜGT
from logging.handlers import SocketHandler
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import Footer, Header, Input, RichLog
# 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", "Beenden")]
def __init__(self):
super().__init__()
self.all_logs = []
self.module_filter = ""
self.level_filter = ""
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="Modul filtern...", id="module_filter")
yield Input(placeholder="Level filtern...", id="level_filter")
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 _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 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()
if __name__ == "__main__":
app = LogViewerApp()
app.run()

4
reposinit Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
#
echo "* Creating virtual env"
mkvenv

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
textual

21
style.tcss Normal file
View File

@ -0,0 +1,21 @@
#app-grid {
layout: vertical;
overflow-y: scroll;
scrollbar-gutter: stable;
height: 90%;
}
#bottom-bar {
layout: grid;
grid-size: 2;
height: 10%;
border-top: solid $primary;
}
#module_filter {
width: 100%;
}
#level_filter {
width: 100%;
}

34
testlogs.py Normal file
View File

@ -0,0 +1,34 @@
import logging
import logging.handlers
import time
import random
# Konfiguriere den SocketHandler, um Logs an den Server zu senden
socket_handler = logging.handlers.SocketHandler("localhost", 19996)
# Definiere verschiedene Logger für unterschiedliche Module
logger_main = logging.getLogger("main_app")
logger_main.setLevel(logging.DEBUG)
logger_main.addHandler(socket_handler)
logger_db = logging.getLogger("database.connector")
logger_db.setLevel(logging.DEBUG)
logger_db.addHandler(socket_handler)
logger_api = logging.getLogger("api.v1")
logger_api.setLevel(logging.DEBUG)
logger_api.addHandler(socket_handler)
log_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
loggers = [logger_main, logger_db, logger_api]
if __name__ == "__main__":
print("Sende Logs an den Server. Drücken Sie STRG+C zum Beenden.")
try:
while True:
level = random.choice(log_levels)
logger = random.choice(loggers)
logger.log(level, f"Dies ist eine Testnachricht.")
time.sleep(random.uniform(0.5, 2.0))
except KeyboardInterrupt:
print("Senden der Logs beendet.")