AutoPercenty3/loggerModule.py

400 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 re
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=2, 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$"
# Python 3.11 호환성을 위해 정규식 패턴을 re.Pattern 객체로 변환
file_handler.extMatch = re.compile(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
)