feat: integrate Recovery, CostGuard, ContainerCleaner in lifespan
- webapp.py lifespan now initializes json_logging, recovery, cost_guard, task_history - Dispatcher receives cost_guard and task_history dependencies - ContainerCleaner starts if Docker is available - Added /health/costs endpoint for API cost monitoring - Added tests/test_smoke.py with 2 tests for basic health and costs endpoint - All existing health tests still pass (8 tests)
This commit is contained in:
parent
c0cb4b7499
commit
b2c52abf06
@ -24,14 +24,43 @@ async def lifespan(app: FastAPI):
|
||||
from agent.message_store import get_message_store
|
||||
from agent.dispatcher import Dispatcher
|
||||
from agent.integrations.discord_handler import DiscordHandler
|
||||
from agent.json_logging import setup_logging
|
||||
from agent.recovery import recover_on_startup, ContainerCleaner
|
||||
from agent.cost_guard import get_cost_guard
|
||||
from agent.task_history import get_task_history
|
||||
|
||||
# 구조화 로깅 설정
|
||||
setup_logging(log_format=os.environ.get("LOG_FORMAT", "json"))
|
||||
|
||||
task_queue = await get_task_queue()
|
||||
message_store = await get_message_store()
|
||||
|
||||
dispatcher = Dispatcher(task_queue=task_queue)
|
||||
# 서버 시작 시 복구
|
||||
await recover_on_startup(task_queue)
|
||||
|
||||
# CostGuard + TaskHistory 초기화
|
||||
cost_guard = await get_cost_guard()
|
||||
task_history = await get_task_history()
|
||||
|
||||
# Dispatcher에 CostGuard + TaskHistory 주입
|
||||
dispatcher = Dispatcher(task_queue=task_queue, cost_guard=cost_guard, task_history=task_history)
|
||||
await dispatcher.start()
|
||||
app.state.dispatcher = dispatcher
|
||||
|
||||
# ContainerCleaner 시작
|
||||
container_cleaner = None
|
||||
try:
|
||||
import docker
|
||||
docker_client = docker.from_env()
|
||||
sandbox_timeout = int(os.environ.get("SANDBOX_TIMEOUT", "600"))
|
||||
container_cleaner = ContainerCleaner(
|
||||
docker_client=docker_client,
|
||||
max_age_seconds=sandbox_timeout * 2,
|
||||
)
|
||||
await container_cleaner.start()
|
||||
except Exception:
|
||||
logger.debug("Docker not available, container cleanup disabled")
|
||||
|
||||
discord_token = os.environ.get("DISCORD_TOKEN", "")
|
||||
discord_handler = None
|
||||
if discord_token:
|
||||
@ -43,8 +72,12 @@ async def lifespan(app: FastAPI):
|
||||
yield
|
||||
|
||||
await dispatcher.stop()
|
||||
if container_cleaner:
|
||||
await container_cleaner.stop()
|
||||
if discord_handler:
|
||||
await discord_handler.close()
|
||||
await cost_guard.close()
|
||||
await task_history.close()
|
||||
await task_queue.close()
|
||||
await message_store.close()
|
||||
logger.info("Application shutdown complete")
|
||||
@ -170,6 +203,14 @@ async def health_queue():
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health/costs")
|
||||
async def health_costs():
|
||||
"""API 비용 현황을 반환한다."""
|
||||
from agent.cost_guard import get_cost_guard
|
||||
guard = await get_cost_guard()
|
||||
return await guard.get_daily_summary()
|
||||
|
||||
|
||||
@app.post("/webhooks/gitea")
|
||||
@limiter.limit("10/minute")
|
||||
async def gitea_webhook(request: Request):
|
||||
|
||||
44
tests/test_smoke.py
Normal file
44
tests/test_smoke.py
Normal file
@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
from contextlib import asynccontextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def mock_lifespan(app):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smoke_health():
|
||||
from agent.webapp import app
|
||||
app.router.lifespan_context = mock_lifespan
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
|
||||
resp = await client.get("/health")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "ok"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smoke_health_costs():
|
||||
from agent.webapp import app
|
||||
app.router.lifespan_context = mock_lifespan
|
||||
|
||||
mock_guard = MagicMock()
|
||||
mock_guard.get_daily_summary = AsyncMock(return_value={
|
||||
"total_cost_usd": 1.5,
|
||||
"daily_limit_usd": 10.0,
|
||||
"remaining_usd": 8.5,
|
||||
"record_count": 3,
|
||||
"total_tokens_input": 50000,
|
||||
"total_tokens_output": 20000,
|
||||
})
|
||||
|
||||
with patch("agent.cost_guard.get_cost_guard", new_callable=AsyncMock, return_value=mock_guard):
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
|
||||
resp = await client.get("/health/costs")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total_cost_usd"] == 1.5
|
||||
assert data["daily_limit_usd"] == 10.0
|
||||
Loading…
x
Reference in New Issue
Block a user