Files
dl-organizer/src/vlm/logging_config.py
T

130 lines
4.1 KiB
Python
Raw Normal View History

2026-02-09 17:43:35 +08:00
"""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"
2026-02-09 17:43:35 +08:00
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()
2026-02-09 17:43:35 +08:00
# 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}"
)
2026-02-09 17:43:35 +08:00
# 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)