2025-05-30 19:54:56 +08:00
|
|
|
"""
|
|
|
|
|
Unified logging system for the TCP Dashboard project.
|
|
|
|
|
|
|
|
|
|
Provides centralized logging with:
|
|
|
|
|
- Component-specific log directories
|
2025-06-06 21:02:08 +08:00
|
|
|
- Date-based file rotation using standard library handlers
|
2025-05-30 19:54:56 +08:00
|
|
|
- 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:
|
2025-06-06 21:02:08 +08:00
|
|
|
from utils.logger import get_logger, cleanup_old_logs
|
2025-05-30 19:54:56 +08:00
|
|
|
|
|
|
|
|
logger = get_logger('bot_manager')
|
|
|
|
|
logger.info("This is an info message")
|
|
|
|
|
|
2025-06-06 21:02:08 +08:00
|
|
|
# Clean up logs older than 7 days
|
|
|
|
|
cleanup_old_logs('bot_manager', days_to_keep=7)
|
2025-05-30 19:54:56 +08:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import logging
|
2025-06-06 21:02:08 +08:00
|
|
|
import logging.handlers
|
2025-05-30 19:54:56 +08:00
|
|
|
import os
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from pathlib import Path
|
2025-06-06 21:02:08 +08:00
|
|
|
from typing import Optional
|
2025-05-30 19:54:56 +08:00
|
|
|
import threading
|
|
|
|
|
|
2025-06-06 21:02:08 +08:00
|
|
|
# Lock for thread-safe logger configuration
|
|
|
|
|
_lock = threading.Lock()
|
2025-05-30 19:54:56 +08:00
|
|
|
|
2025-06-06 21:07:52 +08:00
|
|
|
def get_logger(component_name: str = "default_logger", log_level: str = "INFO",
|
2025-05-30 19:54:56 +08:00
|
|
|
verbose: Optional[bool] = None, clean_old_logs: bool = True,
|
|
|
|
|
max_log_files: int = 30) -> logging.Logger:
|
|
|
|
|
"""
|
2025-06-06 21:02:08 +08:00
|
|
|
Get or create a logger for the specified component.
|
|
|
|
|
|
|
|
|
|
This function is thread-safe and ensures that handlers are not duplicated.
|
2025-05-30 19:54:56 +08:00
|
|
|
|
|
|
|
|
Args:
|
2025-06-06 21:07:52 +08:00
|
|
|
component_name: Name of the component (e.g., 'bot_manager', 'data_collector').
|
|
|
|
|
Defaults to 'default_logger' if not provided.
|
2025-05-30 19:54:56 +08:00
|
|
|
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
|
|
|
verbose: Enable console logging. If None, uses VERBOSE_LOGGING from .env
|
2025-06-06 21:02:08 +08:00
|
|
|
clean_old_logs: (Deprecated) This is now handled by max_log_files.
|
|
|
|
|
The parameter is kept for backward compatibility.
|
2025-05-30 19:54:56 +08:00
|
|
|
max_log_files: Maximum number of log files to keep (default: 30)
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-06-06 21:02:08 +08:00
|
|
|
Configured logger instance for the component
|
|
|
|
|
"""
|
|
|
|
|
with _lock:
|
|
|
|
|
logger_name = f"tcp_dashboard.{component_name}"
|
|
|
|
|
logger = logging.getLogger(logger_name)
|
2025-05-30 19:54:56 +08:00
|
|
|
|
2025-06-06 21:02:08 +08:00
|
|
|
# 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
|
2025-05-30 19:54:56 +08:00
|
|
|
|
2025-06-06 21:02:08 +08:00
|
|
|
# Create log directory for component
|
|
|
|
|
log_dir = Path("logs") / component_name
|
|
|
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
2025-05-30 19:54:56 +08:00
|
|
|
|
2025-06-06 21:02:08 +08:00
|
|
|
# Unified formatter
|
|
|
|
|
formatter = logging.Formatter(
|
2025-06-06 21:07:52 +08:00
|
|
|
'[%(asctime)s - %(levelname)s - %(pathname)s:%(lineno)d - %(funcName)s] - %(message)s',
|
2025-06-06 21:02:08 +08:00
|
|
|
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
|
2025-05-30 19:54:56 +08:00
|
|
|
|
2025-06-06 21:02:08 +08:00
|
|
|
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)
|
2025-05-30 19:54:56 +08:00
|
|
|
|
|
|
|
|
def cleanup_old_logs(component_name: str, days_to_keep: int = 30):
|
|
|
|
|
"""
|
|
|
|
|
Clean up old log files for a component based on age.
|
|
|
|
|
|
2025-06-06 21:02:08 +08:00
|
|
|
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.
|
|
|
|
|
|
2025-05-30 19:54:56 +08:00
|
|
|
Args:
|
|
|
|
|
component_name: Name of the component
|
2025-06-06 21:02:08 +08:00
|
|
|
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.
|
2025-05-30 19:54:56 +08:00
|
|
|
"""
|
2025-06-06 21:02:08 +08:00
|
|
|
logging.shutdown()
|