ui_logger.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import logging
  2. import threading
  3. import time
  4. from textual.app import App
  5. from textual.widgets import RichLog
  6. class PipelineLogger:
  7. """
  8. Manages centralized logging for both a standard system log file
  9. and a Textual RichLog UI widget. Automatically handles thread-safety
  10. and task-specific context tracking.
  11. """
  12. def __init__(
  13. self,
  14. app: App,
  15. widget_id: str = "#live_log",
  16. log_file: str = "alnn_rewrite.log",
  17. source: str = "SYSTEM",
  18. file_logger: logging.Logger | None = None,
  19. ):
  20. self.app = app
  21. self.widget_id = widget_id
  22. self.source = source
  23. # Share the underlying file logger if spawning a sub-logger
  24. if file_logger is None:
  25. logging.basicConfig(
  26. filename=log_file,
  27. level=logging.INFO,
  28. format="%(asctime)s - %(levelname)s - %(message)s",
  29. )
  30. self.file_logger = logging.getLogger("CNNHarness")
  31. else:
  32. self.file_logger = file_logger
  33. def get_task_logger(self, task_name: str) -> "PipelineLogger":
  34. """Spawns a new instance of this logger hardcoded to a specific task's source name."""
  35. return PipelineLogger(
  36. app=self.app,
  37. widget_id=self.widget_id,
  38. source=task_name,
  39. file_logger=self.file_logger,
  40. )
  41. def write(self, message: str, error: bool = False) -> None:
  42. """Core write method that handles both file and UI logging intelligently."""
  43. # 1. System file logging
  44. log_msg = f"[{self.source}] {message}"
  45. if error:
  46. self.file_logger.error(log_msg)
  47. else:
  48. self.file_logger.info(log_msg)
  49. # 2. UI logging (Conditional Thread-Safe Execution)
  50. def _do_write():
  51. try:
  52. timestamp = time.strftime("%H:%M:%S")
  53. color = (
  54. "red"
  55. if error
  56. else ("magenta" if self.source == "SYSTEM" else "cyan")
  57. )
  58. prefix = f"[dim]{timestamp}[/dim] [[bold {color}]{self.source}[/bold {color}]]"
  59. log_widget = self.app.query_one(self.widget_id, RichLog)
  60. log_widget.write(f"{prefix} {message}")
  61. except Exception:
  62. pass
  63. # Textual throws an error if call_from_thread is used on the main thread.
  64. # This safely executes _do_write() based on where it's being called from.
  65. if threading.current_thread() is threading.main_thread():
  66. _do_write()
  67. else:
  68. self.app.call_from_thread(_do_write)
  69. def info(self, message: str) -> None:
  70. """Helper for standard informational logging."""
  71. self.write(message, error=False)
  72. def error(self, message: str) -> None:
  73. """Helper for error logging."""
  74. self.write(message, error=True)