316 lines
9.9 KiB
Python
316 lines
9.9 KiB
Python
#!/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
|