handOver2/core/logger.py

366 lines
11 KiB
Python

# -*- 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