fix(logs): reduce production log spam — agent llm-config polling, scheduler plumbing
- middleware: suppress structured REASON/REFLECT framing for high-frequency
pollers (/api/agent/llm-config, /api/tasks/{id}, health/summary,
session/activity, settings/consolidated); fixes tasks/{id} never matching
- agent: _fetch_llm_config treats 401/403 as terminal (no retry, log once),
bounded backoff 5s/15s/60s on connect/timeout/5xx; langgraph_setup logs
auth failure once per process
- scheduler: auto-end plumbing lines (executed/scan triggered) -> DEBUG
- thumbnail: Superset 4xx rejections logged at DEBUG instead of EXPLORE
This commit is contained in:
@@ -144,6 +144,9 @@ async def init_checkpointer() -> None:
|
||||
# #endregion AgentChat.LangGraph.Setup.InitCheckpointer
|
||||
|
||||
_llm_config: dict | None = None
|
||||
# 401/403 from /api/agent/llm-config is a permanent misconfiguration (service JWT).
|
||||
# Log the EXPLORE once per process — retrying per user message would spam logs.
|
||||
_llm_config_auth_failure_logged = False
|
||||
|
||||
|
||||
# #region AgentChat.LangGraph.Setup.LlmDiagnostics [C:2] [TYPE Function] [SEMANTICS agent-chat,llm,observability,redaction]
|
||||
@@ -181,7 +184,7 @@ def configure_from_api(llm_config: dict) -> None:
|
||||
# @ingroup AgentChat
|
||||
# @BRIEF Fetch LLM provider config from FastAPI /api/agent/llm-config.
|
||||
async def _fetch_llm_config() -> dict | None:
|
||||
global _llm_config
|
||||
global _llm_config, _llm_config_auth_failure_logged
|
||||
logger.reason(
|
||||
"Fetching agent LLM configuration",
|
||||
payload={"fastapi_host": urlsplit(FASTAPI_URL).hostname or ""},
|
||||
@@ -212,6 +215,16 @@ async def _fetch_llm_config() -> dict | None:
|
||||
extra={"src": "AgentChat.LangGraph.Setup.FetchLlmConfig"},
|
||||
)
|
||||
else:
|
||||
if resp.status_code in (401, 403):
|
||||
if not _llm_config_auth_failure_logged:
|
||||
_llm_config_auth_failure_logged = True
|
||||
logger.explore(
|
||||
"Agent LLM configuration rejected by backend",
|
||||
payload={"http_status": resp.status_code, "fastapi_host": urlsplit(fastapi_url).hostname or ""},
|
||||
error="Invalid or expired SERVICE_JWT — not retrying",
|
||||
extra={"src": "AgentChat.LangGraph.Setup.FetchLlmConfig"},
|
||||
)
|
||||
return _llm_config
|
||||
logger.explore(
|
||||
"Agent LLM configuration request failed",
|
||||
payload={"http_status": resp.status_code, "fastapi_host": urlsplit(fastapi_url).hostname or ""},
|
||||
|
||||
@@ -38,17 +38,20 @@ def _find_free_port(start_port: int, max_attempts: int = 100) -> int:
|
||||
|
||||
|
||||
def _fetch_llm_config() -> dict | None:
|
||||
"""Fetch active LLM provider config from FastAPI with retry.
|
||||
"""Fetch active LLM provider config from FastAPI with bounded retry.
|
||||
|
||||
Retries up to 30s (6 × 5s) to wait for FastAPI to be ready.
|
||||
Falls back to env vars if FastAPI is unreachable or returns no active provider.
|
||||
- 401/403 (invalid/expired SERVICE_JWT): terminal — log once, do NOT retry
|
||||
(an auth failure will not fix itself; endless retries just spam backend logs).
|
||||
- Connect/timeout/5xx: retry with backoff (5s, 15s, 60s; 4 attempts total).
|
||||
- configured=false: no retry — fall back to env vars.
|
||||
"""
|
||||
import time
|
||||
service_token = SERVICE_JWT
|
||||
headers = {"Authorization": f"Bearer {service_token}"} if service_token else {}
|
||||
|
||||
ssl_ctx = httpx_verify()
|
||||
for attempt in range(6):
|
||||
backoff = (5, 15, 60)
|
||||
for attempt in range(len(backoff) + 1):
|
||||
try:
|
||||
resp = httpx.get(
|
||||
f"{FASTAPI_URL}/api/agent/llm-config",
|
||||
@@ -56,6 +59,14 @@ def _fetch_llm_config() -> dict | None:
|
||||
timeout=5,
|
||||
verify=ssl_ctx,
|
||||
)
|
||||
if resp.status_code in (401, 403):
|
||||
logger.explore(
|
||||
"Agent LLM config rejected by backend",
|
||||
payload={"http_status": resp.status_code},
|
||||
error="Invalid or expired SERVICE_JWT — not retrying",
|
||||
extra={"src": "AgentChat.Run.FetchLlmConfig"},
|
||||
)
|
||||
return None
|
||||
resp.raise_for_status()
|
||||
config = resp.json()
|
||||
if config.get("configured"):
|
||||
@@ -71,17 +82,18 @@ def _fetch_llm_config() -> dict | None:
|
||||
error="No configured LLM provider",
|
||||
extra={"src": "AgentChat.Run.FetchLlmConfig"},
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
if attempt < 5:
|
||||
if attempt < len(backoff):
|
||||
logger.reason(
|
||||
f"Waiting for FastAPI (attempt {attempt + 1}/6)",
|
||||
payload={"error": str(e)},
|
||||
f"Waiting for FastAPI (attempt {attempt + 1}/{len(backoff) + 1})",
|
||||
payload={"error": str(e), "retry_after_s": backoff[attempt]},
|
||||
extra={"src": "AgentChat.Run.FetchLlmConfig"},
|
||||
)
|
||||
time.sleep(5)
|
||||
time.sleep(backoff[attempt])
|
||||
else:
|
||||
logger.explore(
|
||||
"Failed to fetch LLM config after 6 attempts",
|
||||
"Failed to fetch LLM config after retries",
|
||||
error=str(e),
|
||||
extra={"src": "AgentChat.Run.FetchLlmConfig"},
|
||||
)
|
||||
|
||||
@@ -69,6 +69,35 @@ class TestFetchLlmConfig:
|
||||
mock_get.return_value = mock_response
|
||||
result = _fetch_llm_config()
|
||||
assert result is None
|
||||
# configured=false is terminal — single request, no retry
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
def test_does_not_retry_on_401(self):
|
||||
from ss_tools.agent.run import _fetch_llm_config
|
||||
import time as time_module
|
||||
with patch("ss_tools.agent.run.httpx.get") as mock_get, \
|
||||
patch.object(time_module, "sleep") as mock_sleep:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 401
|
||||
mock_get.return_value = mock_response
|
||||
result = _fetch_llm_config()
|
||||
assert result is None
|
||||
# Auth failure is terminal — exactly one request, no backoff sleep
|
||||
assert mock_get.call_count == 1
|
||||
mock_sleep.assert_not_called()
|
||||
|
||||
def test_does_not_retry_on_403(self):
|
||||
from ss_tools.agent.run import _fetch_llm_config
|
||||
import time as time_module
|
||||
with patch("ss_tools.agent.run.httpx.get") as mock_get, \
|
||||
patch.object(time_module, "sleep") as mock_sleep:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 403
|
||||
mock_get.return_value = mock_response
|
||||
result = _fetch_llm_config()
|
||||
assert result is None
|
||||
assert mock_get.call_count == 1
|
||||
mock_sleep.assert_not_called()
|
||||
|
||||
def test_retries_on_failure(self):
|
||||
from ss_tools.agent.run import _fetch_llm_config
|
||||
@@ -78,7 +107,8 @@ class TestFetchLlmConfig:
|
||||
mock_get.side_effect = Exception("Connection refused")
|
||||
result = _fetch_llm_config()
|
||||
assert result is None
|
||||
assert mock_get.call_count == 6
|
||||
# 1 initial attempt + 3 backoff retries (5s/15s/60s)
|
||||
assert mock_get.call_count == 4
|
||||
|
||||
def test_retries_then_returns_config(self):
|
||||
from ss_tools.agent.run import _fetch_llm_config
|
||||
@@ -104,7 +134,8 @@ class TestFetchLlmConfig:
|
||||
mock_get.return_value = mock_response
|
||||
result = _fetch_llm_config()
|
||||
assert result is None
|
||||
assert mock_get.call_count == 6
|
||||
# 5xx errors are retried with backoff: 1 initial + 3 retries
|
||||
assert mock_get.call_count == 4
|
||||
|
||||
def test_uses_service_token_header(self):
|
||||
from ss_tools.agent.run import _fetch_llm_config
|
||||
|
||||
Reference in New Issue
Block a user