""" FastAPI 브릿지 메인 애플리케이션 Phase 1: 기본 프록시 기능 """ import asyncio import logging from typing import Any, Dict import aiohttp import jwt as pyjwt import uvicorn from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles from config import settings from cache import cache_manager, cached from analytics import analytics_manager, AnalyticsMiddleware # 로깅 설정 logging.basicConfig(level=getattr(logging, settings.LOG_LEVEL)) logger = logging.getLogger(__name__) # FastAPI 앱 생성 app = FastAPI( title=settings.PROJECT_NAME, version=settings.VERSION, description="Technical Korea FastAPI Bridge - Express.js API 프록시" ) # CORS 설정 app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 분석 미들웨어 추가 app.add_middleware(AnalyticsMiddleware) # 정적 파일 서빙 설정 app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/css", StaticFiles(directory="static/css"), name="css") app.mount("/js", StaticFiles(directory="static/js"), name="js") app.mount("/img", StaticFiles(directory="static/img"), name="img") app.mount("/pages", StaticFiles(directory="static/pages"), name="pages") app.mount("/components", StaticFiles(directory="static/components"), name="components") # HTTP 클라이언트 세션 (전역) http_session: aiohttp.ClientSession = None @app.on_event("startup") async def startup_event(): """앱 시작시 초기화""" global http_session # 캐시 매니저 연결 await cache_manager.connect() # HTTP 클라이언트 세션 생성 connector = aiohttp.TCPConnector( limit=settings.MAX_CONNECTIONS, ttl_dns_cache=300, use_dns_cache=True, ) timeout = aiohttp.ClientTimeout(total=settings.PROXY_TIMEOUT) http_session = aiohttp.ClientSession( connector=connector, timeout=timeout ) logger.info(f"🚀 {settings.PROJECT_NAME} v{settings.VERSION} 시작됨") logger.info(f"📍 포트: {settings.FASTAPI_PORT}") logger.info(f"🔗 Express.js API: {settings.EXPRESS_API_URL}") # 캐시 상태 로깅 cache_stats = cache_manager.get_stats() logger.info(f"💾 캐시: {cache_stats['cache_type']} ({'연결됨' if cache_stats['redis_available'] else '메모리 모드'})") @app.on_event("shutdown") async def shutdown_event(): """앱 종료시 정리""" global http_session # 캐시 매니저 연결 종료 await cache_manager.disconnect() if http_session: await http_session.close() logger.info("✅ FastAPI 브릿지가 정상적으로 종료되었습니다") @app.get("/") async def root(): """루트 경로 - index.html 서빙""" return FileResponse("static/index.html") @app.get("/status") async def status(): """서비스 상태 정보""" return { "service": settings.PROJECT_NAME, "version": settings.VERSION, "status": "healthy", "express_api": settings.EXPRESS_API_URL } @app.get("/health") @cached(prefix="health", ttl=settings.CACHE_HEALTH_TTL) async def health_check(): """헬스체크 엔드포인트 (캐싱 적용)""" try: # Express.js API 헬스체크 async with http_session.get(f"{settings.EXPRESS_API_URL}/api/health") as response: express_status = "healthy" if response.status == 200 else "unhealthy" express_data = await response.json() if response.status == 200 else None except Exception as e: logger.error(f"Express.js 헬스체크 실패: {e}") express_status = "unhealthy" express_data = None import datetime return { "fastapi_bridge": "healthy", "express_api": express_status, "express_data": express_data, "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "cached": True } @app.get("/cache/stats") async def cache_stats(): """캐시 통계 조회""" stats = cache_manager.get_stats() return { "cache_stats": stats, "settings": { "default_ttl": settings.CACHE_DEFAULT_TTL, "health_ttl": settings.CACHE_HEALTH_TTL, "auth_ttl": settings.CACHE_AUTH_TTL, "api_ttl": settings.CACHE_API_TTL, "static_ttl": settings.CACHE_STATIC_TTL } } @app.delete("/cache/clear") async def clear_cache(): """캐시 전체 삭제""" cleared_count = await cache_manager.clear_pattern("*") return { "message": "캐시가 삭제되었습니다", "cleared_keys": cleared_count } # ========================================== # Phase 4: 분석 및 모니터링 API # ========================================== @app.get("/analytics/summary") async def analytics_summary(): """분석 요약 통계""" return { "analytics": analytics_manager.get_summary_stats(), "cache": cache_manager.get_stats(), "prediction": analytics_manager.predict_load() } @app.get("/analytics/endpoints") async def analytics_endpoints(limit: int = 20): """엔드포인트별 성능 통계""" return { "top_endpoints": analytics_manager.get_top_endpoints(limit), "total_tracked": len(analytics_manager.endpoint_stats) } @app.get("/analytics/trends") async def analytics_trends(hours: int = 24): """시간대별 트렌드 분석""" return { "trends": analytics_manager.get_hourly_trends(hours), "period_hours": hours } @app.get("/analytics/clients") async def analytics_clients(): """클라이언트 분석""" return analytics_manager.get_client_analysis() @app.get("/analytics/dashboard") async def analytics_dashboard(): """종합 대시보드 데이터""" return { "summary": analytics_manager.get_summary_stats(), "top_endpoints": analytics_manager.get_top_endpoints(10), "trends_6h": analytics_manager.get_hourly_trends(6), "clients": analytics_manager.get_client_analysis(), "cache_stats": cache_manager.get_stats(), "prediction": analytics_manager.predict_load(), "server_info": { "version": settings.VERSION, "environment": settings.NODE_ENV, "redis_available": cache_manager.is_redis_available } } def _verify_proxy_token(request: Request) -> dict: """프록시 요청의 JWT 토큰을 검증하여 사용자 정보 반환""" auth_header = request.headers.get("authorization", "") if not auth_header.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing or invalid authorization") token = auth_header.split(" ", 1)[1] if not settings.JWT_SECRET: logger.warning("JWT_SECRET이 설정되지 않아 토큰 검증을 건너뜁니다") return {} try: payload = pyjwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"]) return payload except pyjwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token") @app.api_route("/api/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) async def proxy_to_express(path: str, request: Request) -> Dict[str, Any]: """Express.js API로 모든 요청을 프록시 (GET 요청은 캐싱 적용)""" # JWT 검증 (defense in depth — Express 백엔드도 자체 검증함) user_payload = _verify_proxy_token(request) user_id = user_payload.get("user_id", user_payload.get("id", "anon")) # Express.js API URL 구성 target_url = f"{settings.EXPRESS_API_URL}/api/{path}" # 요청 데이터 준비 headers = dict(request.headers) headers.pop("host", None) # host 헤더 제거 params = dict(request.query_params) # GET 요청에 대해서만 캐싱 적용 (user_id 포함하여 사용자 간 캐시 격리) if request.method == "GET": cache_key = cache_manager._generate_key("api", path, _uid=str(user_id), **params) cached_result = await cache_manager.get(cache_key) if cached_result is not None: logger.info(f"🟢 캐시 히트: GET {target_url}") return JSONResponse( content=cached_result, status_code=200 ) try: # 요청 바디 읽기 (있는 경우) body = None if request.method in ["POST", "PUT", "PATCH"]: body = await request.body() if body: body = body.decode("utf-8") if isinstance(body, bytes) else body logger.info(f"🔗 프록시: {request.method} {target_url}") # Express.js API로 요청 전달 async with http_session.request( method=request.method, url=target_url, headers=headers, params=params, data=body ) as response: # 응답 데이터 읽기 response_data = await response.text() # JSON으로 파싱 시도 try: response_json = await response.json() except: response_json = {"data": response_data} logger.info(f"✅ 응답: {response.status} ({len(response_data)} bytes)") # GET 요청 성공시 캐시에 저장 if request.method == "GET" and response.status == 200: await cache_manager.set(cache_key, response_json, settings.CACHE_API_TTL) logger.info(f"💾 캐시 저장: GET {target_url}") return JSONResponse( content=response_json, status_code=response.status, headers=dict(response.headers) ) except aiohttp.ClientTimeout: logger.error(f"⏰ 타임아웃: {target_url}") raise HTTPException(status_code=504, detail="Gateway Timeout") except aiohttp.ClientError as e: logger.error(f"🚫 클라이언트 오류: {e}") raise HTTPException(status_code=502, detail="Bad Gateway") except Exception as e: logger.error(f"💥 예상치 못한 오류: {e}") raise HTTPException(status_code=500, detail="Internal Server Error") if __name__ == "__main__": uvicorn.run( "main:app", host="0.0.0.0", port=settings.FASTAPI_PORT, reload=settings.NODE_ENV == "development", log_level=settings.LOG_LEVEL.lower() )