# -*- coding: utf-8 -*- """ 로깅 시스템 모듈 애플리케이션의 로깅 기능을 설정하고 관리합니다. 이 모듈은 다음 기능을 제공합니다: - 파일 및 콘솔 로깅 - 일별 로그 로테이션 - 로그 레벨 필터링 - 상세한 로그 포맷팅 """ import os import sys import logging from datetime import datetime from pathlib import Path from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler from typing import Optional from .constants import LOGS_DIR, LOG_RETENTION_DAYS, APP_NAME # ============================================================================ # 로그 포맷 정의 # ============================================================================ # 콘솔 로그 포맷 (간략) CONSOLE_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s" # 파일 로그 포맷 (상세) FILE_FORMAT = ( "%(asctime)s | %(levelname)-8s | %(name)s | " "%(filename)s:%(lineno)d | %(funcName)s | %(message)s" ) # 날짜 포맷 DATE_FORMAT = "%Y-%m-%d %H:%M:%S" # ============================================================================ # 커스텀 로그 필터 # ============================================================================ class LevelFilter(logging.Filter): """ 특정 레벨 이상의 로그만 통과시키는 필터 Args: level: 최소 로그 레벨 """ def __init__(self, level: int): super().__init__() self.level = level def filter(self, record: logging.LogRecord) -> bool: return record.levelno >= self.level class ModuleFilter(logging.Filter): """ 특정 모듈의 로그만 통과시키는 필터 Args: modules: 허용할 모듈 이름 리스트 """ def __init__(self, modules: list): super().__init__() self.modules = modules def filter(self, record: logging.LogRecord) -> bool: return any(record.name.startswith(module) for module in self.modules) # ============================================================================ # 커스텀 핸들러 # ============================================================================ class ColoredConsoleHandler(logging.StreamHandler): """ 컬러 콘솔 출력 핸들러 로그 레벨에 따라 다른 색상으로 출력합니다. Windows 콘솔에서도 ANSI 색상 코드를 지원합니다. """ # ANSI 색상 코드 COLORS = { 'DEBUG': '\033[36m', # Cyan 'INFO': '\033[32m', # Green 'WARNING': '\033[33m', # Yellow 'ERROR': '\033[31m', # Red 'CRITICAL': '\033[35m', # Magenta 'RESET': '\033[0m', # Reset } def __init__(self, stream=None): super().__init__(stream) # Windows에서 ANSI 색상 코드 활성화 if sys.platform == 'win32': try: import ctypes kernel32 = ctypes.windll.kernel32 kernel32.SetConsoleMode( kernel32.GetStdHandle(-11), 7 ) except Exception: pass def emit(self, record: logging.LogRecord): try: # 색상 코드 추가 color = self.COLORS.get(record.levelname, self.COLORS['RESET']) reset = self.COLORS['RESET'] # 원본 메시지 백업 original_msg = record.msg # 색상 적용 record.msg = f"{color}{record.msg}{reset}" record.levelname = f"{color}{record.levelname}{reset}" super().emit(record) # 원본 메시지 복원 record.msg = original_msg except Exception: self.handleError(record) # ============================================================================ # 로거 설정 함수 # ============================================================================ def setup_logger( name: str = APP_NAME, level: int = logging.DEBUG, log_to_file: bool = True, log_to_console: bool = True, log_dir: Optional[Path] = None ) -> logging.Logger: """ 로거를 설정하고 반환합니다. 이 함수는 애플리케이션 시작 시 한 번 호출되어야 합니다. 로그는 콘솔과 파일에 동시에 기록됩니다. Args: name: 로거 이름 (기본값: 앱 이름) level: 로그 레벨 (기본값: DEBUG) log_to_file: 파일 로깅 활성화 여부 log_to_console: 콘솔 로깅 활성화 여부 log_dir: 로그 디렉토리 경로 (기본값: LOGS_DIR) Returns: 설정된 Logger 객체 Examples: >>> logger = setup_logger() >>> logger.info("애플리케이션이 시작되었습니다.") """ # 로거 생성 logger = logging.getLogger(name) logger.setLevel(level) # 기존 핸들러 제거 (중복 방지) logger.handlers.clear() # 콘솔 핸들러 설정 if log_to_console: console_handler = ColoredConsoleHandler(sys.stdout) console_handler.setLevel(logging.INFO) console_formatter = logging.Formatter(CONSOLE_FORMAT, DATE_FORMAT) console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) # 파일 핸들러 설정 if log_to_file: # 로그 디렉토리 생성 log_directory = log_dir or LOGS_DIR log_directory.mkdir(parents=True, exist_ok=True) # 로그 파일 경로 log_filename = log_directory / f"app_{datetime.now().strftime('%Y%m%d')}.log" # 일별 로테이션 핸들러 file_handler = TimedRotatingFileHandler( filename=log_filename, when='midnight', interval=1, backupCount=LOG_RETENTION_DAYS, encoding='utf-8' ) file_handler.setLevel(logging.DEBUG) file_formatter = logging.Formatter(FILE_FORMAT, DATE_FORMAT) file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) # 에러 전용 파일 핸들러 error_filename = log_directory / f"error_{datetime.now().strftime('%Y%m%d')}.log" error_handler = TimedRotatingFileHandler( filename=error_filename, when='midnight', interval=1, backupCount=LOG_RETENTION_DAYS, encoding='utf-8' ) error_handler.setLevel(logging.ERROR) error_handler.setFormatter(file_formatter) logger.addHandler(error_handler) # 로거가 루트 로거로 전파되지 않도록 설정 logger.propagate = False return logger def get_logger(name: str = None) -> logging.Logger: """ 지정된 이름의 로거를 반환합니다. 모듈별로 별도의 로거를 사용할 때 호출합니다. Args: name: 로거 이름 (기본값: 호출 모듈의 __name__) Returns: Logger 객체 Examples: >>> logger = get_logger(__name__) >>> logger.debug("디버그 메시지") """ if name is None: # 호출자의 모듈 이름 가져오기 import inspect frame = inspect.currentframe() if frame and frame.f_back: name = frame.f_back.f_globals.get('__name__', APP_NAME) else: name = APP_NAME return logging.getLogger(name) def set_log_level(level: int, logger_name: str = None): """ 로거의 로그 레벨을 변경합니다. Args: level: 새로운 로그 레벨 logger_name: 로거 이름 (기본값: 루트 로거) Examples: >>> set_log_level(logging.WARNING) """ logger = logging.getLogger(logger_name) if logger_name else logging.getLogger() logger.setLevel(level) for handler in logger.handlers: handler.setLevel(level) def cleanup_old_logs(log_dir: Optional[Path] = None, days: int = LOG_RETENTION_DAYS): """ 오래된 로그 파일을 삭제합니다. Args: log_dir: 로그 디렉토리 경로 days: 보관 기간 (일) """ import time log_directory = log_dir or LOGS_DIR if not log_directory.exists(): return now = time.time() cutoff = now - (days * 86400) # days to seconds for log_file in log_directory.glob("*.log*"): if log_file.stat().st_mtime < cutoff: try: log_file.unlink() except Exception as e: print(f"로그 파일 삭제 실패: {log_file} - {e}") # ============================================================================ # 로그 유틸리티 함수 # ============================================================================ def log_function_call(logger: logging.Logger): """ 함수 호출을 로깅하는 데코레이터 Args: logger: 사용할 로거 Returns: 데코레이터 함수 Examples: >>> @log_function_call(logger) ... def my_function(x, y): ... return x + y """ def decorator(func): def wrapper(*args, **kwargs): logger.debug(f"호출: {func.__name__}(args={args}, kwargs={kwargs})") try: result = func(*args, **kwargs) logger.debug(f"완료: {func.__name__} -> {result}") return result except Exception as e: logger.error(f"예외 발생: {func.__name__} -> {e}") raise return wrapper return decorator def log_exception(logger: logging.Logger, exc: Exception, extra_info: str = None): """ 예외를 상세하게 로깅합니다. Args: logger: 사용할 로거 exc: 예외 객체 extra_info: 추가 정보 """ import traceback error_message = f"예외 발생: {type(exc).__name__}: {exc}" if extra_info: error_message = f"{extra_info} - {error_message}" logger.error(error_message) logger.debug(f"스택 트레이스:\n{traceback.format_exc()}") # ============================================================================ # 모듈 레벨 기본 로거 # ============================================================================ # 기본 로거 (모듈 로드 시 설정되지 않음) _default_logger: Optional[logging.Logger] = None def get_default_logger() -> logging.Logger: """ 기본 로거를 반환합니다. 로거가 설정되지 않은 경우 기본 설정으로 초기화합니다. """ global _default_logger if _default_logger is None: _default_logger = setup_logger() return _default_logger