"""
YTSage logging configuration using loguru.
This module provides centralized logging configuration for the entire YTSage application.
It replaces the inefficient print statements with structured logging using loguru.
"""
import os
import sys
from pathlib import Path
# Try to import loguru, but handle case where it might not be available
try:
from loguru import logger
LOGURU_AVAILABLE = True
except ImportError:
LOGURU_AVAILABLE = False
# Create a dummy logger class that does nothing
class DummyLogger:
def info(self, *args, **kwargs): pass
def debug(self, *args, **kwargs): pass
def warning(self, *args, **kwargs): pass
def error(self, *args, **kwargs): pass
def critical(self, *args, **kwargs): pass
def remove(self, *args, **kwargs): pass
def add(self, *args, **kwargs): pass
def bind(self, *args, **kwargs): return self
@property
def _core(self):
class Core:
handlers = []
return Core()
logger = DummyLogger()
def setup_logging():
"""
Configure loguru logging for YTSage application.
Sets up multiple log levels and outputs:
- Console output for INFO and above
- File output for DEBUG and above
- Separate error log file for ERROR and above
"""
if not LOGURU_AVAILABLE:
return logger
# Remove default logger to avoid duplicate output
try:
logger.remove()
except Exception:
pass
# Get the application data directory with fallbacks
try:
if sys.platform == 'win32':
localappdata = os.environ.get('LOCALAPPDATA')
if localappdata:
log_dir = Path(localappdata) / 'YTSage' / 'logs'
else:
# Fallback for PyInstaller or when LOCALAPPDATA is not set
log_dir = Path.home() / 'AppData' / 'Local' / 'YTSage' / 'logs'
elif sys.platform == 'darwin':
log_dir = Path.home() / 'Library' / 'Application Support' / 'YTSage' / 'logs'
else:
log_dir = Path.home() / '.local' / 'share' / 'YTSage' / 'logs'
except Exception:
# Ultimate fallback - use current directory
log_dir = Path.cwd() / 'logs'
# Create log directory if it doesn't exist
try:
log_dir.mkdir(parents=True, exist_ok=True)
except Exception:
# If we can't create the log directory, fall back to current directory
log_dir = Path.cwd()
try:
log_dir.mkdir(exist_ok=True)
except Exception:
pass # If we still can't create it, we'll just log to console
# Console handler - INFO and above, with colors
# Check if stdout is available (it might be None in PyInstaller windowed apps)
stdout_available = sys.stdout is not None
if stdout_available:
try:
logger.add(
sys.stdout,
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
colorize=True,
catch=True
)
except Exception:
# Fallback to basic console logging without colors
try:
logger.add(
sys.stdout,
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
catch=True
)
except Exception:
stdout_available = False
# If stdout is not available, try stderr or skip console logging entirely
if not stdout_available:
try:
if sys.stderr is not None:
logger.add(
sys.stderr,
level="WARNING", # Only warnings and errors to stderr
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
catch=True
)
except Exception:
# If even stderr fails, we'll rely only on file logging
pass
# Only add file handlers if we successfully created a log directory
if log_dir and log_dir.exists():
try:
# Main log file - DEBUG and above, with rotation
logger.add(
log_dir / "ytsage.log",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
rotation="10 MB", # Rotate when file reaches 10MB
retention="7 days", # Keep logs for 7 days
compression="zip", # Compress old logs
catch=True
)
# Error log file - ERROR and above only
logger.add(
log_dir / "ytsage_errors.log",
level="ERROR",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
rotation="5 MB",
retention="30 days", # Keep error logs longer
compression="zip",
catch=True
)
except Exception as e:
# If file logging fails, just log to console
logger.warning(f"Could not set up file logging: {e}")
# Log startup message if we have any handlers
if logger._core.handlers:
logger.info("YTSage logging system initialized")
if log_dir and log_dir.exists():
logger.debug(f"Log directory: {log_dir}")
else:
logger.warning("File logging disabled - could not create log directory")
# If no handlers were successfully added, add a null handler to prevent errors
if not logger._core.handlers:
# Add a minimal handler that just discards messages
# This prevents loguru from complaining about no handlers
import tempfile
try:
# Try to add a temporary file handler as last resort
temp_log = Path(tempfile.gettempdir()) / "ytsage_temp.log"
logger.add(temp_log, level="ERROR", catch=True)
except Exception:
# If even that fails, we're in a very restricted environment
# loguru should handle this gracefully with its internal fallbacks
pass
return logger
def get_logger(name: str = None):
"""
Get a logger instance for a specific module.
Args:
name: Name of the module/component requesting the logger
Returns:
Configured logger instance
"""
if name:
return logger.bind(name=name)
return logger
# Initialize logging when module is imported - with maximum safety
_setup_complete = False
def safe_setup():
"""Safely initialize logging with multiple fallback strategies."""
global _setup_complete
if _setup_complete:
return logger
try:
setup_logging()
_setup_complete = True
except Exception:
# If all else fails, create an even simpler logger that just prints
if LOGURU_AVAILABLE:
try:
logger.remove()
except Exception:
pass
# At this point, just ensure we have something that won't crash
_setup_complete = True
return logger
# Try to set up logging, but don't let it crash the module import
try:
safe_setup()
except Exception:
# Ultimate fallback - the module will still import successfully
pass
# Export the main logger for convenience
__all__ = ['logger', 'get_logger', 'setup_logging']