210 lines
5.8 KiB
Python
210 lines
5.8 KiB
Python
"""
|
||
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
|