TCPDashboard/utils/logger.py

154 lines
5.5 KiB
Python
Raw Permalink Normal View History

"""
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()