""" Unified logging system for the TCP Dashboard project. Provides centralized logging with: - Component-specific log directories - Date-based file rotation using standard library handlers - Unified log format: [YYYY-MM-DD HH:MM:SS - LEVEL - message] - Thread-safe operations - Automatic directory creation - Verbose console logging with proper level handling Usage: from utils.logger import get_logger, cleanup_old_logs logger = get_logger('bot_manager') logger.info("This is an info message") # Clean up logs older than 7 days cleanup_old_logs('bot_manager', days_to_keep=7) """ import logging import logging.handlers import os from datetime import datetime from pathlib import Path from typing import Optional import threading # Lock for thread-safe logger configuration _lock = threading.Lock() def get_logger(component_name: str = "default_logger", log_level: str = "INFO", verbose: Optional[bool] = None, clean_old_logs: bool = True, max_log_files: int = 30) -> logging.Logger: """ Get or create a logger for the specified component. This function is thread-safe and ensures that handlers are not duplicated. Args: component_name: Name of the component (e.g., 'bot_manager', 'data_collector'). Defaults to 'default_logger' if not provided. log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) verbose: Enable console logging. If None, uses VERBOSE_LOGGING from .env clean_old_logs: (Deprecated) This is now handled by max_log_files. The parameter is kept for backward compatibility. max_log_files: Maximum number of log files to keep (default: 30) Returns: Configured logger instance for the component """ with _lock: logger_name = f"tcp_dashboard.{component_name}" logger = logging.getLogger(logger_name) # Avoid re-configuring if logger already has handlers if logger.handlers: return logger # Set logger level try: level = getattr(logging, log_level.upper()) logger.setLevel(level) except AttributeError: print(f"Warning: Invalid log level '{log_level}'. Defaulting to INFO.") logger.setLevel(logging.INFO) # Prevent propagation to root logger logger.propagate = False # Create log directory for component log_dir = Path("logs") / component_name log_dir.mkdir(parents=True, exist_ok=True) # Unified formatter formatter = logging.Formatter( '[%(asctime)s - %(levelname)s - %(pathname)s:%(lineno)d - %(funcName)s] - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # Add date-rotating file handler try: log_file = log_dir / f"{component_name}.log" # Rotates at midnight, keeps 'max_log_files' backups file_handler = logging.handlers.TimedRotatingFileHandler( log_file, when='midnight', interval=1, backupCount=max_log_files, encoding='utf-8' ) file_handler.setFormatter(formatter) logger.addHandler(file_handler) except Exception as e: print(f"Warning: Failed to setup file logging for {component_name}: {e}") # Add console handler based on verbose setting if _should_enable_console_logging(verbose): console_handler = logging.StreamHandler() console_level = _get_console_log_level(log_level) console_handler.setLevel(console_level) console_handler.setFormatter(formatter) logger.addHandler(console_handler) return logger def _should_enable_console_logging(verbose: Optional[bool]) -> bool: """Determine if console logging should be enabled.""" if verbose is not None: return verbose env_verbose = os.getenv('VERBOSE_LOGGING', 'false').lower() env_console = os.getenv('LOG_TO_CONSOLE', 'false').lower() return env_verbose in ('true', '1', 'yes') or env_console in ('true', '1', 'yes') def _get_console_log_level(log_level: str) -> int: """Get appropriate console log level.""" level_mapping = { 'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'WARNING': logging.WARNING, 'ERROR': logging.ERROR, 'CRITICAL': logging.CRITICAL } return level_mapping.get(log_level.upper(), logging.INFO) def cleanup_old_logs(component_name: str, days_to_keep: int = 30): """ Clean up old log files for a component based on age. Note: TimedRotatingFileHandler already manages log file counts. This function is for age-based cleanup, which might be redundant but is kept for specific use cases. Args: component_name: Name of the component days_to_keep: Number of days of logs to retain """ log_dir = Path("logs") / component_name if not log_dir.is_dir(): return cutoff_date = datetime.now().timestamp() - (days_to_keep * 24 * 60 * 60) for log_file in log_dir.glob("*"): try: if log_file.is_file() and log_file.stat().st_mtime < cutoff_date: log_file.unlink() print(f"Deleted old log file: {log_file}") except Exception as e: print(f"Failed to delete old log file {log_file}: {e}") def shutdown_logging(): """ Shuts down the logging system, closing all file handlers. This is important for clean exit, especially in tests. """ logging.shutdown()