366 lines
11 KiB
Python
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
|
|
|
|
|