FastAPI Logging파일 일별로 관리하기

LTT·2025년 6월 23일

Introduce


로그는 언제나 중요하다. 로컬에서 작업할 때는 몰라도, 배포 환경에서 벌어진 문제에 대응하기 위해서는 로그를 남겨두는 것이 무조건 필요하다.

이제 로그파일을 남겨두기 위해 코드를 작성해보자

1차 작성


우선은 가장 간단한 형태로 작성했다.

log_util.py

import logging
import os
from datetime import date

today = date.today()
LOG_FILE_PATH = f"logs/{today}.log"

os.makedirs(os.path.dirname(LOG_FILE_PATH), exist_ok=True)

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE_PATH, mode="a", encoding="utf-8"),
        logging.StreamHandler(),
    ],
)

logger = logging.getLogger("app_logger")
logger.info("Logging setup complete.")

위와 같이 작성해줬고, 아래와 같이 로그가 쌓인다.

2025-06-20 15:52:25,939 - app_logger - INFO - AUTH BY: USER-01JPVYFQ8WMZ2H20X04K18W8NH

하지만 이렇게만 했을 경우에는 몇가지 문제가 있다.

  1. Request가 들어오고 Response가 나가는 것을 하나하나 다 수동으로 찍어줘야 한다.
  2. 실행하고 나서 서버를 재구동하지 않으면 날짜에 따라 로그 파일이 갱신되지 않는다.

우선 1번부터 해결해보자.

2차 작성


Request, Response 로깅


main.py

from fastapi import FastAPI, Response, Request
from starlette.background import BackgroundTask
from starlette.types import Message
from app.utils.log_util import logger

app = FastAPI()

# ...
# 추가 초기 설정
# ...

def end_log_info(res_body: bytes):
    logger.info(f"RESPONSE: {res_body.decode(errors='ignore')}")

def entry_log_info(endpoint: str):
    logger.info(f"ENDPOINT: {endpoint}")

async def set_body(request: Request, body: bytes):
    async def receive() -> Message:
        return {"type": "http.request", "body": body}

    request._receive = receive

@app.middleware("http")
async def logging_middleware(request: Request, call_next):
    req_body = await request.body()
    await set_body(request, req_body)
    endpoint = f"{request.method} {request.url.path}"

    entry_log_info(endpoint)

    response = await call_next(request)

    res_body = b""
    async for chunk in response.body_iterator:
        res_body += chunk

    task = BackgroundTask(end_log_info, res_body)

    return Response(
        content=res_body,
        status_code=response.status_code,
        headers=dict(response.headers),
        media_type=response.media_type,
        background=task,
    )
  • @app.middleware("http")

HTTP Request와 Response를 처리할 때 이 미들웨어 함수를 중간에 끼워 넣겠다는 의미

  • 클라이언트가 HTTP Request를 보냄
  • FastAPI에서는 @app.middleware("http")에 등록된 메소드를 우선 실행
  • 이 함수에서 call_next(request)를 호출해야 실제 라우터 핸들러로 넘어감
  • 응답이 돌아오면 다시 이 미들웨어에서 응답을 가공하거나 처리할 수 있음

그래서 이 미들웨어 메소드를 가지고 HTTP Request가 들어왔을 때 로깅 처리를 해주려고 한다.

이중에서 또 볼 부분은 BackgroundTask이다.

  • BackgroundTask

BackgroundTask

  • Response이후에 실행할 작업을 정의하는 유틸리티
  • 라우터 함수가 끝나더라도 비동기적으로 백그라운드에서 실행됨
  • 동시성(성능)과 응답속도 향상에 도움이 됨
  • 따라서 로깅으로 인해 Response time에 영향을 주지 않음

혹시모를 로깅으로 인한 지연을 대비해 BackgroundTask를 적용해보았다.

날짜별 로그파일 갱신


log_util.py

import logging
import os
from datetime import date
from logging.handlers import TimedRotatingFileHandler

LOG_DIR = "logs"
os.makedirs(LOG_DIR, exist_ok=True)

logger = logging.getLogger("app_logger")
logger.setLevel(logging.DEBUG)

# Setting formatter for log messages
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")

# File Handler
file_handler = TimedRotatingFileHandler(
    filename=os.path.join(LOG_DIR, f"{date.today()}.log"),
    when="midnight",
    interval=1,
    encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)

# Console Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(formatter)

# Reset handlers to avoid duplicate logs
if not logger.hasHandlers():
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)

logger.debug("This is a DEBUG message.")
logger.info("Logging setup complete.")

위와 같이 TimeRotatingFileHandler를 이용하여 일별로 로그 관리를 하려고 작성했다.

이렇게 해두면 배포했을 때, 일별로 로그파일이 생성되며 관리하기 용이해진다.

profile
개발자에서 엔지니어로, 엔지니어에서 리더로

0개의 댓글