""" FastAPI용 로깅 모듈 Django 로거 구조를 참고하여 FastAPI에 최적화된 로깅 시스템. 사용 예시: from app.utils.logger import get_logger logger = get_logger("song") logger.info("노래 생성 완료") logger.error("오류 발생", exc_info=True) 로그 레벨: 1. DEBUG: 디버깅 목적 2. INFO: 일반 정보 3. WARNING: 경고 정보 (작은 문제) 4. ERROR: 오류 정보 (큰 문제) 5. CRITICAL: 아주 심각한 문제 """ import logging import sys from functools import lru_cache from logging.handlers import RotatingFileHandler from typing import Literal from app.utils.timezone import today_str from config import log_settings # 로그 디렉토리 설정 (config.py의 LogSettings에서 관리) LOG_DIR = log_settings.get_log_dir() # 로그 레벨 타입 LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] class LoggerConfig: """로거 설정 클래스 (config.py의 LogSettings 참조)""" # 출력 대상 설정 (LogSettings에서 가져옴) CONSOLE_ENABLED: bool = log_settings.LOG_CONSOLE_ENABLED FILE_ENABLED: bool = log_settings.LOG_FILE_ENABLED # 기본 설정 (LogSettings에서 가져옴) DEFAULT_LEVEL: str = log_settings.LOG_LEVEL CONSOLE_LEVEL: str = log_settings.LOG_CONSOLE_LEVEL FILE_LEVEL: str = log_settings.LOG_FILE_LEVEL MAX_BYTES: int = log_settings.LOG_MAX_SIZE_MB * 1024 * 1024 BACKUP_COUNT: int = log_settings.LOG_BACKUP_COUNT ENCODING: str = "utf-8" # 포맷 설정 (LogSettings에서 가져옴) CONSOLE_FORMAT: str = log_settings.LOG_CONSOLE_FORMAT FILE_FORMAT: str = log_settings.LOG_FILE_FORMAT DATE_FORMAT: str = log_settings.LOG_DATE_FORMAT def _create_console_handler() -> logging.StreamHandler: """콘솔 핸들러 생성""" handler = logging.StreamHandler(sys.stdout) handler.setLevel(getattr(logging, LoggerConfig.CONSOLE_LEVEL)) formatter = logging.Formatter( fmt=LoggerConfig.CONSOLE_FORMAT, datefmt=LoggerConfig.DATE_FORMAT, style="{", ) handler.setFormatter(formatter) return handler # ============================================================================= # 공유 파일 핸들러 (싱글톤) # 모든 로거가 동일한 파일 핸들러를 공유하여 하나의 로그 파일에 기록 # ============================================================================= _shared_file_handler: RotatingFileHandler | None = None _shared_error_handler: RotatingFileHandler | None = None def _get_shared_file_handler() -> RotatingFileHandler: """ 공유 파일 핸들러 반환 (싱글톤) 모든 로거가 하나의 기본 로그 파일(app.log)에 기록합니다. 파일명: logs/{날짜}_app.log """ global _shared_file_handler if _shared_file_handler is None: today = today_str() log_file = LOG_DIR / f"{today}_app.log" _shared_file_handler = RotatingFileHandler( filename=log_file, maxBytes=LoggerConfig.MAX_BYTES, backupCount=LoggerConfig.BACKUP_COUNT, encoding=LoggerConfig.ENCODING, ) _shared_file_handler.setLevel(getattr(logging, LoggerConfig.FILE_LEVEL)) formatter = logging.Formatter( fmt=LoggerConfig.FILE_FORMAT, datefmt=LoggerConfig.DATE_FORMAT, style="{", ) _shared_file_handler.setFormatter(formatter) return _shared_file_handler def _get_shared_error_handler() -> RotatingFileHandler: """ 공유 에러 파일 핸들러 반환 (싱글톤) 모든 로거의 ERROR 이상 로그가 하나의 에러 로그 파일(error.log)에 기록됩니다. 파일명: logs/{날짜}_error.log """ global _shared_error_handler if _shared_error_handler is None: today = today_str() log_file = LOG_DIR / f"{today}_error.log" _shared_error_handler = RotatingFileHandler( filename=log_file, maxBytes=LoggerConfig.MAX_BYTES, backupCount=LoggerConfig.BACKUP_COUNT, encoding=LoggerConfig.ENCODING, ) _shared_error_handler.setLevel(logging.ERROR) formatter = logging.Formatter( fmt=LoggerConfig.FILE_FORMAT, datefmt=LoggerConfig.DATE_FORMAT, style="{", ) _shared_error_handler.setFormatter(formatter) return _shared_error_handler @lru_cache(maxsize=32) def get_logger(name: str = "app") -> logging.Logger: """ 로거 인스턴스 반환 (캐싱 적용) Args: name: 로거 이름 (모듈명 권장: "song", "lyric", "video" 등) Returns: 설정된 로거 인스턴스 Example: logger = get_logger("song") logger.info("노래 처리 시작") """ logger = logging.getLogger(name) # 이미 핸들러가 설정된 경우 반환 if logger.handlers: return logger # 로그 레벨 설정 logger.setLevel(getattr(logging, LoggerConfig.DEFAULT_LEVEL)) # 핸들러 추가 (설정에 따라 선택적으로 추가) if LoggerConfig.CONSOLE_ENABLED: logger.addHandler(_create_console_handler()) if LoggerConfig.FILE_ENABLED: logger.addHandler(_get_shared_file_handler()) logger.addHandler(_get_shared_error_handler()) # 상위 로거로 전파 방지 (중복 출력 방지) logger.propagate = False return logger def setup_uvicorn_logging() -> dict: """ Uvicorn 서버의 로깅 설정을 반환합니다. ============================================================ 언제 사용하는가? ============================================================ Uvicorn 서버를 Python 코드로 직접 실행할 때 사용합니다. CLI 명령어(uvicorn main:app --reload)로 실행할 때는 적용되지 않습니다. ============================================================ 사용 방법 ============================================================ 1. Python 코드에서 uvicorn.run() 호출 시: # run.py 또는 main.py 하단 import uvicorn from app.utils.logger import setup_uvicorn_logging if __name__ == "__main__": uvicorn.run( "main:app", host="0.0.0.0", port=8000, reload=True, log_config=setup_uvicorn_logging(), # 여기서 적용 ) 2. 실행: python run.py 또는 python main.py ============================================================ 어떤 동작을 하는가? ============================================================ Uvicorn의 기본 로깅 형식을 애플리케이션의 LogSettings와 일치시킵니다. - formatters: 로그 출력 형식 정의 - default: 일반 로그용 (서버 시작/종료, 에러 등) - access: HTTP 요청 로그용 (클라이언트 IP, 요청 경로, 상태 코드) - handlers: 로그 출력 대상 설정 - stdout으로 콘솔에 출력 - loggers: Uvicorn 내부 로거 설정 - uvicorn: 메인 로거 - uvicorn.error: 에러/시작/종료 로그 - uvicorn.access: HTTP 요청 로그 ============================================================ 출력 예시 ============================================================ 적용 전 (Uvicorn 기본): INFO: 127.0.0.1:52341 - "GET /docs HTTP/1.1" 200 OK INFO: Uvicorn running on http://0.0.0.0:8000 적용 후: [2026-01-14 15:30:00] INFO [uvicorn.access] 127.0.0.1 - "GET /docs HTTP/1.1" 200 [2026-01-14 15:30:00] INFO [uvicorn:startup:45] Uvicorn running on http://0.0.0.0:8000 ============================================================ 반환값 구조 (Python logging.config.dictConfig 형식) ============================================================ { "version": 1, # dictConfig 버전 (항상 1) "disable_existing_loggers": False, # 기존 로거 유지 "formatters": { ... }, # 포맷터 정의 "handlers": { ... }, # 핸들러 정의 "loggers": { ... }, # 로거 정의 } Returns: dict: Uvicorn log_config 파라미터에 전달할 설정 딕셔너리 """ return { # -------------------------------------------------------- # dictConfig 버전 (필수, 항상 1) # -------------------------------------------------------- "version": 1, # -------------------------------------------------------- # 기존 로거 비활성화 여부 # False: 기존 로거 유지 (권장) # True: 기존 로거 모두 비활성화 # -------------------------------------------------------- "disable_existing_loggers": False, # -------------------------------------------------------- # 포맷터 정의 # 로그 메시지의 출력 형식을 지정합니다. # -------------------------------------------------------- "formatters": { # 일반 로그용 포맷터 (서버 시작/종료, 에러 등) "default": { "format": LoggerConfig.CONSOLE_FORMAT, "datefmt": LoggerConfig.DATE_FORMAT, "style": "{", # {변수명} 스타일 사용 }, # HTTP 요청 로그용 포맷터 # 사용 가능한 변수: client_addr, request_line, status_code "access": { "format": '[{asctime}] {levelname:8} [{name}] {client_addr} - "{request_line}" {status_code}', "datefmt": LoggerConfig.DATE_FORMAT, "style": "{", }, }, # -------------------------------------------------------- # 핸들러 정의 # 로그를 어디에 출력할지 지정합니다. # -------------------------------------------------------- "handlers": { # 일반 로그 핸들러 (stdout 출력) "default": { "formatter": "default", "class": "logging.StreamHandler", "stream": "ext://sys.stdout", }, # HTTP 요청 로그 핸들러 (stdout 출력) "access": { "formatter": "access", "class": "logging.StreamHandler", "stream": "ext://sys.stdout", }, }, # -------------------------------------------------------- # 로거 정의 # Uvicorn 내부에서 사용하는 로거들을 설정합니다. # -------------------------------------------------------- "loggers": { # Uvicorn 메인 로거 "uvicorn": { "handlers": ["default"], "level": "DEBUG", "propagate": False, # 상위 로거로 전파 방지 }, # 에러/시작/종료 로그 "uvicorn.error": { "handlers": ["default"], "level": "DEBUG", "propagate": False, }, # HTTP 요청 로그 (GET /path HTTP/1.1 200 등) "uvicorn.access": { "handlers": ["access"], "level": "DEBUG", "propagate": False, }, }, } # 편의를 위한 사전 정의된 로거 이름 상수 HOME_LOGGER = "home" LYRIC_LOGGER = "lyric" SONG_LOGGER = "song" VIDEO_LOGGER = "video" CELERY_LOGGER = "celery" APP_LOGGER = "app"