soulsync/utils/logging_config.py
Broque Thomas c1561b15ab Add dynamic log level control via Web UI
Introduces API endpoints and UI elements to view and change the application's log level from the web interface. Log level changes are applied immediately and persisted in the database. Updates backend logic, logging utilities, and frontend scripts to support this feature.
2025-12-08 12:53:44 -08:00

117 lines
No EOL
4 KiB
Python

import logging
import sys
import re
from pathlib import Path
from datetime import datetime
from typing import Optional
class SafeFormatter(logging.Formatter):
"""Formatter that handles Unicode characters safely on Windows"""
@staticmethod
def strip_emojis(text):
"""Remove emoji characters from text for Windows compatibility"""
# Remove emoji characters but keep other Unicode
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F" # emoticons
u"\U0001F300-\U0001F5FF" # symbols & pictographs
u"\U0001F680-\U0001F6FF" # transport & map symbols
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
u"\U00002702-\U000027B0"
u"\U000024C2-\U0001F251"
"]+", flags=re.UNICODE)
return emoji_pattern.sub('', text)
def format(self, record):
# Try to format with emojis first, fall back to stripped version
try:
return super().format(record)
except UnicodeEncodeError:
# Strip emojis and try again for Windows compatibility
record.getMessage = lambda: self.strip_emojis(record.msg % record.args if record.args else record.msg)
return super().format(record)
class ColoredFormatter(SafeFormatter):
COLORS = {
'DEBUG': '\033[94m',
'INFO': '\033[92m',
'WARNING': '\033[93m',
'ERROR': '\033[91m',
'CRITICAL': '\033[95m',
'RESET': '\033[0m'
}
def format(self, record):
log_color = self.COLORS.get(record.levelname, self.COLORS['RESET'])
reset_color = self.COLORS['RESET']
record.levelname = f"{log_color}{record.levelname}{reset_color}"
return super().format(record)
def setup_logging(level: str = "INFO", log_file: Optional[str] = None) -> logging.Logger:
log_level = getattr(logging, level.upper(), logging.INFO)
logger = logging.getLogger("newmusic")
logger.setLevel(log_level)
if logger.handlers:
logger.handlers.clear()
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(log_level)
# Force UTF-8 encoding for Windows compatibility with Unicode characters
if hasattr(console_handler.stream, 'reconfigure'):
console_handler.stream.reconfigure(encoding='utf-8', errors='replace')
console_formatter = ColoredFormatter(
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
if log_file:
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_path, encoding='utf-8')
file_handler.setLevel(log_level)
file_formatter = SafeFormatter(
fmt='%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
logger.info(f"Logging initialized with level: {level}")
return logger
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(f"newmusic.{name}")
def set_log_level(level: str) -> bool:
"""Dynamically change the log level for all loggers without restart"""
try:
log_level = getattr(logging, level.upper(), logging.INFO)
# Get the root "newmusic" logger
root_logger = logging.getLogger("newmusic")
root_logger.setLevel(log_level)
# Update all handlers
for handler in root_logger.handlers:
handler.setLevel(log_level)
root_logger.info(f"Log level changed to: {level.upper()}")
return True
except Exception as e:
print(f"Error setting log level: {e}")
return False
def get_current_log_level() -> str:
"""Get the current log level"""
root_logger = logging.getLogger("newmusic")
return logging.getLevelName(root_logger.level)
main_logger = get_logger("main")