Add cloud server log forwarding via WebSocket#548
Conversation
Forward fal.ai server-side logs to the client through the existing cloud WebSocket connection. Logs are captured from the subprocess stdout, batched, and sent as JSON messages to the client where they are re-emitted into Python logging with a [CLOUD] prefix. This makes cloud-side logs visible in the terminal, log file, and Electron LogViewer — interleaved chronologically with local logs. Also adds a frontend log panel (toggle via StatusBar terminal icon) with filter/copy/clear functionality, and a /api/v1/logs/tail endpoint for efficient incremental log polling. Signed-off-by: emranemran <[email protected]>
9485470 to
4c3e94e
Compare
🚀 fal.ai Preview Deployment
TestingConnect to this preview deployment by setting the fal endpoint in your client: 🧪 E2E tests will run automatically against this deployment. |
✅ E2E Tests passed
Test ArtifactsCheck the workflow run for screenshots. |
…tdout LogBroadcaster was using asyncio.Queue which is not thread-safe — publish() from the subprocess reader thread couldn't properly wake up the async forwarder's await q.get(). Switch to stdlib queue.Queue with polling. Also set PYTHONUNBUFFERED=1 in subprocess env to prevent Python's block buffering when stdout is a pipe (lines were stuck in 8KB buffer). Signed-off-by: emranemran <[email protected]>
mjh1
left a comment
There was a problem hiding this comment.
Does the logs panel have any kind of limit on the number of logs? Just wondering for long sessions if it might slow the UI down or anything.
| else: | ||
| # Unsolicited message (e.g., notifications) | ||
| logger.debug(f"Received unsolicited message: {msg_type}") | ||
| msg_type = data.get("type") | ||
| if msg_type == "logs": | ||
| self._handle_cloud_logs(data) | ||
| else: | ||
| logger.debug(f"Received unsolicited message: {msg_type}") |
There was a problem hiding this comment.
we're already getting msg_type above, and we don't want this extra "unsolicited message" log. so it could just become an elif then keep the existing else as it was
| else: | |
| # Unsolicited message (e.g., notifications) | |
| logger.debug(f"Received unsolicited message: {msg_type}") | |
| msg_type = data.get("type") | |
| if msg_type == "logs": | |
| self._handle_cloud_logs(data) | |
| else: | |
| logger.debug(f"Received unsolicited message: {msg_type}") | |
| elif msg_type == "logs": | |
| self._handle_cloud_logs(data) | |
| else: | |
| # Unsolicited message (e.g., notifications) | |
| logger.debug(f"Received unsolicited message: {msg_type}") |
|
I've made a fix so that we don't fetch duplicate logs over and over: 81f3819 (i have to thank claude for spotting that one). It didn't affect the UI too much but it threw off the new log counter thing (showing 99+ when really the logs weren't changing).
|
Signed-off-by: Max Holland <[email protected]>
|
Good catch to ask about! Yes, there's a limit — const combined = [...prev, ...parsed];
return combined.length > MAX_LOG_LINES
? combined.slice(-MAX_LOG_LINES)
: combined;So long sessions shouldn't cause UI slowdown or memory buildup. |
Skip scope.server.kafka_publisher INFO lines (heartbeats, session events) from being forwarded to the client via WebSocket. These are infrastructure bookkeeping with no debugging value to the user. Full logs remain on fal container stdout. Errors/warnings from skipped loggers are still forwarded. Configurable via CLOUD_LOG_SKIP_LOGGERS env var to add more loggers. Also clean up duplicate logger.debug line in cloud_connection._handle_message. Signed-off-by: emranemran <[email protected]>
Remove redundant [CLOUD] prefix from cloud log lines — the scope.cloud logger name already identifies them. Update frontend and Electron to detect cloud lines via "- scope.cloud -" instead. Add blue left border and subtle background tint to cloud lines in LogPanel for clear visual separation. Add "Logs" label next to terminal icon in StatusBar. Signed-off-by: emranemran <[email protected]>
…t error Logger names (cloud_connection, cloud_webrtc_client, cloud_track, etc.) already identify the source. The frontend log panel now uses visual demarcation (blue border + CLOUD badge) instead of text prefixes. Also moves queue import to module level in fal_app.py to fix ruff F821. Signed-off-by: emranemran <[email protected]>
Signed-off-by: emranemran <[email protected]>

Summary
Forward fal.ai server-side logs to the client through the existing Cloud WebSocket connection, making cloud-side debugging visible locally.
PopenwithPYTHONUNBUFFERED=1, batched (up to 50 lines), and sent as{"type": "logs"}JSON messagesscope.cloudlogger — appears in terminal, log file, and Electron LogViewer interleaved with local logsLogBroadcasterusing stdlibqueue.Queueto bridge the sync subprocess reader thread and async WebSocket forwarderkafka_publisher) are skipped from WebSocket forwarding while still appearing in fal container stdout. Configurable viaCLOUD_LOG_SKIP_LOGGERSenv varGET /api/v1/logs/tailendpoint for incremental log pollingHow it works
Cloud Relay Mode (local backend proxies to fal.ai)
Direct Cloud Mode (browser connects directly to fal.ai)
Key implementation details
Server-side (fal.ai)
fal_app.py:LogBroadcasteruses stdlibqueue.Queue(thread-safe) — the subprocess reader thread callspublish(), the async forwarder polls viaget_nowait()+asyncio.sleep(0.5)PYTHONUNBUFFERED=1set in subprocess env to prevent Python block-buffering stdout when piped_should_forward_log()filters noisy loggers (default:kafka_publisher) before WebSocket batching — full logs remain on fal container stdoutCLOUD_LOG_SKIP_LOGGERSenv varClient-side (local backend)
cloud_connection.py: Routestype: "logs"messages to_handle_cloud_logs(), re-emits viascope.cloudlogger (no[CLOUD]prefix — the logger name identifies the source)app.py:/api/v1/logs/tailendpoint with byte offset trackingFrontend
cloudAdapter.ts:onLogs()handler for direct cloud modeuseLogStream.ts: Unified hook — WS push (cloud) + polling (local/relay). Detects cloud lines via"- scope.cloud -"logger nameLogPanel.tsx: Bottom panel with All/Errors/Cloud filters, color-coded levels, auto-scroll, copy. Cloud lines have blue left border + subtle background tint for visual demarcationStatusBar.tsx: Terminal icon + "Logs" label with unread badgeElectron
LogViewer.js+LogViewer.html:- scope.cloud -marker with purple highlightingTest plan
scope.cloudloggerkafka_publisherlogs do NOT appear locally but DO appear on fal dashboardcurl "http://localhost:8000/api/v1/logs/tail?lines=10"returns recent log lines with offsetnpm run buildsucceeds