TCPDashboard/utils/logger.py
Vasily.onl e147aa1873 Update logging documentation and refactor logger implementation
- Revised the logging documentation to clarify the unified logging system's features and usage patterns.
- Simplified the logger implementation by removing the custom `DateRotatingFileHandler` and utilizing the standard library's `TimedRotatingFileHandler` for date-based log rotation.
- Enhanced the `get_logger` function to ensure thread-safe logger configuration and prevent duplicate handlers.
- Introduced a new `cleanup_old_logs` function for age-based log cleanup, while retaining the existing count-based cleanup mechanism.
- Improved error handling and logging setup to ensure robust logging behavior across components.

These changes enhance the clarity and maintainability of the logging system, making it easier for developers to implement and utilize logging in their components.
2025-06-06 21:02:08 +08:00

153 lines
5.4 KiB
Python

"""
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, 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')
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 - %(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()