Remote stack logs: surface proxy/agent errors instead of silent disconnect (0.13.1)
Remote-stack log streaming showed only "disconnected, 0 lines" whenever the agent log proxy failed, because the LogViewer ignored type:"error" messages and the proxy swallowed connection errors. - ws.py: the agent-logs proxy now reports a clear, logged reason on failure — distinguishes "cannot reach agent <url>" from a handshake rejection (HTTP 404 hints the agent is outdated and lacks live-log support) and forwards abnormal upstream close codes (e.g. 4401 bad agent token). - LogViewer: renders type:"error" messages (red) and surfaces a 4401 close as an authorization error, instead of silently showing "Waiting for log output…". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
25bba1cf2c
commit
5ebd615651
+39
-7
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import contextlib
|
||||
|
||||
@@ -16,6 +17,8 @@ from database import engine
|
||||
from models.agent import Agent
|
||||
from services import compose_service
|
||||
|
||||
logger = logging.getLogger("stackpilot.ws")
|
||||
|
||||
router = APIRouter(tags=["ws"])
|
||||
|
||||
|
||||
@@ -113,18 +116,47 @@ async def ws_agent_logs(
|
||||
else "ws://" + base)
|
||||
ws_url += f"/agent/ws/logs/{stack_id}?token={agent.token}"
|
||||
|
||||
async def _err(detail: str) -> None:
|
||||
with contextlib.suppress(Exception):
|
||||
await websocket.send_text(json.dumps({"type": "error", "detail": detail}))
|
||||
|
||||
# Connect to the agent. Surface connection problems (agent down, wrong URL,
|
||||
# an outdated agent that lacks /agent/ws/logs, TLS issues) instead of
|
||||
# silently dropping the socket.
|
||||
try:
|
||||
async with websockets.connect(ws_url, open_timeout=10, ping_interval=20) as upstream:
|
||||
async for message in upstream:
|
||||
await websocket.send_text(
|
||||
message if isinstance(message, str) else message.decode("utf-8", "replace")
|
||||
)
|
||||
upstream = await websockets.connect(ws_url, open_timeout=10, ping_interval=20)
|
||||
except websockets.InvalidStatus as exc:
|
||||
code = getattr(getattr(exc, "response", None), "status_code", None)
|
||||
hint = " — the agent may be running an old version without live-log support; update it." if code == 404 else ""
|
||||
logger.warning("Agent log proxy: handshake to %s failed (%s)", agent.name, code)
|
||||
await _err(f"Agent '{agent.name}' rejected the log stream (HTTP {code}){hint}")
|
||||
with contextlib.suppress(Exception):
|
||||
await websocket.close()
|
||||
return
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("Agent log proxy: cannot reach %s at %s: %s", agent.name, agent.url, exc)
|
||||
await _err(f"Could not connect to agent '{agent.name}' at {agent.url}: {exc}")
|
||||
with contextlib.suppress(Exception):
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
try:
|
||||
async for message in upstream:
|
||||
await websocket.send_text(
|
||||
message if isinstance(message, str) else message.decode("utf-8", "replace")
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except websockets.ConnectionClosed as exc:
|
||||
# Abnormal upstream close (e.g. 4401 bad token, or agent-side error).
|
||||
if exc.code not in (1000, 1001):
|
||||
await _err(f"Agent log stream closed unexpectedly (code {exc.code}).")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
with contextlib.suppress(Exception):
|
||||
await websocket.send_text(json.dumps({"type": "error", "detail": str(exc)}))
|
||||
logger.warning("Agent log proxy: stream error from %s: %s", agent.name, exc)
|
||||
await _err(str(exc))
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
await upstream.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await websocket.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user