RA.Aid/ra_aid/logging_config.py

170 lines
6.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
import os
import sys
from datetime import datetime
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Optional
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
class PrettyHandler(logging.Handler):
def __init__(self, level=logging.NOTSET):
super().__init__(level)
self.console = Console()
def emit(self, record):
try:
msg = self.format(record)
# Determine title and style based on log level
if record.levelno >= logging.CRITICAL:
title = "🔥 CRITICAL"
style = "bold red"
elif record.levelno >= logging.ERROR:
title = "❌ ERROR"
style = "red"
elif record.levelno >= logging.WARNING:
title = "⚠️ WARNING"
style = "yellow"
elif record.levelno >= logging.INFO:
title = " INFO"
style = "green"
else:
title = "🐞 DEBUG"
style = "blue"
self.console.print(Panel(Markdown(msg.strip()), title=title, style=style))
except Exception:
self.handleError(record)
def setup_logging(log_mode: str = "file", pretty: bool = False, log_level: Optional[str] = None) -> None:
"""
Configure logging for ra_aid.
Args:
log_mode: Determines where logs are output. Options:
- "file": Log to both file and console, with console showing only warnings+
- "console": Log to console at the specified log_level (no file logging)
pretty: Set to True to enable pretty console logging.
log_level: Optional explicit log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
When log_mode="file": Only affects file logging level, console shows warnings+
When log_mode="console": Controls the console logging level
Console logging behavior:
- With log_mode="file": Only WARNING and higher level messages are shown in console
- With log_mode="console": Console shows messages at the requested log_level
File logging behavior:
- Only active when log_mode="file"
- Uses the requested log_level
- When log_level=debug is used with log_mode="file", debug logs only go to the file, not to the console
"""
# Create .ra-aid/logs directory if it doesn't exist
cwd = os.getcwd()
ra_aid_dir_str = os.path.join(cwd, ".ra-aid")
logs_dir_str = os.path.join(ra_aid_dir_str, "logs")
# Create directory structure if log_mode is "file"
if log_mode == "file":
for directory in [ra_aid_dir_str, logs_dir_str]:
path = Path(directory)
if not path.exists():
try:
path.mkdir(mode=0o755, parents=True, exist_ok=True)
except Exception as e:
print(f"Warning: Failed to create log directory {directory}: {str(e)}")
# Determine log level
if log_level is not None:
# Use provided log level if specified (case-insensitive)
specified_log_level = getattr(logging, log_level.upper(), None)
if not isinstance(specified_log_level, int):
# If invalid log level is provided, fall back to default
print(f"Invalid log level: {log_level}")
specified_log_level = logging.WARNING
else:
# No log_level specified, use WARNING as default
specified_log_level = logging.WARNING
# Determine console log level based on log_mode
if log_mode == "console":
# When log_mode="console", use the specified log level for console
console_log_level = specified_log_level
else:
# When log_mode="file", console only shows warnings and errors
console_log_level = logging.WARNING
# Configure the root logger
root_logger = logging.getLogger()
# Always set the root logger to DEBUG level
# This ensures all messages flow through to their respective handlers
# Best practice is to set root logger to lowest level and let handlers control message filtering
root_logger.setLevel(logging.DEBUG)
# Clear existing handlers from root logger to avoid duplicates
if root_logger.handlers:
root_logger.handlers.clear()
# Create console handler
if pretty:
console_handler = PrettyHandler()
else:
console_handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
console_handler.setFormatter(formatter)
# Set console handler log level based on log_mode and log_level
console_handler.setLevel(console_log_level)
# Add console handler to root logger
root_logger.addHandler(console_handler)
# Create file handler with rotation - only when log_mode is "file"
if log_mode == "file":
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_filename = os.path.join(logs_dir_str, f"ra_aid_{timestamp}.log")
# RotatingFileHandler with 5MB max size and 100 backup files
file_handler = RotatingFileHandler(
log_filename,
maxBytes=5 * 1024 * 1024, # 5MB
backupCount=100,
encoding="utf-8"
)
file_formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(file_formatter)
# File handler always uses the specified log level
file_handler.setLevel(specified_log_level)
# Add file handler to root logger
root_logger.addHandler(file_handler)
# Create an ra_aid logger for compatibility
logger = logging.getLogger("ra_aid")
logger.setLevel(logging.DEBUG)
logger.propagate = True # Let messages propagate to root handlers
# Log configuration details for debugging
logger.debug(f"Logging configuration: log_mode={log_mode}, log_level={log_level}, "
f"root_level={root_logger.level}, logger_level={logger.level}, "
f"console_level={console_handler.level}, file_level={file_handler.level}, "
f"propagate={logger.propagate}")
logger.info(f"Log file created: {log_filename}")
except Exception as e:
root_logger.error(f"Failed to set up file logging: {str(e)}")
def get_logger(name: Optional[str] = None) -> logging.Logger:
return logging.getLogger(f"ra_aid.{name}" if name else "ra_aid")