"""Configurable logging system with XDG directory support.""" import logging import logging.handlers import yaml from pathlib import Path from typing import Optional from .config import get_config class LoggingConfig: """Logging configuration manager.""" def __init__(self): """Initialize logging configuration.""" self.config = get_config() self.log_dir = Path(self.config.get("logging.dir", "~/.local/share/crypto_trader/logs")).expanduser() self.log_dir.mkdir(parents=True, exist_ok=True) self.retention_days = self.config.get("logging.retention_days", 30) self._setup_logging() def _setup_logging(self): """Set up logging configuration.""" log_level = self.config.get("logging.level", "INFO") level = getattr(logging, log_level.upper(), logging.INFO) # Root logger root_logger = logging.getLogger() root_logger.setLevel(level) # Clear existing handlers root_logger.handlers.clear() # Console handler console_handler = logging.StreamHandler() console_handler.setLevel(level) console_formatter = logging.Formatter( '%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) console_handler.setFormatter(console_formatter) root_logger.addHandler(console_handler) # File handler with rotation log_file = self.log_dir / "crypto_trader.log" file_handler = logging.handlers.TimedRotatingFileHandler( log_file, when='midnight', interval=1, backupCount=self.retention_days, encoding='utf-8' ) file_handler.setLevel(logging.DEBUG) file_formatter = logging.Formatter( '%(asctime)s [%(levelname)s] %(name)s:%(lineno)d: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) file_handler.setFormatter(file_formatter) root_logger.addHandler(file_handler) # Compress old logs self._setup_log_compression() def _setup_log_compression(self): """Set up log compression for old log files.""" import gzip import glob # Compress logs older than retention period log_files = list(self.log_dir.glob("crypto_trader.log.*")) for log_file in log_files: if not log_file.name.endswith('.gz'): try: with open(log_file, 'rb') as f_in: with gzip.open(f"{log_file}.gz", 'wb') as f_out: f_out.writelines(f_in) log_file.unlink() except Exception: pass # Skip if compression fails def get_logger(self, name: str) -> logging.Logger: """Get a logger with the specified name. Args: name: Logger name (typically module name) Returns: Logger instance """ logger = logging.getLogger(name) return logger def set_level(self, level: str): """Set logging level. Args: level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) """ log_level = getattr(logging, level.upper(), logging.INFO) logging.getLogger().setLevel(log_level) for handler in logging.getLogger().handlers: handler.setLevel(log_level) # Global logging config instance _logging_config: Optional[LoggingConfig] = None def get_logger(name: str) -> logging.Logger: """Get a logger instance. Args: name: Logger name (typically __name__) Returns: Logger instance """ global _logging_config if _logging_config is None: _logging_config = LoggingConfig() return _logging_config.get_logger(name) def setup_logging(): """Set up logging system.""" global _logging_config _logging_config = LoggingConfig()