o2o-negosium-original/lps/router/router.py
민헌 c81df5bd88 refactor(lps): 설정을 TOML 단일 소스로 통합 — env/.env 이중 관리 제거
설정이 .env(compose 주입)·config.toml·코드 곳곳의 os.environ 직독 3계층에
흩어져 관리가 어려웠다. TOML 하나로 통합한다(협의 결정).

- 신설 [WorkerConfig](동시성·폴백·프로필·데드라인·유예·Chrome·하트비트),
  [AlertConfig](웹훅·쿨다운·임계 10종). [WebServerConfig].api_keys(guard),
  [DecodoConfig].ip_request_budget/port_cooldown_sec 추가 — 흩어져 있던
  LPS_* env 20여 개를 섹션으로 흡수.
- server_configs 의 env override 계층(DB_*·시크릿·NAVER_KEYS 등) 삭제.
  남는 env 는 APP_ENV(부트스트랩)·PROCESS_COUNT/WORKER_CONCURRENCY(실행
  스크립트 대화형 입력 전용)·LPS_LIVE(테스트 옵트인)뿐.
- Docker: env 주입 → config.docker.toml 마운트 + APP_ENV=docker.
  이미지 무시크릿 유지, 마운트 누락 시 FileNotFoundError 즉시 실패.
  .env.example 삭제, config.docker.toml.example 신설.
- negodata 호출부: guard 키를 env 직독에서 [WebServerConfig].lps_api_key
  (+기존 관례대로 env override)로 이동.
- 실행 스크립트: 프로필·폴백·예산 프롬프트 제거(toml 소스 안내),
  동시성/프로세스 수만 임시 override 로 유지.
- docs 7종·example toml 의 env 표기를 toml 키로 일괄 갱신.
- 전체 145 passed + APP_ENV=docker 로딩·API 기동 스모크 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:11:31 +09:00

96 lines
3.3 KiB
Python

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.<domain>.<file> 를 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 에 키 채움 + 포트 비공개 필수")