import asyncio import time from contextlib import asynccontextmanager from fastapi import Depends, FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from common.alerts import run_pool_monitor from common.database.db_session_manager import DB_SESSION_MNG from common.logger import LOG from common.utils.gtime import GTime from config.server_configs import web_server_config from router.v1.validator.auth import configured_keys, require_api_key import router.v1.lps.search API_SERVER_START_TIME = GTime.UTCStr() @asynccontextmanager async def lifespan(app: FastAPI): # startup: API 자신의 DB 풀 포화 감시(경량) — 대량 폴링으로 풀을 고갈시키는 주범이 API 일 수 있다. stop = asyncio.Event() pool_monitor = asyncio.create_task(run_pool_monitor(stop)) yield # shutdown: 모니터 정지 후 DB 엔진 커넥션 풀 정리 stop.set() pool_monitor.cancel() try: await pool_monitor except asyncio.CancelledError: pass await DB_SESSION_MNG.dispose_all() app = FastAPI(title="LPS Api Server", lifespan=lifespan) # CORS: config 의 cors_origins 가 있을 때만 적용(브라우저 프론트 호출 허용). # 명시적 오리진을 쓰므로 allow_credentials=True 가능(쿠키/Authorization 헤더 허용). if web_server_config.cors_origins: app.add_middleware( CORSMiddleware, allow_origins=web_server_config.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Accept-Encoding: gzip 요청에 대해 1000 bytes 이상 응답을 압축. app.add_middleware(GZipMiddleware, minimum_size=1000) @app.middleware("http") async def log_time(request: Request, call_next): start_time = time.time() response = await call_next(request) elapsed = time.time() - start_time LOG.d(f"took: {elapsed:.4f} - {request.url.path}") return response @app.get( path="/healthz", summary="헬스체크(liveness)", description="서버 기동 시각을 반환. 프로세스가 살아있는지만 확인(DB 무관).", responses={404: {"description": "Not found"}}, ) async def healthz(): return API_SERVER_START_TIME @app.get( path="/readyz", summary="레디니스(readiness)", description="DB 도달성까지 확인. 오케스트레이터/LB 가 트래픽 라우팅 여부 판단에 사용. 실패 시 503.", ) async def readyz(): from fastapi import Response from crud.job_crud import JobQueue try: await JobQueue().ping() return {"ready": True} except Exception as ex: return Response(content=f'{{"ready": false, "error": "{type(ex).__name__}"}}', media_type="application/json", status_code=503) # 각 도메인 라우터를 등록한다. 새 기능 추가 시 router.v1.. 를 import 후 include. # guard: [WebServerConfig].api_keys 설정 시 /v1 전체에 X-API-Key 검증(개발은 빈값=개방 — auth.py 참고). app.include_router(router.v1.lps.search.router, dependencies=[Depends(require_api_key)]) if configured_keys(): LOG.i(f"API guard ON — X-API-Key 검증({len(configured_keys())}개 키)") else: LOG.w("[WebServerConfig].api_keys 비어있음 — API 개방 모드(개발용). prod 는 toml 에 키 채움 + 포트 비공개 필수")