Replace custom get_logger() wrapper with standard Python logging pattern across executor.py and quarantine.py. Remove dead logger imports from reports.py. Remove get_logger() function from logging_config.py and its tests. Reduces logging patterns from 2 to 1. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
130 lines
4.1 KiB
Python
130 lines
4.1 KiB
Python
"""Logging configuration for Video Library Manager.
|
|
|
|
This module provides centralized logging configuration with:
|
|
- Configurable log levels (DEBUG, INFO, WARNING, ERROR)
|
|
- Dual output: console (INFO+) and file (DEBUG+)
|
|
- Timestamps, operation type, and file paths in log entries
|
|
- Log rotation at 10MB threshold
|
|
"""
|
|
|
|
import logging
|
|
import logging.handlers
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
DEFAULT_LOG_FILE = "vlm.log"
|
|
MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB in bytes
|
|
BACKUP_COUNT = 5 # Keep 5 backup log files
|
|
|
|
|
|
def default_log_dir() -> Path:
|
|
"""Return the default log directory resolved at runtime."""
|
|
return Path.home() / ".vlm" / "logs"
|
|
|
|
|
|
class OperationContextFilter(logging.Filter):
|
|
"""Filter to add operation context to log records."""
|
|
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
"""Add operation_type and file_path attributes if not present."""
|
|
if not hasattr(record, 'operation_type'):
|
|
record.operation_type = 'general'
|
|
if not hasattr(record, 'file_path'):
|
|
record.file_path = ''
|
|
return True
|
|
|
|
|
|
def setup_logging(
|
|
log_level: str = "INFO",
|
|
log_dir: Optional[Path] = None,
|
|
log_file: str = DEFAULT_LOG_FILE
|
|
) -> logging.Logger:
|
|
"""Configure logging with dual output (console and file) and rotation.
|
|
|
|
Args:
|
|
log_level: Minimum log level for console output (DEBUG, INFO, WARNING, ERROR)
|
|
log_dir: Directory for log files (defaults to ~/.vlm/logs)
|
|
log_file: Name of the log file (defaults to vlm.log)
|
|
|
|
Returns:
|
|
Configured logger instance
|
|
|
|
Raises:
|
|
ValueError: If log_level is invalid
|
|
"""
|
|
# Validate log level
|
|
numeric_level = getattr(logging, log_level.upper(), None)
|
|
if not isinstance(numeric_level, int):
|
|
raise ValueError(f"Invalid log level: {log_level}")
|
|
|
|
# Use default log directory if not specified
|
|
if log_dir is None:
|
|
log_dir = default_log_dir()
|
|
|
|
# Get root logger
|
|
logger = logging.getLogger("vlm")
|
|
logger.setLevel(logging.DEBUG) # Capture all levels, handlers will filter
|
|
|
|
# Remove existing handlers to avoid duplicates
|
|
logger.handlers.clear()
|
|
|
|
# Create formatter with timestamps, operation type, and file paths
|
|
formatter = logging.Formatter(
|
|
fmt='%(asctime)s - %(levelname)s - [%(operation_type)s] - %(message)s%(file_path)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
|
|
# Console handler (INFO+)
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setLevel(numeric_level)
|
|
console_handler.setFormatter(formatter)
|
|
console_handler.addFilter(OperationContextFilter())
|
|
logger.addHandler(console_handler)
|
|
|
|
# File handler with rotation (DEBUG+)
|
|
try:
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
log_file_path = log_dir / log_file
|
|
file_handler = logging.handlers.RotatingFileHandler(
|
|
filename=log_file_path,
|
|
maxBytes=MAX_LOG_SIZE,
|
|
backupCount=BACKUP_COUNT,
|
|
encoding='utf-8'
|
|
)
|
|
file_handler.setLevel(logging.DEBUG)
|
|
file_handler.setFormatter(formatter)
|
|
file_handler.addFilter(OperationContextFilter())
|
|
logger.addHandler(file_handler)
|
|
except (OSError, PermissionError) as exc:
|
|
logger.warning(
|
|
f"File logging disabled (cannot write to {log_dir}): {exc}"
|
|
)
|
|
|
|
# Prevent propagation to root logger
|
|
logger.propagate = False
|
|
|
|
return logger
|
|
|
|
|
|
def log_operation(
|
|
logger: logging.Logger,
|
|
level: int,
|
|
message: str,
|
|
operation_type: str = "general",
|
|
file_path: Optional[Path] = None
|
|
) -> None:
|
|
"""Log a message with operation context.
|
|
|
|
Args:
|
|
logger: Logger instance
|
|
level: Log level (logging.DEBUG, logging.INFO, etc.)
|
|
message: Log message
|
|
operation_type: Type of operation (scan, parse, execute, etc.)
|
|
file_path: Optional file path related to the operation
|
|
"""
|
|
extra = {
|
|
'operation_type': operation_type,
|
|
'file_path': f' - {file_path}' if file_path else ''
|
|
}
|
|
logger.log(level, message, extra=extra)
|