""" Colored Console Logger for DS_L10N 컬러 콘솔 로깅 시스템 """ import logging import sys from datetime import datetime from pathlib import Path from typing import Optional # Windows에서 ANSI 컬러 지원 활성화 if sys.platform == 'win32': import os os.system('') # ANSI escape sequences 활성화 # UTF-8 콘솔 출력 설정 try: import ctypes kernel32 = ctypes.windll.kernel32 kernel32.SetConsoleCP(65001) # UTF-8 input kernel32.SetConsoleOutputCP(65001) # UTF-8 output except: pass # ANSI 컬러 코드 class Colors: RESET = '\033[0m' BOLD = '\033[1m' # 기본 색상 BLACK = '\033[30m' RED = '\033[31m' GREEN = '\033[32m' YELLOW = '\033[33m' BLUE = '\033[34m' MAGENTA = '\033[35m' CYAN = '\033[36m' WHITE = '\033[37m' # 밝은 색상 BRIGHT_RED = '\033[91m' BRIGHT_GREEN = '\033[92m' BRIGHT_YELLOW = '\033[93m' BRIGHT_BLUE = '\033[94m' BRIGHT_MAGENTA = '\033[95m' BRIGHT_CYAN = '\033[96m' BRIGHT_WHITE = '\033[97m' # 이모지 아이콘 class Icons: SUCCESS = '✅' ERROR = '❌' WARNING = '⚠️' INFO = 'ℹ️' ROCKET = '🚀' GEAR = '⚙️' FILE = '📄' FOLDER = '📁' CHART = '📊' SEARCH = '🔍' CLEAN = '🧹' BACKUP = '💾' CLOCK = '⏱️' class ColoredFormatter(logging.Formatter): """컬러 로그 포매터""" LEVEL_COLORS = { 'DEBUG': Colors.CYAN, 'INFO': Colors.BLUE, 'WARNING': Colors.YELLOW, 'ERROR': Colors.RED, 'CRITICAL': Colors.BRIGHT_RED + Colors.BOLD, 'SUCCESS': Colors.GREEN, } LEVEL_ICONS = { 'DEBUG': Icons.GEAR, 'INFO': Icons.INFO, 'WARNING': Icons.WARNING, 'ERROR': Icons.ERROR, 'CRITICAL': Icons.ERROR, 'SUCCESS': Icons.SUCCESS, } def __init__(self, colored=True): super().__init__() self.colored = colored def format(self, record): if self.colored: level_color = self.LEVEL_COLORS.get(record.levelname, '') level_icon = self.LEVEL_ICONS.get(record.levelname, '') reset = Colors.RESET # 타임스탬프 timestamp = datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S') # 레벨명 포맷 level_name = f"{level_color}{record.levelname:<8}{reset}" # 메시지 message = record.getMessage() return f"[{timestamp}] {level_icon} {level_name} {message}" else: timestamp = datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S') return f"[{timestamp}] [{record.levelname:<8}] {record.getMessage()}" class DSLogger: """DS_L10N 전용 로거""" def __init__(self, name: str = 'DS_L10N', console_level: str = 'INFO', file_level: str = 'DEBUG', log_file: Optional[Path] = None, colored: bool = True): self.logger = logging.getLogger(name) self.logger.setLevel(logging.DEBUG) self.logger.handlers.clear() # 기존 핸들러 제거 # 콘솔 핸들러 (UTF-8 강제) if sys.platform == 'win32': # Windows에서 UTF-8 출력 스트림 사용 import io console_stream = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True) else: console_stream = sys.stdout console_handler = logging.StreamHandler(console_stream) console_handler.setLevel(getattr(logging, console_level.upper())) console_handler.setFormatter(ColoredFormatter(colored=colored)) self.logger.addHandler(console_handler) # 파일 핸들러 if log_file: log_file.parent.mkdir(parents=True, exist_ok=True) file_handler = logging.FileHandler(log_file, encoding='utf-8') file_handler.setLevel(getattr(logging, file_level.upper())) file_handler.setFormatter(ColoredFormatter(colored=False)) self.logger.addHandler(file_handler) # SUCCESS 레벨 추가 (INFO와 WARNING 사이) logging.addLevelName(25, 'SUCCESS') def debug(self, message: str): self.logger.debug(message) def info(self, message: str): self.logger.info(message) def success(self, message: str): self.logger.log(25, message) def warning(self, message: str): self.logger.warning(message) def error(self, message: str): self.logger.error(message) def critical(self, message: str): self.logger.critical(message) def separator(self, char: str = '=', length: int = 80): """구분선 출력""" self.logger.info(char * length) def section(self, title: str, icon: str = Icons.ROCKET): """섹션 제목 출력""" self.separator() self.logger.info(f"{icon} {title}") self.separator() def stats(self, **kwargs): """통계 정보 출력""" self.logger.info(f"{Icons.CHART} 통계:") for key, value in kwargs.items(): self.logger.info(f" - {key}: {value}") # 전역 로거 인스턴스 _global_logger: Optional[DSLogger] = None def get_logger() -> DSLogger: """전역 로거 가져오기""" global _global_logger if _global_logger is None: _global_logger = DSLogger() return _global_logger def init_logger(config: dict, log_file: Optional[Path] = None) -> DSLogger: """로거 초기화""" global _global_logger logging_config = config.get('logging', {}) _global_logger = DSLogger( console_level=logging_config.get('console_level', 'INFO'), file_level=logging_config.get('file_level', 'DEBUG'), log_file=log_file, colored=logging_config.get('colored_output', True) ) return _global_logger