#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 로깅 유틸리티 모듈 네이버 크롤러에서 사용하는 공통 로깅 기능 """ import os import sys import json import logging from datetime import datetime from typing import Optional, Dict, Any from pathlib import Path class Config: """설정 파일 관리 클래스""" _instance = None _config = None def __new__(cls): if cls._instance is None: cls._instance = super(Config, cls).__new__(cls) return cls._instance def __init__(self): if self._config is None: self.load_config() def load_config(self, config_path: str = None): """설정 파일 로드""" if config_path is None: # 현재 스크립트 위치에서 config.json 찾기 current_dir = Path(__file__).parent config_path = current_dir / 'config.json' try: with open(config_path, 'r', encoding='utf-8') as f: self._config = json.load(f) print(f"설정 파일 로드 완료: {config_path}") except FileNotFoundError: print(f"설정 파일을 찾을 수 없습니다: {config_path}") print("기본 설정을 사용합니다.") self._config = self._get_default_config() except json.JSONDecodeError as e: print(f"설정 파일 파싱 오류: {e}") print("기본 설정을 사용합니다.") self._config = self._get_default_config() def _get_default_config(self) -> Dict[str, Any]: """기본 설정 반환""" return { "crawler": { "max_blogs": 5, "default_search_query": "선돌막국수" }, "image_filter": { "min_width": 400, "min_height": 400, "min_file_size_kb": 10, "max_file_size_mb": 10, "require_both_dimensions": False, "allowed_formats": [".jpg", ".jpeg", ".png", ".webp"] }, "paths": { "project_dir_linux": "/data/crawling" }, "webdriver": { "headless": False, "window_size": "1920,1080", "page_load_timeout": 30, "implicit_wait": 10 }, "logging": { "console_level": "INFO", "file_level": "DEBUG", "log_format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" } } def get(self, key_path: str, default=None): """ 점 표기법으로 중첩된 설정값 가져오기 예: config.get('crawler.max_blogs') """ keys = key_path.split('.') value = self._config for key in keys: if isinstance(value, dict) and key in value: value = value[key] else: return default return value def get_all(self) -> Dict[str, Any]: """전체 설정 반환""" return self._config.copy() def save_config(self, config_path: str = None): """설정 파일 저장""" if config_path is None: current_dir = Path(__file__).parent config_path = current_dir / 'config.json' try: with open(config_path, 'w', encoding='utf-8') as f: json.dump(self._config, f, ensure_ascii=False, indent=2) print(f"설정 파일 저장 완료: {config_path}") except Exception as e: print(f"설정 파일 저장 실패: {e}") def update(self, key_path: str, value: Any): """설정값 업데이트""" keys = key_path.split('.') config = self._config for key in keys[:-1]: if key not in config: config[key] = {} config = config[key] config[keys[-1]] = value # 전역 설정 인스턴스 config = Config() def setup_logger( name: str, log_dir: str, log_prefix: str = "log", console_level: int = None, file_level: int = None ) -> logging.Logger: """ 로거 설정 함수 Args: name: 로거 이름 log_dir: 로그 파일 저장 디렉토리 log_prefix: 로그 파일명 접두사 console_level: 콘솔 출력 로그 레벨 (None이면 설정에서 읽음) file_level: 파일 출력 로그 레벨 (None이면 설정에서 읽음) Returns: 설정된 로거 객체 """ # 설정에서 로그 레벨 가져오기 if console_level is None: console_level_str = config.get('logging.console_level', 'INFO') console_level = getattr(logging, console_level_str) if file_level is None: file_level_str = config.get('logging.file_level', 'DEBUG') file_level = getattr(logging, file_level_str) # 로그 디렉토리 생성 os.makedirs(log_dir, exist_ok=True) # 로그 포맷 설정 log_format = config.get('logging.log_format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s') # 로그 파일 경로 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_file = os.path.join(log_dir, f'{log_prefix}_{timestamp}.log') # 로거 생성 logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) # 기존 핸들러 제거 (중복 방지) logger.handlers.clear() # 파일 핸들러 file_handler = logging.FileHandler(log_file, encoding='utf-8') file_handler.setLevel(file_level) file_handler.setFormatter(logging.Formatter(log_format)) # 콘솔 핸들러 console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(console_level) console_handler.setFormatter(logging.Formatter(log_format)) # 핸들러 추가 logger.addHandler(file_handler) logger.addHandler(console_handler) logger.info(f"로거 '{name}' 초기화 완료") logger.info(f"로그 파일: {log_file}") return logger def get_logger(name: str) -> logging.Logger: return logging.getLogger(name) class LoggerMixin: def setup_logger( self, logger_name: Optional[str] = None, log_dir: Optional[str] = None, log_prefix: Optional[str] = None ): """ 클래스용 로거 설정 Args: logger_name: 로거 이름 (기본값: 클래스명) log_dir: 로그 디렉토리 (기본값: self.project_dir/logs) log_prefix: 로그 파일 접두사 (기본값: 클래스명) """ if logger_name is None: logger_name = self.__class__.__name__ if log_dir is None: log_dir = os.path.join(getattr(self, 'project_dir', '.'), 'logs') if log_prefix is None: log_prefix = self.__class__.__name__.lower() self.logger = setup_logger( name=logger_name, log_dir=log_dir, log_prefix=log_prefix ) def log_debug(self, message: str): """디버그 로그""" if hasattr(self, 'logger'): self.logger.debug(message) def log_info(self, message: str): """정보 로그""" if hasattr(self, 'logger'): self.logger.info(message) def log_warning(self, message: str): """경고 로그""" if hasattr(self, 'logger'): self.logger.warning(message) def log_error(self, message: str, exc_info: bool = False): """에러 로그""" if hasattr(self, 'logger'): self.logger.error(message, exc_info=exc_info) def log_critical(self, message: str): """치명적 에러 로그""" if hasattr(self, 'logger'): self.logger.critical(message) def log_execution_time(logger: logging.Logger = None): """ 함수 실행 시간을 로깅하는 데코레이터 Args: logger: 사용할 로거 (없으면 함수명으로 새로 생성) """ import functools import time def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): nonlocal logger if logger is None: logger = logging.getLogger(func.__name__) start_time = time.time() logger.debug(f"{func.__name__} 시작") try: result = func(*args, **kwargs) elapsed = time.time() - start_time logger.info(f"{func.__name__} 완료 (실행시간: {elapsed:.2f}초)") return result except Exception as e: elapsed = time.time() - start_time logger.error(f"{func.__name__} 실패 (실행시간: {elapsed:.2f}초): {str(e)}") raise return wrapper return decorator def log_exception(logger: logging.Logger = None): """ 예외를 로깅하는 데코레이터 Args: logger: 사용할 로거 """ import functools def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): nonlocal logger if logger is None: logger = logging.getLogger(func.__name__) try: return func(*args, **kwargs) except Exception as e: logger.error(f"{func.__name__}에서 예외 발생: {str(e)}", exc_info=True) raise return wrapper return decorator