AutoPercenty3/loggerModule.py

397 lines
14 KiB
Python

"""
개선된 Logger 모듈 - GUI와 파일 로그 분리, 자동 정리 기능
GUI 로그 레벨 권장 설정:
- 일반 사용자: logging.INFO (정보성 메시지만)
- 개발자: logging.DEBUG (모든 메시지)
- 운영 환경: logging.WARNING (경고 이상만)
사용 예시:
```python
# 기본 사용
logger = Logger(
gui_logger=your_gui_function,
log_file="logs/app.log",
gui_log_level=logging.INFO # GUI에는 INFO 이상만
)
# 동적 레벨 변경
logger.set_gui_log_level(logging.WARNING) # GUI에는 경고만
logger.set_file_log_level(logging.DEBUG) # 파일에는 모든 로그
```
"""
import logging
from logging.handlers import TimedRotatingFileHandler
import os
import glob
import time
import threading
from datetime import datetime, timedelta
from pathlib import Path
from PySide6.QtCore import QObject, Signal
import traceback
# 로그 레벨 이름 매핑 (안전하고 호환성 좋은 방법)
def get_level_name(level):
"""로그 레벨을 이름으로 변환 (호환성 보장)"""
level_names = {
logging.NOTSET: "NOTSET",
logging.DEBUG: "DEBUG",
logging.INFO: "INFO",
logging.WARNING: "WARNING",
logging.ERROR: "ERROR",
logging.CRITICAL: "CRITICAL"
}
return level_names.get(level, f"Level {level}")
class Logger(QObject):
log_signal = Signal(str) # GUI로 로그 메시지를 전달할 시그널
def __init__(self, gui_logger=None, log_file="Edit_PartTimer_log.log", logger_name="Edit_PartTimer_log",
file_log_level=logging.DEBUG, gui_log_level=logging.INFO,
max_days=3, cleanup_interval=3600):
"""
개선된 Logger 초기화
:param gui_logger: GUI에 로그를 출력할 콜백 함수
:param log_file: 로그 파일 이름
:param logger_name: 로거 이름
:param file_log_level: 파일 로거의 로그 레벨
:param gui_log_level: GUI 로거의 로그 레벨
:param max_days: 로그 파일 보관 일수 (기본 3일)
:param cleanup_interval: 정리 작업 간격(초, 기본 1시간)
"""
super().__init__()
self.gui_logger = gui_logger
self.file_log_level = file_log_level
self.gui_log_level = gui_log_level
self.max_days = max_days
self.cleanup_interval = cleanup_interval
self.log_dir = Path(log_file).parent
self.log_base_name = Path(log_file).stem
# 로그 디렉토리 생성
self.log_dir.mkdir(parents=True, exist_ok=True)
# 로그 설정
self.logger = logging.getLogger(logger_name)
self.logger.setLevel(file_log_level)
# 기존 핸들러 제거 (중복 방지)
for handler in self.logger.handlers[:]:
self.logger.removeHandler(handler)
# 포맷 설정
self.simple_format = "[%(asctime)s] [%(levelname)s] %(message)s"
self.detailed_format = (
"[%(asctime)s] [%(threadName)s] [%(levelname)s] "
"[%(filename)s:%(funcName)s:%(lineno)d] %(message)s"
)
# GUI 전용 간결한 포맷 (시간:분:초 + 메시지만)
self.gui_format = "[%(asctime)s] %(message)s"
# 핸들러 추가
self._add_console_handler(file_log_level)
self._add_file_handler(log_file, file_log_level)
# GUI Logger 연결
if self.gui_logger:
self.log_signal.connect(self.gui_logger)
# 자동 정리 스레드 시작
self._start_cleanup_thread()
def _add_console_handler(self, level):
"""콘솔 핸들러 추가"""
console_handler = logging.StreamHandler()
console_handler.setLevel(level)
formatter = logging.Formatter(
self.detailed_format if level <= logging.DEBUG else self.simple_format
)
console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler)
def _add_file_handler(self, log_file, level):
"""개선된 시간 기반 파일 핸들러 추가"""
# 확장자가 .log가 아니면 .log로 변경
if not log_file.endswith('.log'):
base_name, _ = os.path.splitext(log_file)
log_file = base_name + '.log'
# 시간 기반 로테이팅 핸들러 사용 (매일 자정에 로테이션)
file_handler = TimedRotatingFileHandler(
log_file,
when='midnight', # 매일 자정에 로테이션
interval=1, # 1일 간격
backupCount=self.max_days, # 보관할 파일 수
encoding="utf-8",
utc=False
)
# 로테이션된 파일명 형식 설정 (YYYY-MM-DD)
file_handler.suffix = "%Y-%m-%d.log"
file_handler.extMatch = r"^\d{4}-\d{2}-\d{2}\.log$"
file_handler.setLevel(level)
formatter = logging.Formatter(
self.detailed_format if level <= logging.DEBUG else self.simple_format
)
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
# 핸들러 참조 저장 (정리 작업용)
self.file_handler = file_handler
def _start_cleanup_thread(self):
"""자동 정리 스레드 시작"""
def cleanup_worker():
while True:
try:
self._cleanup_old_logs()
time.sleep(self.cleanup_interval)
except Exception as e:
# 정리 작업 실패 시에도 계속 동작
pass
cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
cleanup_thread.start()
def _cleanup_old_logs(self):
"""오래된 로그 파일 정리"""
try:
cutoff_date = datetime.now() - timedelta(days=self.max_days)
log_pattern = f"{self.log_base_name}*.log"
for log_file in self.log_dir.glob(log_pattern):
try:
# 파일 수정 시간 확인
file_mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
# 현재 활성 로그 파일은 삭제하지 않음
if log_file.name == f"{self.log_base_name}.log":
continue
# 오래된 파일 삭제
if file_mtime < cutoff_date:
log_file.unlink()
self.logger.info(f"오래된 로그 파일 삭제: {log_file.name}")
except Exception as e:
self.logger.warning(f"로그 파일 정리 중 오류: {log_file.name} - {e}")
except Exception as e:
self.logger.error(f"로그 정리 작업 실패: {e}")
def log(self, message, level=logging.INFO, exc_info=False):
"""로그 메시지 기록"""
if exc_info:
message = f"{message}\n{traceback.format_exc()}"
# 호출 위치 정보를 동적으로 추출
caller_frame = logging.currentframe().f_back
record = self.logger.makeRecord(
self.logger.name, level, caller_frame.f_code.co_filename,
caller_frame.f_lineno, message, None, None, caller_frame.f_code.co_name
)
# 파일 로거에 메시지 전달
if level >= self.file_log_level:
self.logger.handle(record)
# GUI 로그로 전달 (포맷 적용)
if self.gui_logger and level >= self.gui_log_level:
# GUI용 간결한 포맷터 (시간:분:초만 표시)
gui_formatter = logging.Formatter(
fmt="[%(asctime)s] %(message)s",
datefmt="%H:%M:%S" # 시:분:초만 표시
)
formatted_message = gui_formatter.format(record)
colored_message = self.format_gui_message(formatted_message, level)
self.log_signal.emit(colored_message)
def format_gui_message(self, message, level):
"""GUI 로그 메시지의 HTML 색상 지정"""
color_map = {
logging.DEBUG: "#808080", # 회색
logging.INFO: "#000000", # 검정
logging.WARNING: "#FF8C00", # 주황
logging.ERROR: "#DC143C", # 빨강
logging.CRITICAL: "#8B008B", # 보라
}
color = color_map.get(level, "#000000")
return f'<span style="color:{color};">{message}</span>'
def set_gui_logger(self, gui_logger, gui_log_level=None):
"""
GUI 로그 콜백 함수를 설정하고, log_signal에 연결합니다.
선택적으로 GUI 로그 레벨도 변경할 수 있습니다.
"""
self.gui_logger = gui_logger
self.log_signal.connect(gui_logger)
if gui_log_level is not None:
self.gui_log_level = gui_log_level
def set_gui_log_level(self, level):
"""GUI 로그 레벨을 동적으로 변경합니다"""
self.gui_log_level = level
level_name = get_level_name(level)
self.logger.info(f"GUI 로그 레벨이 {level_name}로 변경되었습니다")
def set_file_log_level(self, level):
"""파일 로그 레벨을 동적으로 변경합니다"""
self.file_log_level = level
self.logger.setLevel(level)
for handler in self.logger.handlers:
if isinstance(handler, (logging.FileHandler, TimedRotatingFileHandler)):
handler.setLevel(level)
level_name = get_level_name(level)
self.logger.info(f"파일 로그 레벨이 {level_name}로 변경되었습니다")
def get_log_levels(self):
"""현재 로그 레벨 정보 반환"""
return {
"file_level": get_level_name(self.file_log_level),
"gui_level": get_level_name(self.gui_log_level),
"logger_level": get_level_name(self.logger.level)
}
def get_log_info(self):
"""로그 파일 정보 반환"""
try:
log_files = list(self.log_dir.glob(f"{self.log_base_name}*.log"))
total_size = sum(f.stat().st_size for f in log_files)
return {
"log_dir": str(self.log_dir),
"total_files": len(log_files),
"total_size_mb": total_size / (1024 * 1024),
"max_days": self.max_days,
"files": [f.name for f in log_files]
}
except Exception:
return {"error": "로그 정보 조회 실패"}
def force_cleanup(self):
"""수동 정리 실행"""
self._cleanup_old_logs()
# 파이썬 표준 로깅 인터페이스 지원
def debug(self, message, *args, **kwargs):
"""DEBUG 레벨 로그"""
if args:
message = message % args
self.log(message, level=logging.DEBUG, exc_info=kwargs.get('exc_info', False))
def info(self, message, *args, **kwargs):
"""INFO 레벨 로그"""
if args:
message = message % args
self.log(message, level=logging.INFO, exc_info=kwargs.get('exc_info', False))
def warning(self, message, *args, **kwargs):
"""WARNING 레벨 로그"""
if args:
message = message % args
self.log(message, level=logging.WARNING, exc_info=kwargs.get('exc_info', False))
def error(self, message, *args, **kwargs):
"""ERROR 레벨 로그"""
if args:
message = message % args
self.log(message, level=logging.ERROR, exc_info=kwargs.get('exc_info', False))
def critical(self, message, *args, **kwargs):
"""CRITICAL 레벨 로그"""
if args:
message = message % args
self.log(message, level=logging.CRITICAL, exc_info=kwargs.get('exc_info', False))
def exception(self, message, *args, **kwargs):
"""ERROR 레벨로 예외 정보와 함께 로그"""
if args:
message = message % args
self.log(message, level=logging.ERROR, exc_info=True)
# 전역 기본 로거 인스턴스
_default_logger = None
def get_default_logger():
"""기본 로거 인스턴스 반환"""
global _default_logger
if _default_logger is None:
_default_logger = Logger(log_file="logs/default.log")
return _default_logger
def set_default_logger(logger):
"""기본 로거 설정"""
global _default_logger
_default_logger = logger
# 편의 함수들 - 실제 구현
def debug(msg, *args, **kwargs):
"""편의 함수 - DEBUG 레벨 로그"""
get_default_logger().debug(msg, *args, **kwargs)
def info(msg, *args, **kwargs):
"""편의 함수 - INFO 레벨 로그"""
get_default_logger().info(msg, *args, **kwargs)
def warning(msg, *args, **kwargs):
"""편의 함수 - WARNING 레벨 로그"""
get_default_logger().warning(msg, *args, **kwargs)
def error(msg, *args, **kwargs):
"""편의 함수 - ERROR 레벨 로그"""
get_default_logger().error(msg, *args, **kwargs)
def critical(msg, *args, **kwargs):
"""편의 함수 - CRITICAL 레벨 로그"""
get_default_logger().critical(msg, *args, **kwargs)
def exception(msg, *args, **kwargs):
"""편의 함수 - 예외 정보와 함께 로그"""
get_default_logger().exception(msg, *args, **kwargs)
# 구조화된 로깅을 위한 추가 클래스
class StructuredLogger(Logger):
"""JSON 형태의 구조화된 로깅을 지원하는 로거"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def log_structured(self, event, level=logging.INFO, **context):
"""구조화된 로그 기록"""
import json
log_data = {
"timestamp": datetime.now().isoformat(),
"event": event,
"level": get_level_name(level),
**context
}
message = json.dumps(log_data, ensure_ascii=False, separators=(',', ':'))
self.log(message, level)
def log_performance(self, operation, duration, **context):
"""성능 로그 기록"""
self.log_structured(
"performance",
level=logging.INFO,
operation=operation,
duration_ms=round(duration * 1000, 2),
**context
)
def log_error_with_context(self, error, **context):
"""에러와 컨텍스트를 함께 로그"""
self.log_structured(
"error",
level=logging.ERROR,
error_type=type(error).__name__,
error_message=str(error),
**context
)