| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687 |
- import logging
- import threading
- import time
- from textual.app import App
- from textual.widgets import RichLog
- class PipelineLogger:
- """
- Manages centralized logging for both a standard system log file
- and a Textual RichLog UI widget. Automatically handles thread-safety
- and task-specific context tracking.
- """
- def __init__(
- self,
- app: App,
- widget_id: str = "#live_log",
- log_file: str = "alnn_rewrite.log",
- source: str = "SYSTEM",
- file_logger: logging.Logger | None = None,
- ):
- self.app = app
- self.widget_id = widget_id
- self.source = source
- # Share the underlying file logger if spawning a sub-logger
- if file_logger is None:
- logging.basicConfig(
- filename=log_file,
- level=logging.INFO,
- format="%(asctime)s - %(levelname)s - %(message)s",
- )
- self.file_logger = logging.getLogger("CNNHarness")
- else:
- self.file_logger = file_logger
- def get_task_logger(self, task_name: str) -> "PipelineLogger":
- """Spawns a new instance of this logger hardcoded to a specific task's source name."""
- return PipelineLogger(
- app=self.app,
- widget_id=self.widget_id,
- source=task_name,
- file_logger=self.file_logger,
- )
- def write(self, message: str, error: bool = False) -> None:
- """Core write method that handles both file and UI logging intelligently."""
- # 1. System file logging
- log_msg = f"[{self.source}] {message}"
- if error:
- self.file_logger.error(log_msg)
- else:
- self.file_logger.info(log_msg)
- # 2. UI logging (Conditional Thread-Safe Execution)
- def _do_write():
- try:
- timestamp = time.strftime("%H:%M:%S")
- color = (
- "red"
- if error
- else ("magenta" if self.source == "SYSTEM" else "cyan")
- )
- prefix = f"[dim]{timestamp}[/dim] [[bold {color}]{self.source}[/bold {color}]]"
- log_widget = self.app.query_one(self.widget_id, RichLog)
- log_widget.write(f"{prefix} {message}")
- except Exception:
- pass
- # Textual throws an error if call_from_thread is used on the main thread.
- # This safely executes _do_write() based on where it's being called from.
- if threading.current_thread() is threading.main_thread():
- _do_write()
- else:
- self.app.call_from_thread(_do_write)
- def info(self, message: str) -> None:
- """Helper for standard informational logging."""
- self.write(message, error=False)
- def error(self, message: str) -> None:
- """Helper for error logging."""
- self.write(message, error=True)
|