feat(agent-office): fetch_service_logs 추가 (path_prefix 정규식 필터)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
import httpx
|
import httpx
|
||||||
|
import logging
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from .config import STOCK_URL, MUSIC_LAB_URL, INSTA_LAB_URL, REALESTATE_LAB_URL
|
from .config import STOCK_URL, MUSIC_LAB_URL, INSTA_LAB_URL, REALESTATE_LAB_URL
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_client = httpx.AsyncClient(timeout=30.0)
|
_client = httpx.AsyncClient(timeout=30.0)
|
||||||
|
|
||||||
async def fetch_stock_news(limit: int = 10, category: str = None) -> List[Dict[str, Any]]:
|
async def fetch_stock_news(limit: int = 10, category: str = None) -> List[Dict[str, Any]]:
|
||||||
@@ -394,3 +397,38 @@ async def lotto_evolver_evaluate() -> Dict[str, Any]:
|
|||||||
resp = await client.post(f"{LOTTO_BACKEND_URL}/api/lotto/evolver/evaluate-now")
|
resp = await client.post(f"{LOTTO_BACKEND_URL}/api/lotto/evolver/evaluate-now")
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
from .config import AGENT_CONTAINER_MAP
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_service_logs(
|
||||||
|
agent_id: str,
|
||||||
|
since: Optional[str] = None,
|
||||||
|
limit: int = 200,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""해당 에이전트가 가리키는 컨테이너의 /logs/recent 를 호출해서
|
||||||
|
path_prefix 정규식으로 필터한 결과를 반환.
|
||||||
|
|
||||||
|
네트워크 실패 시 빈 리스트를 반환하고 warning 만 남김 (LogTab 이 죽지 않게).
|
||||||
|
"""
|
||||||
|
mapping = AGENT_CONTAINER_MAP.get(agent_id)
|
||||||
|
if not mapping:
|
||||||
|
return []
|
||||||
|
host, port, path_re = mapping
|
||||||
|
url = f"http://{host}:{port}/logs/recent"
|
||||||
|
params: Dict[str, Any] = {"limit": limit}
|
||||||
|
if since:
|
||||||
|
params["since"] = since
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=3.0) as client:
|
||||||
|
resp = await client.get(url, params=params)
|
||||||
|
data = resp.json().get("logs", [])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("fetch_service_logs(%s) 실패: %s", agent_id, e)
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
x for x in data
|
||||||
|
if x.get("source") == "log"
|
||||||
|
or path_re.match(x.get("path", "") or "")
|
||||||
|
]
|
||||||
|
|||||||
53
agent-office/tests/test_service_proxy_logs.py
Normal file
53
agent-office/tests/test_service_proxy_logs.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import pytest
|
||||||
|
import respx
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from app.service_proxy import fetch_service_logs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@respx.mock
|
||||||
|
async def test_fetch_service_logs_filters_by_path_prefix():
|
||||||
|
# lotto 컨테이너 응답: lotto + personal 섞임
|
||||||
|
respx.get("http://lotto:8000/logs/recent").mock(
|
||||||
|
return_value=httpx.Response(200, json={
|
||||||
|
"logs": [
|
||||||
|
{"ts": "2026-05-28T10:00:00Z", "source": "access",
|
||||||
|
"method": "GET", "path": "/api/lotto/recommend",
|
||||||
|
"status": 200, "ms": 12,
|
||||||
|
"message": "GET /api/lotto/recommend → 200 (12ms)"},
|
||||||
|
{"ts": "2026-05-28T10:00:01Z", "source": "access",
|
||||||
|
"method": "GET", "path": "/api/blog/posts",
|
||||||
|
"status": 200, "ms": 5,
|
||||||
|
"message": "GET /api/blog/posts → 200 (5ms)"},
|
||||||
|
{"ts": "2026-05-28T10:00:02Z", "source": "log",
|
||||||
|
"logger": "lotto", "level": "info",
|
||||||
|
"message": "성과 통계 캐시 갱신"},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await fetch_service_logs("lotto", limit=50)
|
||||||
|
# lotto path 와 모든 log 이벤트만 통과
|
||||||
|
paths = [x.get("path") for x in result]
|
||||||
|
assert "/api/lotto/recommend" in paths
|
||||||
|
assert "/api/blog/posts" not in paths
|
||||||
|
# 비즈니스 로그도 포함
|
||||||
|
assert any(x["source"] == "log" and x["message"] == "성과 통계 캐시 갱신"
|
||||||
|
for x in result)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fetch_service_logs_unknown_agent_returns_empty():
|
||||||
|
result = await fetch_service_logs("nonexistent", limit=50)
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@respx.mock
|
||||||
|
async def test_fetch_service_logs_handles_connection_error():
|
||||||
|
respx.get("http://lotto:8000/logs/recent").mock(
|
||||||
|
side_effect=httpx.ConnectError("connection refused")
|
||||||
|
)
|
||||||
|
result = await fetch_service_logs("lotto", limit=50)
|
||||||
|
assert result == []
|
||||||
Reference in New Issue
Block a user