2025-10-29 13:32:42 +09:00
|
|
|
|
"""
|
|
|
|
|
|
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 활성화
|
|
|
|
|
|
|
2025-10-29 15:35:54 +09:00
|
|
|
|
# UTF-8 콘솔 출력 설정
|
|
|
|
|
|
try:
|
|
|
|
|
|
import ctypes
|
|
|
|
|
|
kernel32 = ctypes.windll.kernel32
|
|
|
|
|
|
kernel32.SetConsoleCP(65001) # UTF-8 input
|
|
|
|
|
|
kernel32.SetConsoleOutputCP(65001) # UTF-8 output
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2025-10-29 13:32:42 +09:00
|
|
|
|
# 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() # 기존 핸들러 제거
|
|
|
|
|
|
|
2025-10-29 15:35:54 +09:00
|
|
|
|
# 콘솔 핸들러 (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)
|
2025-10-29 13:32:42 +09:00
|
|
|
|
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
|