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