Files

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()
)