Python Decorator를 이용한 API 요청 로깅

문제 상황

회사 레거시 API 서버(Flask 기반)에서 간헐적으로 500 에러가 발생했다. 에러 추적을 위해 요청 파라미터와 응답을 로깅해야 했는데, 각 엔드포인트마다 로깅 코드를 추가하기엔 중복이 너무 많았다.

Decorator를 이용한 해결

Python의 decorator를 활용하면 기존 함수를 수정하지 않고 로깅 기능을 추가할 수 있다.

import functools
import logging
import time
from flask import request, jsonify

logger = logging.getLogger(__name__)

def log_api_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        
        # 요청 정보 로깅
        logger.info(f"[REQUEST] {request.method} {request.path}")
        logger.info(f"[PARAMS] {request.args.to_dict()}")
        
        try:
            result = func(*args, **kwargs)
            elapsed = time.time() - start_time
            logger.info(f"[RESPONSE] {func.__name__} - {elapsed:.2f}s")
            return result
        except Exception as e:
            logger.error(f"[ERROR] {func.__name__}: {str(e)}")
            raise
    
    return wrapper

적용은 간단했다.

@app.route('/api/users/<user_id>')
@log_api_call
def get_user(user_id):
    user = User.query.get(user_id)
    return jsonify(user.to_dict())

개선 사항

실제로 운영해보니 몇 가지 문제가 있었다.

  1. POST 요청의 body 로깅 누락 → request.get_json() 추가
  2. 민감정보(password 등) 로깅 → 특정 필드 마스킹 처리
  3. 로그 파일 비대화 → logrotate 설정으로 해결

특히 민감정보 처리를 위해 decorator에 옵션을 추가했다.

def log_api_call(mask_fields=None):
    if mask_fields is None:
        mask_fields = ['password', 'token']
    
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 로깅 로직...
            pass
        return wrapper
    return decorator

결과

일주일간 로그를 수집한 결과, 특정 쿼리 파라미터 조합에서 DB 타임아웃이 발생하는 것을 확인했다. Decorator 패턴 덕분에 전체 API에 일관된 로깅을 빠르게 적용할 수 있었다.