341 lines
13 KiB
Python
341 lines
13 KiB
Python
|
|
"""
|
||
|
|
Unified logging system for the TCP Dashboard project.
|
||
|
|
|
||
|
|
Provides centralized logging with:
|
||
|
|
- Component-specific log directories
|
||
|
|
- Date-based file rotation
|
||
|
|
- Unified log format: [YYYY-MM-DD HH:MM:SS - LEVEL - message]
|
||
|
|
- Thread-safe operations
|
||
|
|
- Automatic directory creation
|
||
|
|
- Verbose console logging with proper level handling
|
||
|
|
- Automatic old log cleanup
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
from utils.logger import get_logger
|
||
|
|
|
||
|
|
logger = get_logger('bot_manager')
|
||
|
|
logger.info("This is an info message")
|
||
|
|
logger.error("This is an error message")
|
||
|
|
|
||
|
|
# With verbose console output
|
||
|
|
logger = get_logger('bot_manager', verbose=True)
|
||
|
|
|
||
|
|
# With custom cleanup settings
|
||
|
|
logger = get_logger('bot_manager', clean_old_logs=True, max_log_files=7)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import logging
|
||
|
|
import os
|
||
|
|
from datetime import datetime
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Dict, Optional
|
||
|
|
import threading
|
||
|
|
|
||
|
|
|
||
|
|
class DateRotatingFileHandler(logging.FileHandler):
|
||
|
|
"""
|
||
|
|
Custom file handler that rotates log files based on date changes.
|
||
|
|
Creates new log files when the date changes to ensure daily separation.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, log_dir: Path, component_name: str, cleanup_callback=None, max_files=30):
|
||
|
|
self.log_dir = log_dir
|
||
|
|
self.component_name = component_name
|
||
|
|
self.current_date = None
|
||
|
|
self.cleanup_callback = cleanup_callback
|
||
|
|
self.max_files = max_files
|
||
|
|
self._lock = threading.Lock()
|
||
|
|
|
||
|
|
# Initialize with today's file
|
||
|
|
self._update_filename()
|
||
|
|
super().__init__(self.current_filename, mode='a', encoding='utf-8')
|
||
|
|
|
||
|
|
def _update_filename(self):
|
||
|
|
"""Update the filename based on current date."""
|
||
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
||
|
|
if self.current_date != today:
|
||
|
|
self.current_date = today
|
||
|
|
self.current_filename = self.log_dir / f"{today}.txt"
|
||
|
|
|
||
|
|
# Ensure the directory exists
|
||
|
|
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
# Cleanup old logs if callback is provided
|
||
|
|
if self.cleanup_callback:
|
||
|
|
self.cleanup_callback(self.component_name, self.max_files)
|
||
|
|
|
||
|
|
def emit(self, record):
|
||
|
|
"""Emit a log record, rotating file if date has changed."""
|
||
|
|
with self._lock:
|
||
|
|
# Check if we need to rotate to a new file
|
||
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
||
|
|
if self.current_date != today:
|
||
|
|
# Close current file
|
||
|
|
if hasattr(self, 'stream') and self.stream:
|
||
|
|
self.stream.close()
|
||
|
|
|
||
|
|
# Update filename and reopen (this will trigger cleanup)
|
||
|
|
self._update_filename()
|
||
|
|
self.baseFilename = str(self.current_filename)
|
||
|
|
self.stream = self._open()
|
||
|
|
|
||
|
|
super().emit(record)
|
||
|
|
|
||
|
|
|
||
|
|
class UnifiedLogger:
|
||
|
|
"""
|
||
|
|
Unified logger class that manages component-specific loggers with consistent formatting.
|
||
|
|
"""
|
||
|
|
|
||
|
|
_loggers: Dict[str, logging.Logger] = {}
|
||
|
|
_lock = threading.Lock()
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def get_logger(cls, component_name: str, 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.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
component_name: Name of the component (e.g., 'bot_manager', 'data_collector')
|
||
|
|
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||
|
|
verbose: Enable console logging. If None, uses VERBOSE_LOGGING from .env
|
||
|
|
clean_old_logs: Automatically clean old log files when creating new ones
|
||
|
|
max_log_files: Maximum number of log files to keep (default: 30)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Configured logger instance for the component
|
||
|
|
"""
|
||
|
|
# Create a unique key for logger configuration
|
||
|
|
logger_key = f"{component_name}_{log_level}_{verbose}_{clean_old_logs}_{max_log_files}"
|
||
|
|
|
||
|
|
with cls._lock:
|
||
|
|
if logger_key in cls._loggers:
|
||
|
|
return cls._loggers[logger_key]
|
||
|
|
|
||
|
|
# Create new logger
|
||
|
|
logger = logging.getLogger(f"tcp_dashboard.{component_name}.{hash(logger_key) % 10000}")
|
||
|
|
logger.setLevel(getattr(logging, log_level.upper()))
|
||
|
|
|
||
|
|
# Prevent duplicate handlers if logger already exists
|
||
|
|
if logger.handlers:
|
||
|
|
logger.handlers.clear()
|
||
|
|
|
||
|
|
# Create log directory for component
|
||
|
|
log_dir = Path("logs") / component_name
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Setup cleanup callback if enabled
|
||
|
|
cleanup_callback = cls._cleanup_old_logs if clean_old_logs else None
|
||
|
|
|
||
|
|
# Add date-rotating file handler
|
||
|
|
file_handler = DateRotatingFileHandler(
|
||
|
|
log_dir, component_name, cleanup_callback, max_log_files
|
||
|
|
)
|
||
|
|
file_handler.setLevel(logging.DEBUG)
|
||
|
|
|
||
|
|
# Create unified formatter
|
||
|
|
formatter = logging.Formatter(
|
||
|
|
'[%(asctime)s - %(levelname)s - %(message)s]',
|
||
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||
|
|
)
|
||
|
|
file_handler.setFormatter(formatter)
|
||
|
|
logger.addHandler(file_handler)
|
||
|
|
|
||
|
|
# Add console handler based on verbose setting
|
||
|
|
should_log_to_console = cls._should_enable_console_logging(verbose)
|
||
|
|
if should_log_to_console:
|
||
|
|
console_handler = logging.StreamHandler()
|
||
|
|
|
||
|
|
# Set console log level based on log_level with proper type handling
|
||
|
|
console_level = cls._get_console_log_level(log_level)
|
||
|
|
console_handler.setLevel(console_level)
|
||
|
|
|
||
|
|
# Use colored formatter for console if available
|
||
|
|
console_formatter = cls._get_console_formatter()
|
||
|
|
console_handler.setFormatter(console_formatter)
|
||
|
|
logger.addHandler(console_handler)
|
||
|
|
|
||
|
|
# Prevent propagation to root logger
|
||
|
|
logger.propagate = False
|
||
|
|
|
||
|
|
cls._loggers[logger_key] = logger
|
||
|
|
|
||
|
|
# Log initialization
|
||
|
|
logger.info(f"Logger initialized for component: {component_name} "
|
||
|
|
f"(verbose={should_log_to_console}, cleanup={clean_old_logs}, "
|
||
|
|
f"max_files={max_log_files})")
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
# Fallback to console logging if file logging fails
|
||
|
|
print(f"Warning: Failed to setup file logging for {component_name}: {e}")
|
||
|
|
console_handler = logging.StreamHandler()
|
||
|
|
console_handler.setLevel(logging.INFO)
|
||
|
|
formatter = logging.Formatter('[%(asctime)s - %(levelname)s - %(message)s]')
|
||
|
|
console_handler.setFormatter(formatter)
|
||
|
|
logger.addHandler(console_handler)
|
||
|
|
logger.propagate = False
|
||
|
|
cls._loggers[logger_key] = logger
|
||
|
|
|
||
|
|
return logger
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def _should_enable_console_logging(cls, verbose: Optional[bool]) -> bool:
|
||
|
|
"""
|
||
|
|
Determine if console logging should be enabled.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
verbose: Explicit verbose setting, or None to use environment variable
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if console logging should be enabled
|
||
|
|
"""
|
||
|
|
if verbose is not None:
|
||
|
|
return verbose
|
||
|
|
|
||
|
|
# Check environment variables
|
||
|
|
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')
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def _get_console_log_level(cls, log_level: str) -> int:
|
||
|
|
"""
|
||
|
|
Get appropriate console log level based on file log level.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
log_level: File logging level
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Console logging level (integer)
|
||
|
|
"""
|
||
|
|
# Map file log levels to console log levels
|
||
|
|
# Generally, console should be less verbose than file
|
||
|
|
level_mapping = {
|
||
|
|
'DEBUG': logging.DEBUG, # Show all debug info on console too
|
||
|
|
'INFO': logging.INFO, # Show info and above
|
||
|
|
'WARNING': logging.WARNING, # Show warnings and above
|
||
|
|
'ERROR': logging.ERROR, # Show errors and above
|
||
|
|
'CRITICAL': logging.CRITICAL # Show only critical
|
||
|
|
}
|
||
|
|
|
||
|
|
return level_mapping.get(log_level.upper(), logging.INFO)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def _get_console_formatter(cls) -> logging.Formatter:
|
||
|
|
"""
|
||
|
|
Get formatter for console output with potential color support.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Configured formatter for console output
|
||
|
|
"""
|
||
|
|
# Basic formatter - could be enhanced with colors in the future
|
||
|
|
return logging.Formatter(
|
||
|
|
'[%(asctime)s - %(levelname)s - %(message)s]',
|
||
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||
|
|
)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def _cleanup_old_logs(cls, component_name: str, max_files: int = 30):
|
||
|
|
"""
|
||
|
|
Clean up old log files for a component, keeping only the most recent files.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
component_name: Name of the component
|
||
|
|
max_files: Maximum number of log files to keep
|
||
|
|
"""
|
||
|
|
log_dir = Path("logs") / component_name
|
||
|
|
if not log_dir.exists():
|
||
|
|
return
|
||
|
|
|
||
|
|
# Get all log files sorted by modification time (newest first)
|
||
|
|
log_files = sorted(
|
||
|
|
log_dir.glob("*.txt"),
|
||
|
|
key=lambda f: f.stat().st_mtime,
|
||
|
|
reverse=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Keep only the most recent max_files
|
||
|
|
files_to_delete = log_files[max_files:]
|
||
|
|
|
||
|
|
for log_file in files_to_delete:
|
||
|
|
try:
|
||
|
|
log_file.unlink()
|
||
|
|
# Only log to console to avoid recursive logging
|
||
|
|
if cls._should_enable_console_logging(None):
|
||
|
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - INFO - "
|
||
|
|
f"Deleted old log file: {log_file}]")
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - WARNING - "
|
||
|
|
f"Failed to delete old log file {log_file}: {e}]")
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def cleanup_old_logs(cls, component_name: str, days_to_keep: int = 30):
|
||
|
|
"""
|
||
|
|
Clean up old log files for a component based on age.
|
||
|
|
|
||
|
|
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.exists():
|
||
|
|
return
|
||
|
|
|
||
|
|
cutoff_date = datetime.now().timestamp() - (days_to_keep * 24 * 60 * 60)
|
||
|
|
|
||
|
|
for log_file in log_dir.glob("*.txt"):
|
||
|
|
if log_file.stat().st_mtime < cutoff_date:
|
||
|
|
try:
|
||
|
|
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}")
|
||
|
|
|
||
|
|
|
||
|
|
# Convenience function for easy import
|
||
|
|
def get_logger(component_name: str, log_level: str = "INFO",
|
||
|
|
verbose: Optional[bool] = None, clean_old_logs: bool = True,
|
||
|
|
max_log_files: int = 30) -> logging.Logger:
|
||
|
|
"""
|
||
|
|
Get a logger instance for the specified component.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
component_name: Name of the component (e.g., 'bot_manager', 'data_collector')
|
||
|
|
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||
|
|
verbose: Enable console logging. If None, uses VERBOSE_LOGGING from .env
|
||
|
|
clean_old_logs: Automatically clean old log files when creating new ones
|
||
|
|
max_log_files: Maximum number of log files to keep (default: 30)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Configured logger instance
|
||
|
|
|
||
|
|
Example:
|
||
|
|
from utils.logger import get_logger
|
||
|
|
|
||
|
|
# Basic usage
|
||
|
|
logger = get_logger('bot_manager')
|
||
|
|
|
||
|
|
# With verbose console output
|
||
|
|
logger = get_logger('bot_manager', verbose=True)
|
||
|
|
|
||
|
|
# With custom cleanup settings
|
||
|
|
logger = get_logger('bot_manager', clean_old_logs=True, max_log_files=7)
|
||
|
|
|
||
|
|
logger.info("Bot started successfully")
|
||
|
|
logger.error("Connection failed", exc_info=True)
|
||
|
|
"""
|
||
|
|
return UnifiedLogger.get_logger(component_name, log_level, verbose, clean_old_logs, max_log_files)
|
||
|
|
|
||
|
|
|
||
|
|
def cleanup_old_logs(component_name: str, days_to_keep: int = 30):
|
||
|
|
"""
|
||
|
|
Clean up old log files for a component based on age.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
component_name: Name of the component
|
||
|
|
days_to_keep: Number of days of logs to retain (default: 30)
|
||
|
|
"""
|
||
|
|
UnifiedLogger.cleanup_old_logs(component_name, days_to_keep)
|