Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
12 changes: 12 additions & 0 deletions apps/pre-processing-service/app/api/endpoints/embedding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# app/api/endpoints/embedding.py
from fastapi import APIRouter
from app.decorators.logging import log_api_call
from ...errors.CustomException import *
from fastapi import APIRouter

# 이 파일만의 독립적인 라우터를 생성합니다.
router = APIRouter()

@router.get("/")
async def root():
return {"message": "Items API"}
12 changes: 12 additions & 0 deletions apps/pre-processing-service/app/api/endpoints/processing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# app/api/endpoints/embedding.py
from fastapi import APIRouter
from app.decorators.logging import log_api_call
from ...errors.CustomException import *
from fastapi import APIRouter

# 이 파일만의 독립적인 라우터를 생성합니다.
router = APIRouter()

@router.get("/")
async def root():
return {"message": "사용자API"}
35 changes: 35 additions & 0 deletions apps/pre-processing-service/app/api/endpoints/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# app/api/endpoints/embedding.py
from fastapi import APIRouter
from app.decorators.logging import log_api_call
from ...errors.CustomException import *
from fastapi import APIRouter

# 이 파일만의 독립적인 라우터를 생성합니다.
router = APIRouter()

@router.get("/")
async def root():
return {"message": "테스트 API"}


@router.get("/hello/{name}" , tags=["hello"])
# @log_api_call
async def say_hello(name: str):
return {"message": f"Hello {name}"}


# 특정 경로에서 의도적으로 에러 발생
#커스텀에러 테스터 url
@router.get("/error/{item_id}")
async def trigger_error(item_id: int):
if item_id == 0:
raise InvalidItemDataException()

if item_id == 404:
raise ItemNotFoundException(item_id=item_id)

if item_id == 500:
raise ValueError("이것은 테스트용 값 오류입니다.")


return {"result": item_id}
29 changes: 29 additions & 0 deletions apps/pre-processing-service/app/api/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# app/api/router.py
from fastapi import APIRouter
from .endpoints import embedding, processing,test
from ..core.config import settings

api_router = APIRouter()

# embedding API URL
api_router.include_router(embedding.router, prefix="/emb", tags=["Embedding"])

# processing API URL
api_router.include_router(processing.router, prefix="/prc", tags=["Processing"])

#모듈 테스터를 위한 endpoint
api_router.include_router(test.router, prefix="/test", tags=["Test"])

@api_router.get("/")
async def root():
return {"message": "서버 실행중입니다."}

@api_router.get("/db")
def get_settings():
"""
환경 변수가 올바르게 로드되었는지 확인하는 엔드포인트
"""
return {
"환경": settings.env_name,
"데이터베이스 URL": settings.db_url
}
48 changes: 48 additions & 0 deletions apps/pre-processing-service/app/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from pydantic_settings import BaseSettings
import os
from typing import Optional


# 공통 설정을 위한 BaseSettings
class BaseSettingsConfig(BaseSettings):

# db_url 대신 개별 필드를 정의합니다.
db_host: str
db_port: str
db_user: str
db_pass: str
db_name: str
env_name: str = "dev"

@property
def db_url(self) -> str:
"""개별 필드를 사용하여 DB URL을 동적으로 생성"""
return f"postgresql://{self.db_user}:{self.db_pass}@{self.db_host}:{self.db_port}/{self.db_name}"

class Config:
env_file = ['.env']


# 환경별 설정 클래스
class DevSettings(BaseSettingsConfig):
class Config:
env_file = ['.env', 'dev.env']


class PrdSettings(BaseSettingsConfig):
class Config:
env_file = ['.env', 'prd.env']


def get_settings() -> BaseSettingsConfig:
"""환경 변수에 따라 적절한 설정 객체를 반환하는 함수"""
mode = os.getenv("MODE", "dev")
if mode == "dev":
return DevSettings()
elif mode == "prd":
return PrdSettings()
else:
raise ValueError(f"Invalid MODE environment variable: {mode}")


settings = get_settings()
53 changes: 0 additions & 53 deletions apps/pre-processing-service/app/core/decorators.py

This file was deleted.

1 change: 1 addition & 0 deletions apps/pre-processing-service/app/db/db_connecter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ..core.config import settings
Empty file.
88 changes: 88 additions & 0 deletions apps/pre-processing-service/app/decorators/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# app/decorators/logging.py

from fastapi import Request
from loguru import logger
import functools
import time


def log_api_call(func):
"""
FastAPI API 호출에 대한 상세 정보를 로깅하는 데코레이터입니다.
IP 주소, User-Agent, URL, 메서드, 실행 시간 등을 기록합니다.
"""

@functools.wraps(func)
async def wrapper(*args, **kwargs):
# 1. request 객체를 안전하게 가져옵니다.
# kwargs에서 'request'를 찾고, 없으면 args가 비어있지 않은 경우에만 args[0]을 시도합니다.
request: Request | None = kwargs.get('request')
if request is None and args and isinstance(args[0], Request):
request = args[0]

