22FastAPI 메인 애플리케이션
33"""
44
5- from fastapi import FastAPI
5+ from fastapi import FastAPI , Request
66from fastapi .middleware .cors import CORSMiddleware
7+ from fastapi .responses import JSONResponse
8+ from fastapi .exceptions import RequestValidationError
9+ from starlette .exceptions import HTTPException as StarletteHTTPException
710from contextlib import asynccontextmanager
811import logging
912import sys
13+ import uuid
1014
11- from .routers import sentiment , vector , llm , restaurant , test
15+ from .routers import sentiment , vector , llm , test
16+ from ..cpu_monitor import get_cpu_monitor
1217
1318# 로거 설정 (콘솔 출력)
1419# basicConfig는 한 번만 실행되므로, root 로거에 직접 핸들러 추가
@@ -38,8 +43,17 @@ async def lifespan(app: FastAPI):
3843 """애플리케이션 생명주기 관리"""
3944 # 시작 시 초기화
4045 logger .info ("FastAPI 애플리케이션 시작" )
46+
47+ # CPU 모니터 시작 (Config.CPU_MONITOR_ENABLE=true일 때만)
48+ cpu_monitor = get_cpu_monitor ()
49+ if cpu_monitor :
50+ cpu_monitor .start ()
51+
4152 yield
53+
4254 # 종료 시 정리
55+ if cpu_monitor :
56+ await cpu_monitor .stop ()
4357 logger .info ("FastAPI 애플리케이션 종료" )
4458
4559
@@ -50,6 +64,65 @@ async def lifespan(app: FastAPI):
5064 lifespan = lifespan ,
5165)
5266
67+ # 요청 ID 미들웨어 (응답 헤더에도 포함)
68+ @app .middleware ("http" )
69+ async def add_request_id (request : Request , call_next ):
70+ request_id = request .headers .get ("X-Request-Id" ) or request .headers .get ("X-Request-ID" ) or str (uuid .uuid4 ())
71+ request .state .request_id = request_id
72+ response = await call_next (request )
73+ response .headers ["X-Request-Id" ] = request_id
74+ return response
75+
76+
77+ def _error_payload (* , code : int , message : str , details , request_id : str ) -> dict :
78+ return {"code" : code , "message" : message , "details" : details , "request_id" : request_id }
79+
80+
81+ @app .exception_handler (RequestValidationError )
82+ async def request_validation_exception_handler (request : Request , exc : RequestValidationError ):
83+ request_id = getattr (request .state , "request_id" , str (uuid .uuid4 ()))
84+ return JSONResponse (
85+ status_code = 422 ,
86+ content = _error_payload (
87+ code = 422 ,
88+ message = "Validation error" ,
89+ details = exc .errors (),
90+ request_id = request_id ,
91+ ),
92+ )
93+
94+
95+ @app .exception_handler (StarletteHTTPException )
96+ async def starlette_http_exception_handler (request : Request , exc : StarletteHTTPException ):
97+ request_id = getattr (request .state , "request_id" , str (uuid .uuid4 ()))
98+ # 404 등 라우팅 단계 HTTP 예외 포함
99+ detail = getattr (exc , "detail" , None )
100+ message = str (detail ) if detail is not None else "HTTP error"
101+ return JSONResponse (
102+ status_code = exc .status_code ,
103+ content = _error_payload (
104+ code = exc .status_code ,
105+ message = message ,
106+ details = detail ,
107+ request_id = request_id ,
108+ ),
109+ )
110+
111+
112+ @app .exception_handler (Exception )
113+ async def unhandled_exception_handler (request : Request , exc : Exception ):
114+ request_id = getattr (request .state , "request_id" , str (uuid .uuid4 ()))
115+ logger .error ("Unhandled exception: %s" , exc , exc_info = True )
116+ return JSONResponse (
117+ status_code = 500 ,
118+ content = _error_payload (
119+ code = 500 ,
120+ message = "Internal server error" ,
121+ details = None ,
122+ request_id = request_id ,
123+ ),
124+ )
125+
53126# CORS 설정
54127app .add_middleware (
55128 CORSMiddleware ,
@@ -63,7 +136,6 @@ async def lifespan(app: FastAPI):
63136app .include_router (sentiment .router , prefix = "/api/v1/sentiment" , tags = ["sentiment" ])
64137app .include_router (vector .router , prefix = "/api/v1/vector" , tags = ["vector" ])
65138app .include_router (llm .router , prefix = "/api/v1/llm" , tags = ["llm" ])
66- app .include_router (restaurant .router , prefix = "/api/v1/restaurants" , tags = ["restaurants" ])
67139app .include_router (test .router , prefix = "/api/v1/test" , tags = ["test" ])
68140
69141
0 commit comments