Initial loggy implementation
This commit is contained in:
parent
ac4a2cef27
commit
50c2a2d33d
6
loggy
Executable file
6
loggy
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
BASEPATH=$(dirname $0)
|
||||||
|
|
||||||
|
$BASEPATH/venv/bin/python $BASEPATH/loggy.py
|
||||||
|
|
135
loggy.py
Normal file
135
loggy.py
Normal 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
4
reposinit
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
echo "* Creating virtual env"
|
||||||
|
mkvenv
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
textual
|
||||||
|
|
21
style.tcss
Normal file
21
style.tcss
Normal 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
34
testlogs.py
Normal 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.")
|
Loading…
x
Reference in New Issue
Block a user