# 2. 로깅에 사용할 추가 정보를 추출합니다.
client_ip: str | None = None
user_agent: str | None = None
if request:
client_ip = request.client.host
user_agent = request.headers.get("user-agent", "N/A")

# 3. 요청 정보를 로그로 기록합니다.
log_context = {
"func": func.__name__,
"ip": client_ip,
"user_agent": user_agent
}
if request:
log_context.update({
"url": str(request.url),
"method": request.method,
})
logger.info(
"API 호출 시작: URL='{url}' 메서드='{method}' 함수='{func}' IP='{ip}' User-Agent='{user_agent}'",
**log_context
)
else:
logger.info("API 호출 시작: 함수='{func}'", **log_context)

start_time = time.time()
result = None

try:
# 4. 원본 함수를 실행합니다.
result = await func(*args, **kwargs)
return result
except Exception as e:
# 5. 예외 발생 시 에러 로그를 기록합니다.
elapsed_time = time.time() - start_time
log_context["exception"] = e
log_context["elapsed"] = f"{elapsed_time:.4f}s"

if request:
logger.error(
"API 호출 실패: URL='{url}' 메서드='{method}' IP='{ip}' 예외='{exception}' ({elapsed})",
**log_context
)
else:
logger.error(
"API 호출 실패: 함수='{func}' 예외='{exception}' ({elapsed})",
**log_context
)
raise # 예외를 다시 발생시켜 FastAPI가 처리하도록 합니다.
finally:
# 6. 성공적으로 완료되면 성공 로그를 기록합니다.
if result is not None:
elapsed_time = time.time() - start_time
log_context["elapsed"] = f"{elapsed_time:.4f}s"
if request:
logger.success(
"API 호출 성공: URL='{url}' 메서드='{method}' IP='{ip}' ({elapsed})",
**log_context
)
else:
logger.success(
"API 호출 성공: 함수='{func}' ({elapsed})",
**log_context
)

return wrapper
26 changes: 26 additions & 0 deletions apps/pre-processing-service/app/errors/CustomException.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# app/errors/CustomException.py
class CustomException(Exception):
"""
개발자가 비지니스 로직에 맞게 의도적으로 에러를 정의
"""
def __init__(self, status_code: int, detail: str, code: str):
self.status_code = status_code
self.detail = detail
self.code = code

# 구체적인 커스텀 예외 정의
class ItemNotFoundException(CustomException):
def __init__(self, item_id: int):
super().__init__(
status_code=404,
detail=f"{item_id}를 찾을수 없습니다.",
code="ITEM_NOT_FOUND"
)

class InvalidItemDataException(CustomException):
def __init__(self):
super().__init__(
status_code=422,
detail="데이터가 유효하지않습니다..",
code="INVALID_ITEM_DATA"
)
Empty file.
65 changes: 65 additions & 0 deletions apps/pre-processing-service/app/errors/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# app/errors/handlers.py
from fastapi import Request, status
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.exceptions import RequestValidationError
from .messages import ERROR_MESSAGES, get_error_message
from ..errors.CustomException import CustomException

# CustomException 핸들러
async def custom_exception_handler(request: Request, exc: CustomException):
"""
CustomException을 상속받는 모든 예외를 처리합니다.
"""
return JSONResponse(
status_code=exc.status_code,
content={
"error_code": exc.code,
"message": exc.detail,
},
)

# FastAPI의 HTTPException 핸들러 (예: 404 Not Found)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
"""
FastAPI에서 기본적으로 발생하는 HTTP 관련 예외를 처리합니다.
"""
if exc.status_code == status.HTTP_404_NOT_FOUND:
# 404 에러의 경우, FastAPI의 기본 "Not Found" 메시지 대신 우리가 정의한 메시지를 사용합니다.
message = ERROR_MESSAGES.get(exc.status_code, "요청하신 리소스를 찾을 수 없습니다.")
else:
# 다른 HTTP 예외들은 FastAPI가 제공하는 detail 메시지를 우선적으로 사용합니다.
message = get_error_message(exc.status_code, exc.detail)

return JSONResponse(
status_code=exc.status_code,
content={
"error_code": f"HTTP_{exc.status_code}",
"message": message
},
)

# Pydantic Validation Error 핸들러 (422)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
Pydantic 모델 유효성 검사 실패 시 발생하는 예외를 처리합니다.
"""
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"error_code": "VALIDATION_ERROR",
"message": ERROR_MESSAGES[status.HTTP_422_UNPROCESSABLE_ENTITY],
"details": exc.errors(),
},
)

# 처리되지 않은 모든 예외 핸들러 (500)
async def unhandled_exception_handler(request: Request, exc: Exception):
# ...
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error_code": "INTERNAL_SERVER_ERROR",
"message": ERROR_MESSAGES[status.HTTP_500_INTERNAL_SERVER_ERROR],
},
)
Loading