- Updated the logging documentation to reflect changes in the unified log message format, including the addition of pathname, line number, and function name for better traceability. - Modified the `get_logger` function to set a default value for `component_name`, improving usability for users who may not specify a component name. - Ensured consistency in the documentation regarding the parameters and their descriptions. These updates improve the clarity and ease of use of the logging system, making it more accessible for developers.
154 lines
5.5 KiB
Python
154 lines
5.5 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 = "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() |