first commit with logger
This commit is contained in:
parent
f0b0033f48
commit
0e2a56f4e1
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Python Debugger: Module",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "radian"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,17 +1,13 @@
|
||||
# radian/__main__.py
|
||||
|
||||
# Example import assuming your main logic is in a 'main' function
|
||||
# within a 'app' module in your 'radian.core' package.
|
||||
# from radian.core.app import main as start_application
|
||||
#
|
||||
# Or, if you have a function in radian.core.core:
|
||||
# from radian.core.core import main_function
|
||||
from radian.core.main_controller import MainController
|
||||
|
||||
def main():
|
||||
print(f"Running RADIAN...")
|
||||
# Placeholder: Replace with your application's entry point
|
||||
# Example: start_application()
|
||||
print("To customize, edit 'radian/__main__.py' and your core modules.")
|
||||
"""
|
||||
Main entry point for the RADIAN application.
|
||||
"""
|
||||
controller = MainController()
|
||||
controller.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
0
radian/components/base_component.py
Normal file
0
radian/components/base_component.py
Normal file
42
radian/config/logging_config.py
Normal file
42
radian/config/logging_config.py
Normal file
@ -0,0 +1,42 @@
|
||||
# radian/config/logging_config.py
|
||||
|
||||
import logging
|
||||
|
||||
# Configuration dictionary for the RADIAN logging system.
|
||||
LOGGING_CONFIG = {
|
||||
# The default level for the root logger. All messages at this level or higher
|
||||
# from any module will be processed.
|
||||
"default_root_level": logging.DEBUG,
|
||||
|
||||
# Set specific logging levels for noisy modules to avoid clutter.
|
||||
# For example, to silence a verbose library:
|
||||
# "specific_levels": {
|
||||
# "noisy_library_name": logging.WARNING,
|
||||
# },
|
||||
"specific_levels": {},
|
||||
|
||||
# The format for all log messages.
|
||||
"format": "%(asctime)s [%(levelname)-8s] %(name)-30s : %(message)s",
|
||||
"date_format": "%Y-%m-%d %H:%M:%S",
|
||||
|
||||
# --- Console Handler Configuration ---
|
||||
"enable_console": True,
|
||||
|
||||
# --- File Handler Configuration ---
|
||||
"enable_file": True,
|
||||
"file_path": "radian_app.log", # Log file name
|
||||
"file_max_bytes": 5 * 1024 * 1024, # 5 MB
|
||||
"file_backup_count": 3,
|
||||
|
||||
# --- Tkinter Handler Configuration ---
|
||||
# Colors for different log levels in the GUI log widget.
|
||||
"colors": {
|
||||
logging.DEBUG: "gray",
|
||||
logging.INFO: "black",
|
||||
logging.WARNING: "orange",
|
||||
logging.ERROR: "red",
|
||||
logging.CRITICAL: "purple",
|
||||
},
|
||||
# How often the Tkinter handler's internal queue is checked.
|
||||
"queue_poll_interval_ms": 100,
|
||||
}
|
||||
66
radian/core/main_controller.py
Normal file
66
radian/core/main_controller.py
Normal file
@ -0,0 +1,66 @@
|
||||
# radian/core/main_controller.py
|
||||
|
||||
from radian.gui.main_window import MainWindow
|
||||
from radian.utils import logger
|
||||
from radian.config.logging_config import LOGGING_CONFIG
|
||||
|
||||
# Get a logger for this specific module
|
||||
log = logger.get_logger(__name__)
|
||||
|
||||
class MainController:
|
||||
"""
|
||||
The main controller of the RADIAN application.
|
||||
|
||||
It orchestrates the application's lifecycle, including initializing the GUI,
|
||||
setting up the logging system, loading components, and handling shutdown.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.app: MainWindow | None = None
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Initializes and starts the RADIAN application.
|
||||
"""
|
||||
# 1. Initialize the main window (the GUI)
|
||||
self.app = MainWindow()
|
||||
|
||||
# 2. Set up the centralized logging system
|
||||
self._setup_logging()
|
||||
|
||||
# 3. Set up a graceful shutdown procedure
|
||||
self.app.protocol("WM_DELETE_WINDOW", self._on_shutdown)
|
||||
|
||||
log.info("RADIAN application started.")
|
||||
log.info("Welcome to the Radar Data Integration & Analysis Network.")
|
||||
|
||||
# 4. Start the Tkinter main event loop
|
||||
self.app.mainloop()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Initializes the basic logger and adds the Tkinter handler."""
|
||||
# Set up the queue, processor, and file/console handlers
|
||||
logger.setup_basic_logging(
|
||||
root_tk_instance_for_processor=self.app,
|
||||
logging_config_dict=LOGGING_CONFIG
|
||||
)
|
||||
|
||||
# Add the handler that writes to our GUI log widget
|
||||
logger.add_tkinter_handler(
|
||||
gui_log_widget=self.app.log_text,
|
||||
root_tk_instance_for_gui_handler=self.app,
|
||||
logging_config_dict=LOGGING_CONFIG
|
||||
)
|
||||
log.info("Logging system configured and GUI handler attached.")
|
||||
|
||||
def _on_shutdown(self):
|
||||
"""Handles the application shutdown sequence."""
|
||||
log.info("Shutdown sequence initiated...")
|
||||
|
||||
# --- Future shutdown logic for components would go here ---
|
||||
|
||||
# Properly shut down the logging system
|
||||
logger.shutdown_logging_system()
|
||||
|
||||
# Destroy the main window
|
||||
if self.app:
|
||||
self.app.destroy()
|
||||
72
radian/gui/main_window.py
Normal file
72
radian/gui/main_window.py
Normal file
@ -0,0 +1,72 @@
|
||||
# radian/gui/main_window.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
class MainWindow(tk.Tk):
|
||||
"""
|
||||
The main window of the RADIAN application.
|
||||
|
||||
It sets up the main application frame, including the layout for
|
||||
the components' UI and the logging area.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# --- Basic Window Configuration ---
|
||||
self.title("RADIAN - Radar Data Integration & Analysis Network")
|
||||
self._set_fullscreen()
|
||||
|
||||
# --- Main Layout Structure ---
|
||||
# The main container frame
|
||||
main_frame = ttk.Frame(self)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Configure the grid layout (2 rows, 1 column)
|
||||
main_frame.rowconfigure(0, weight=3) # Content area takes 3/4 of the space
|
||||
main_frame.rowconfigure(1, weight=1) # Log area takes 1/4 of the space
|
||||
main_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# --- Create Widgets ---
|
||||
self._create_content_area(main_frame)
|
||||
self._create_log_area(main_frame)
|
||||
|
||||
def _set_fullscreen(self):
|
||||
"""Sets the window to a maximized state."""
|
||||
# 'zoomed' is the standard state for a maximized window in Tkinter.
|
||||
# This is generally more reliable across different systems than using attributes.
|
||||
self.state('zoomed')
|
||||
|
||||
def _create_content_area(self, parent: ttk.Frame):
|
||||
"""Creates the frame that will host the components' UI."""
|
||||
content_frame = ttk.LabelFrame(parent, text="Component View", padding="10")
|
||||
content_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# This is a placeholder. In the future, this frame will be populated
|
||||
# by the ComponentManager with the UI of the selected component.
|
||||
placeholder_label = ttk.Label(content_frame, text="Component UI will be displayed here.")
|
||||
placeholder_label.pack(expand=True)
|
||||
|
||||
self.content_frame = content_frame
|
||||
|
||||
def _create_log_area(self, parent: ttk.Frame):
|
||||
"""Creates the scrollable text widget for logging."""
|
||||
log_frame = ttk.LabelFrame(parent, text="System Log", padding="10")
|
||||
log_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# Configure grid to make the text widget fill the frame
|
||||
log_frame.rowconfigure(0, weight=1)
|
||||
log_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# The text widget for displaying logs
|
||||
log_text = tk.Text(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10)
|
||||
log_text.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
# The scrollbar for the text widget
|
||||
scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=log_text.yview)
|
||||
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
|
||||
# Link the scrollbar to the text widget
|
||||
log_text.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.log_text = log_text
|
||||
0
radian/utils/__init__.py
Normal file
0
radian/utils/__init__.py
Normal file
628
radian/utils/logger.py
Normal file
628
radian/utils/logger.py
Normal file
@ -0,0 +1,628 @@
|
||||
# FlightMonitor/utils/logger.py
|
||||
import logging
|
||||
import logging.handlers # For RotatingFileHandler
|
||||
import tkinter as tk
|
||||
from tkinter.scrolledtext import ScrolledText
|
||||
from queue import Queue, Empty as QueueEmpty # Renamed for clarity
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
# --- Module-level globals for the new centralized logging queue system ---
|
||||
_global_log_queue: Optional[Queue[logging.LogRecord]] = None
|
||||
_actual_console_handler: Optional[logging.StreamHandler] = None
|
||||
_actual_file_handler: Optional[logging.handlers.RotatingFileHandler] = None
|
||||
_actual_tkinter_handler: Optional["TkinterTextHandler"] = None # Forward declaration
|
||||
|
||||
_log_processor_after_id: Optional[str] = None
|
||||
_logging_system_active: bool = False
|
||||
_tk_root_instance_for_processing: Optional[tk.Tk] = None
|
||||
_base_formatter: Optional[logging.Formatter] = None # Store the main formatter
|
||||
|
||||
# Interval for polling the global log queue by the GUI thread processor
|
||||
GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS = 50
|
||||
|
||||
|
||||
class TkinterTextHandler(logging.Handler):
|
||||
"""
|
||||
A logging handler that directs log messages to a Tkinter Text widget.
|
||||
This handler itself is now called by _process_global_log_queue,
|
||||
which runs in the GUI thread. Its internal queue is for batching updates
|
||||
to the Text widget if needed, or could be simplified later.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text_widget: tk.Text, # ScrolledText is a tk.Text
|
||||
level_colors: Dict[int, str],
|
||||
# root_tk_instance and queue_poll_interval_ms for its own internal queue might be redundant
|
||||
# if _process_global_log_queue handles batching, but kept for now.
|
||||
# For now, this handler will still use its own internal queue and after loop for safety.
|
||||
root_tk_instance_for_widget_update: tk.Tk,
|
||||
internal_poll_interval_ms: int = 100,
|
||||
):
|
||||
super().__init__()
|
||||
self.text_widget = text_widget
|
||||
self.level_colors = level_colors
|
||||
self._is_active = True # Internal active state for this specific handler
|
||||
self._widget_update_queue = Queue()
|
||||
self._root_for_widget_update = root_tk_instance_for_widget_update
|
||||
self._internal_poll_interval_ms = internal_poll_interval_ms
|
||||
self._internal_after_id: Optional[str] = None
|
||||
|
||||
if not (
|
||||
self.text_widget
|
||||
and hasattr(self.text_widget, "winfo_exists")
|
||||
and self.text_widget.winfo_exists()
|
||||
):
|
||||
print(
|
||||
"ERROR: TkinterTextHandler initialized with an invalid or non-existent text_widget.",
|
||||
flush=True,
|
||||
)
|
||||
self._is_active = False
|
||||
return
|
||||
|
||||
if not (
|
||||
self._root_for_widget_update
|
||||
and hasattr(self._root_for_widget_update, "winfo_exists")
|
||||
and self._root_for_widget_update.winfo_exists()
|
||||
):
|
||||
print(
|
||||
"ERROR: TkinterTextHandler initialized with an invalid root for widget update.",
|
||||
flush=True,
|
||||
)
|
||||
self._is_active = False
|
||||
return
|
||||
|
||||
if self._is_active:
|
||||
self._configure_tags()
|
||||
self._process_widget_update_queue() # Start its own update loop
|
||||
|
||||
def _configure_tags(self):
|
||||
if not self._is_active or not self.text_widget.winfo_exists():
|
||||
return
|
||||
for level, color_value in self.level_colors.items():
|
||||
level_name = logging.getLevelName(level)
|
||||
if color_value:
|
||||
try:
|
||||
self.text_widget.tag_config(level_name, foreground=color_value)
|
||||
except tk.TclError:
|
||||
print(
|
||||
f"Warning: Could not configure tag for {level_name} in TkinterTextHandler.",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
# This emit is called by _process_global_log_queue (already in GUI thread)
|
||||
if not self._is_active or not self.text_widget.winfo_exists():
|
||||
# This case should ideally not happen if setup is correct
|
||||
# print(
|
||||
# f"DEBUG: TkinterTextHandler.emit called but inactive or widget gone. Record: {record.getMessage()}",
|
||||
# flush=True,
|
||||
# )
|
||||
return
|
||||
try:
|
||||
msg = self.format(
|
||||
record
|
||||
) # Format the record using the formatter set on this handler
|
||||
level_name = record.levelname
|
||||
# Put in its own queue for batched widget updates
|
||||
self._widget_update_queue.put_nowait((level_name, msg))
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error in TkinterTextHandler.emit (to internal queue): {e}", flush=True
|
||||
)
|
||||
|
||||
def _process_widget_update_queue(self):
|
||||
if (
|
||||
not self._is_active
|
||||
or not self.text_widget.winfo_exists()
|
||||
or not self._root_for_widget_update.winfo_exists()
|
||||
):
|
||||
self._is_active = False
|
||||
if self._internal_after_id and self._root_for_widget_update.winfo_exists():
|
||||
try:
|
||||
self._root_for_widget_update.after_cancel(self._internal_after_id)
|
||||
except Exception:
|
||||
pass
|
||||
self._internal_after_id = None
|
||||
# print("DEBUG: TkinterTextHandler._process_widget_update_queue stopping (inactive/widget gone).", flush=True)
|
||||
return
|
||||
|
||||
had_messages = False
|
||||
try:
|
||||
while not self._widget_update_queue.empty():
|
||||
had_messages = True
|
||||
level_name, msg = self._widget_update_queue.get_nowait()
|
||||
if self.text_widget.winfo_exists():
|
||||
self.text_widget.configure(state=tk.NORMAL)
|
||||
self.text_widget.insert(tk.END, msg + "\n", (level_name,))
|
||||
self.text_widget.configure(state=tk.DISABLED)
|
||||
else: # Widget destroyed during processing
|
||||
self._is_active = False
|
||||
break
|
||||
self._widget_update_queue.task_done()
|
||||
if had_messages and self.text_widget.winfo_exists():
|
||||
self.text_widget.see(tk.END)
|
||||
|
||||
except tk.TclError as e_tcl:
|
||||
print(
|
||||
f"TkinterTextHandler TclError during widget update: {e_tcl}. Handler stopping.",
|
||||
flush=True,
|
||||
)
|
||||
self._is_active = False
|
||||
except Exception as e_update:
|
||||
print(
|
||||
f"Unexpected error updating TkinterTextHandler widget: {e_update}. Handler stopping.",
|
||||
flush=True,
|
||||
)
|
||||
self._is_active = False
|
||||
|
||||
if self._is_active and self._root_for_widget_update.winfo_exists():
|
||||
self._internal_after_id = self._root_for_widget_update.after(
|
||||
self._internal_poll_interval_ms, self._process_widget_update_queue
|
||||
)
|
||||
else: # Ensure it's cleaned up if we are stopping
|
||||
if self._internal_after_id and self._root_for_widget_update.winfo_exists():
|
||||
try:
|
||||
self._root_for_widget_update.after_cancel(self._internal_after_id)
|
||||
except Exception:
|
||||
pass
|
||||
self._internal_after_id = None
|
||||
|
||||
def close(self):
|
||||
# print("INFO: TkinterTextHandler close called.", flush=True) # Reduced verbosity
|
||||
self._is_active = False
|
||||
if (
|
||||
self._internal_after_id
|
||||
and hasattr(self, "_root_for_widget_update")
|
||||
and self._root_for_widget_update
|
||||
and self._root_for_widget_update.winfo_exists()
|
||||
):
|
||||
try:
|
||||
self._root_for_widget_update.after_cancel(self._internal_after_id)
|
||||
except Exception:
|
||||
pass
|
||||
self._internal_after_id = None
|
||||
# Drain its internal queue
|
||||
while not self._widget_update_queue.empty():
|
||||
try:
|
||||
self._widget_update_queue.get_nowait()
|
||||
self._widget_update_queue.task_done()
|
||||
except Exception:
|
||||
break
|
||||
super().close()
|
||||
|
||||
|
||||
class QueuePuttingHandler(logging.Handler):
|
||||
"""
|
||||
A simple handler that puts any received LogRecord into a global queue.
|
||||
"""
|
||||
|
||||
def __init__(self, handler_queue: Queue[logging.LogRecord]):
|
||||
super().__init__()
|
||||
self.handler_queue = handler_queue
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
try:
|
||||
self.handler_queue.put_nowait(record)
|
||||
except queue.Full: # Standard library 'queue.Full'
|
||||
print(
|
||||
f"CRITICAL: Global log queue is full! Log record for '{record.name}' might be lost.",
|
||||
flush=True,
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"CRITICAL: Error putting log record into global queue: {e}", flush=True
|
||||
)
|
||||
|
||||
|
||||
def _process_global_log_queue():
|
||||
"""
|
||||
GUI Thread: Periodically processes LogRecords from the _global_log_queue
|
||||
and dispatches them to the actual configured handlers.
|
||||
"""
|
||||
global _logging_system_active, _log_processor_after_id
|
||||
# print(f"DEBUG: _process_global_log_queue called. Active: {_logging_system_active}", flush=True)
|
||||
|
||||
if not _logging_system_active:
|
||||
if (
|
||||
_log_processor_after_id
|
||||
and _tk_root_instance_for_processing
|
||||
and _tk_root_instance_for_processing.winfo_exists()
|
||||
):
|
||||
try:
|
||||
_tk_root_instance_for_processing.after_cancel(_log_processor_after_id)
|
||||
except Exception:
|
||||
pass
|
||||
_log_processor_after_id = None
|
||||
return
|
||||
|
||||
if not (
|
||||
_tk_root_instance_for_processing
|
||||
and _tk_root_instance_for_processing.winfo_exists()
|
||||
):
|
||||
_logging_system_active = False
|
||||
_log_processor_after_id = None
|
||||
return
|
||||
|
||||
processed_count = 0
|
||||
try:
|
||||
while _global_log_queue and not _global_log_queue.empty():
|
||||
if not _logging_system_active:
|
||||
break
|
||||
try:
|
||||
record = _global_log_queue.get_nowait()
|
||||
processed_count += 1
|
||||
|
||||
if _actual_console_handler:
|
||||
try:
|
||||
_actual_console_handler.handle(record)
|
||||
except Exception as e_con:
|
||||
print(f"Error in console_handler.handle: {e_con}", flush=True)
|
||||
|
||||
if _actual_file_handler:
|
||||
try:
|
||||
_actual_file_handler.handle(record)
|
||||
except Exception as e_file:
|
||||
print(f"Error in file_handler.handle: {e_file}", flush=True)
|
||||
|
||||
if _actual_tkinter_handler:
|
||||
try:
|
||||
_actual_tkinter_handler.handle(record)
|
||||
except Exception as e_tk:
|
||||
print(f"Error in tkinter_handler.handle: {e_tk}", flush=True)
|
||||
|
||||
_global_log_queue.task_done()
|
||||
except QueueEmpty:
|
||||
break
|
||||
except Exception as e_proc_item:
|
||||
print(
|
||||
f"Error processing a log item from global queue: {e_proc_item}",
|
||||
flush=True,
|
||||
)
|
||||
except Exception as e_outer_loop:
|
||||
print(
|
||||
f"Critical error in _process_global_log_queue outer loop: {e_outer_loop}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if _logging_system_active and _tk_root_instance_for_processing.winfo_exists():
|
||||
_log_processor_after_id = _tk_root_instance_for_processing.after(
|
||||
GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue
|
||||
)
|
||||
|
||||
|
||||
def setup_basic_logging( # MODIFIED: Renamed from setup_logging
|
||||
root_tk_instance_for_processor: Optional[
|
||||
tk.Tk
|
||||
], # MODIFIED: Now mandatory for processor
|
||||
logging_config_dict: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
global _global_log_queue, _actual_console_handler, _actual_file_handler
|
||||
global _logging_system_active, _tk_root_instance_for_processing
|
||||
global _log_processor_after_id, _base_formatter, _actual_tkinter_handler # _actual_tkinter_handler should be None here
|
||||
|
||||
# This function should only be called ONCE.
|
||||
if _logging_system_active:
|
||||
print(
|
||||
"WARNING: setup_basic_logging called but logging system is already active. Ignoring call.",
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
|
||||
print("INFO: Configuring basic centralized queued logging system...", flush=True)
|
||||
_actual_tkinter_handler = None # Ensure it's None at basic setup
|
||||
|
||||
if logging_config_dict is None:
|
||||
print(
|
||||
"Warning: No logging_config_dict. Using basic console-only defaults for queued logging.",
|
||||
flush=True,
|
||||
)
|
||||
# Simplified default, as Tkinter parts are handled by add_tkinter_handler
|
||||
logging_config_dict = {
|
||||
"default_root_level": logging.INFO,
|
||||
"specific_levels": {},
|
||||
"format": "%(asctime)s [%(levelname)-8s] %(name)-25s : %(message)s",
|
||||
"date_format": "%Y-%m-%d %H:%M:%S",
|
||||
"enable_console": True,
|
||||
"enable_file": False,
|
||||
}
|
||||
|
||||
root_log_level = logging_config_dict.get("default_root_level", logging.INFO)
|
||||
specific_levels = logging_config_dict.get("specific_levels", {})
|
||||
log_format_str = logging_config_dict.get(
|
||||
"format", "%(asctime)s [%(levelname)-8s] %(name)-25s : %(message)s"
|
||||
)
|
||||
log_date_format_str = logging_config_dict.get("date_format", "%Y-%m-%d %H:%M:%S")
|
||||
enable_console = logging_config_dict.get("enable_console", True)
|
||||
enable_file = logging_config_dict.get("enable_file", False)
|
||||
|
||||
_base_formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str)
|
||||
_global_log_queue = Queue()
|
||||
_tk_root_instance_for_processing = root_tk_instance_for_processor
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers[
|
||||
:
|
||||
]: # Clear any pre-existing handlers (e.g. from basicConfig)
|
||||
try:
|
||||
handler.close()
|
||||
root_logger.removeHandler(handler)
|
||||
except Exception:
|
||||
pass
|
||||
root_logger.handlers = []
|
||||
root_logger.setLevel(root_log_level)
|
||||
print(
|
||||
f"INFO: Root logger level set to {logging.getLevelName(root_logger.level)}.",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
for logger_name, level in specific_levels.items():
|
||||
named_logger = logging.getLogger(logger_name)
|
||||
named_logger.setLevel(level)
|
||||
named_logger.propagate = True
|
||||
print(
|
||||
f"INFO: Logger '{logger_name}' level set to {logging.getLevelName(named_logger.level)}.",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if enable_console:
|
||||
try:
|
||||
_actual_console_handler = logging.StreamHandler()
|
||||
_actual_console_handler.setFormatter(_base_formatter)
|
||||
_actual_console_handler.setLevel(logging.DEBUG)
|
||||
print("INFO: Actual ConsoleHandler created.", flush=True)
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to create actual ConsoleHandler: {e}", flush=True)
|
||||
_actual_console_handler = None
|
||||
|
||||
if enable_file:
|
||||
try:
|
||||
file_path = logging_config_dict.get("file_path", "flight_monitor_app.log")
|
||||
file_max_bytes = logging_config_dict.get("file_max_bytes", 5 * 1024 * 1024)
|
||||
file_backup_count = logging_config_dict.get("file_backup_count", 3)
|
||||
_actual_file_handler = logging.handlers.RotatingFileHandler(
|
||||
file_path,
|
||||
maxBytes=file_max_bytes,
|
||||
backupCount=file_backup_count,
|
||||
encoding="utf-8",
|
||||
)
|
||||
_actual_file_handler.setFormatter(_base_formatter)
|
||||
_actual_file_handler.setLevel(logging.DEBUG)
|
||||
print(
|
||||
f"INFO: Actual RotatingFileHandler created for '{file_path}'.",
|
||||
flush=True,
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"ERROR: Failed to create actual FileHandler for '{file_path}': {e}",
|
||||
flush=True,
|
||||
)
|
||||
_actual_file_handler = None
|
||||
|
||||
if _global_log_queue is not None:
|
||||
queue_putter = QueuePuttingHandler(handler_queue=_global_log_queue)
|
||||
queue_putter.setLevel(logging.DEBUG)
|
||||
root_logger.addHandler(queue_putter)
|
||||
print(f"INFO: QueuePuttingHandler added to root logger.", flush=True)
|
||||
else:
|
||||
print(
|
||||
"CRITICAL ERROR: Global log queue not initialized in basic setup!",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
_logging_system_active = True
|
||||
if (
|
||||
_tk_root_instance_for_processing
|
||||
and _tk_root_instance_for_processing.winfo_exists()
|
||||
):
|
||||
if _log_processor_after_id: # Should be None here, but defensive
|
||||
try:
|
||||
_tk_root_instance_for_processing.after_cancel(_log_processor_after_id)
|
||||
except Exception:
|
||||
pass
|
||||
_log_processor_after_id = _tk_root_instance_for_processing.after(
|
||||
GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue
|
||||
)
|
||||
print(
|
||||
f"INFO: Global log queue processor scheduled (ID: {_log_processor_after_id}).",
|
||||
flush=True,
|
||||
)
|
||||
elif not _tk_root_instance_for_processing:
|
||||
print(
|
||||
"WARNING: No Tkinter root instance for processor. Logs will queue but not be dispatched by this setup function alone.",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
print("INFO: Basic centralized queued logging system setup complete.", flush=True)
|
||||
logging.getLogger("FlightMonitor.LoggerTest").info(
|
||||
"Basic logging initialized. This is a test from setup_basic_logging."
|
||||
)
|
||||
|
||||
|
||||
def add_tkinter_handler( # NEW FUNCTION
|
||||
gui_log_widget: tk.Text,
|
||||
root_tk_instance_for_gui_handler: tk.Tk,
|
||||
logging_config_dict: Dict[str, Any],
|
||||
):
|
||||
global _actual_tkinter_handler, _base_formatter
|
||||
|
||||
if not _logging_system_active:
|
||||
print(
|
||||
"ERROR: Cannot add Tkinter handler, basic logging system not active.",
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
if not _base_formatter:
|
||||
print("ERROR: Cannot add Tkinter handler, base formatter not set.", flush=True)
|
||||
return
|
||||
|
||||
print("INFO: Attempting to add TkinterTextHandler...", flush=True)
|
||||
|
||||
if _actual_tkinter_handler: # If one already exists, close and prepare to replace
|
||||
print(
|
||||
"INFO: Existing TkinterTextHandler found, closing it before adding new one.",
|
||||
flush=True,
|
||||
)
|
||||
try:
|
||||
_actual_tkinter_handler.close()
|
||||
except Exception as e_close_old_tk:
|
||||
print(
|
||||
f"Warning: Error closing old TkinterTextHandler: {e_close_old_tk}",
|
||||
flush=True,
|
||||
)
|
||||
_actual_tkinter_handler = None
|
||||
|
||||
is_widget_valid = (
|
||||
isinstance(gui_log_widget, (tk.Text, ScrolledText))
|
||||
and hasattr(gui_log_widget, "winfo_exists")
|
||||
and gui_log_widget.winfo_exists()
|
||||
)
|
||||
is_root_valid = (
|
||||
root_tk_instance_for_gui_handler
|
||||
and hasattr(root_tk_instance_for_gui_handler, "winfo_exists")
|
||||
and root_tk_instance_for_gui_handler.winfo_exists()
|
||||
)
|
||||
|
||||
if is_widget_valid and is_root_valid:
|
||||
try:
|
||||
level_colors = logging_config_dict.get("colors", {})
|
||||
tkinter_handler_poll_ms = logging_config_dict.get(
|
||||
"queue_poll_interval_ms", 100
|
||||
)
|
||||
|
||||
_actual_tkinter_handler = TkinterTextHandler(
|
||||
text_widget=gui_log_widget,
|
||||
level_colors=level_colors,
|
||||
root_tk_instance_for_widget_update=root_tk_instance_for_gui_handler,
|
||||
internal_poll_interval_ms=tkinter_handler_poll_ms,
|
||||
)
|
||||
_actual_tkinter_handler.setFormatter(_base_formatter)
|
||||
_actual_tkinter_handler.setLevel(logging.DEBUG)
|
||||
print(
|
||||
"INFO: TkinterTextHandler added and configured successfully.",
|
||||
flush=True,
|
||||
)
|
||||
logging.getLogger("FlightMonitor.LoggerTest").info(
|
||||
"TkinterTextHandler added. This is a test from add_tkinter_handler."
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"ERROR: Failed to create and add TkinterTextHandler: {e}", flush=True
|
||||
)
|
||||
_actual_tkinter_handler = None
|
||||
else:
|
||||
if not is_widget_valid:
|
||||
print(
|
||||
"ERROR: GUI log widget invalid or non-existent, cannot add TkinterTextHandler.",
|
||||
flush=True,
|
||||
)
|
||||
if not is_root_valid:
|
||||
print(
|
||||
"ERROR: Root Tk instance for GUI handler invalid or non-existent.",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""
|
||||
Retrieves a logger instance by name.
|
||||
Module-level loggers should use `get_logger(__name__)`.
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
def shutdown_logging_system():
|
||||
"""
|
||||
Shuts down the centralized logging system, processing remaining logs
|
||||
and closing actual handlers.
|
||||
"""
|
||||
global _logging_system_active, _log_processor_after_id
|
||||
global _actual_console_handler, _actual_file_handler, _actual_tkinter_handler
|
||||
global _global_log_queue, _tk_root_instance_for_processing
|
||||
|
||||
if (
|
||||
not _logging_system_active and not _global_log_queue
|
||||
): # Extra check if it was never really active
|
||||
print(
|
||||
"INFO: Logging system shutdown called, but system was not fully active or already shut down.",
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
|
||||
print("INFO: Initiating shutdown of centralized logging system...", flush=True)
|
||||
_logging_system_active = False
|
||||
|
||||
if (
|
||||
_log_processor_after_id
|
||||
and _tk_root_instance_for_processing
|
||||
and _tk_root_instance_for_processing.winfo_exists()
|
||||
):
|
||||
try:
|
||||
_tk_root_instance_for_processing.after_cancel(_log_processor_after_id)
|
||||
# print("INFO: Cancelled global log queue processor task.", flush=True) # Reduced verbosity
|
||||
except Exception as e_cancel:
|
||||
print(
|
||||
f"Warning: Error cancelling log processor task: {e_cancel}", flush=True
|
||||
)
|
||||
_log_processor_after_id = None
|
||||
|
||||
# print("INFO: Processing any remaining logs in the global queue before full shutdown...", flush=True) # Reduced
|
||||
if _global_log_queue:
|
||||
final_processed_count = 0
|
||||
while not _global_log_queue.empty():
|
||||
try:
|
||||
record = _global_log_queue.get_nowait()
|
||||
final_processed_count += 1
|
||||
if _actual_console_handler:
|
||||
_actual_console_handler.handle(record)
|
||||
if _actual_file_handler:
|
||||
_actual_file_handler.handle(record)
|
||||
if _actual_tkinter_handler:
|
||||
_actual_tkinter_handler.handle(record)
|
||||
_global_log_queue.task_done()
|
||||
except QueueEmpty:
|
||||
break
|
||||
except Exception as e_final_proc:
|
||||
print(f"Error during final log processing: {e_final_proc}", flush=True)
|
||||
break
|
||||
# if final_processed_count > 0: print(f"INFO: Processed {final_processed_count} remaining log messages.", flush=True)
|
||||
# else: print("INFO: No remaining log messages in global queue to process.", flush=True)
|
||||
|
||||
if _actual_tkinter_handler:
|
||||
try:
|
||||
# print("INFO: Closing actual TkinterTextHandler.", flush=True) # Reduced
|
||||
_actual_tkinter_handler.close()
|
||||
except Exception as e:
|
||||
print(f"Error closing TkinterTextHandler: {e}", flush=True)
|
||||
_actual_tkinter_handler = None
|
||||
|
||||
if _actual_console_handler:
|
||||
try:
|
||||
# print("INFO: Closing actual ConsoleHandler.", flush=True) # Reduced
|
||||
_actual_console_handler.close()
|
||||
except Exception as e:
|
||||
print(f"Error closing ConsoleHandler: {e}", flush=True)
|
||||
_actual_console_handler = None
|
||||
|
||||
if _actual_file_handler:
|
||||
try:
|
||||
# print("INFO: Closing actual FileHandler.", flush=True) # Reduced
|
||||
_actual_file_handler.close()
|
||||
except Exception as e:
|
||||
print(f"Error closing FileHandler: {e}", flush=True)
|
||||
_actual_file_handler = None
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers[:]:
|
||||
try:
|
||||
if isinstance(handler, QueuePuttingHandler):
|
||||
handler.close()
|
||||
root_logger.removeHandler(handler)
|
||||
# print(f"INFO: Removed and closed QueuePuttingHandler: {handler}", flush=True) # Reduced
|
||||
except Exception as e_rem_qph:
|
||||
print(f"Error removing QueuePuttingHandler: {e_rem_qph}", flush=True)
|
||||
|
||||
_global_log_queue = None
|
||||
_tk_root_instance_for_processing = None
|
||||
_base_formatter = None
|
||||
print("INFO: Centralized logging system shutdown complete.", flush=True)
|
||||
Loading…
Reference in New Issue
Block a user