294 lines
9.5 KiB
Python
294 lines
9.5 KiB
Python
"""
|
|
FastAPI 브릿지 메인 애플리케이션
|
|
Phase 1: 기본 프록시 기능
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
from typing import Any, Dict
|
|
|
|
import aiohttp
|
|
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.utcnow().isoformat() + "Z",
|
|
"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
|
|
}
|
|
}
|
|
|
|
@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 요청은 캐싱 적용)"""
|
|
|
|
# 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 요청에 대해서만 캐싱 적용
|
|
if request.method == "GET":
|
|
cache_key = cache_manager._generate_key("api", path, **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()
|
|
) |