@@ -271,17 +271,58 @@ def _build_runner_command() -> list[str]:
271271 ]
272272
273273
274- async def _proxy_ws (client_ws : WebSocket ) -> None :
274+ async def _proxy_ws (client_ws : WebSocket , metadata : dict | None = None ) -> None :
275275 """Connect to the local runner and proxy traffic bidirectionally.
276276
277+ If *metadata* is provided, the initial handshake messages are sniffed
278+ (best-effort) to extract ``manifest_id`` and ``user_id`` before the
279+ bidirectional proxy loop begins.
280+
277281 Raises WebSocketDisconnect if the client disconnects.
278282 Returns normally if the runner connection drops.
279283 """
284+ import json
285+
280286 import websockets
281287 from websockets .exceptions import ConnectionClosed
282288
283289 async with websockets .connect (RUNNER_LOCAL_WS_URL ) as runner_ws :
284-
290+ # --- Sniff the two-message handshake (best-effort) ---
291+ # 1. Runner → Client: {"type": "ready", "connection_id": "..."}
292+ ready_msg = await runner_ws .recv ()
293+ if isinstance (ready_msg , bytes ):
294+ await client_ws .send_bytes (ready_msg )
295+ else :
296+ if metadata is not None :
297+ try :
298+ parsed = json .loads (ready_msg )
299+ metadata ["connection_id" ] = parsed .get ("connection_id" )
300+ except (json .JSONDecodeError , AttributeError ):
301+ pass
302+ await client_ws .send_text (ready_msg )
303+
304+ # 2. Client → Runner: job_info with manifest_id, params.daydream_user_id
305+ job_msg = await client_ws .receive ()
306+ job_msg_type = job_msg .get ("type" )
307+ if job_msg_type == "websocket.receive" :
308+ text_data = job_msg .get ("text" )
309+ bytes_data = job_msg .get ("bytes" )
310+ if text_data is not None :
311+ if metadata is not None :
312+ try :
313+ parsed = json .loads (text_data )
314+ metadata ["manifest_id" ] = parsed .get ("manifest_id" )
315+ params = parsed .get ("params" ) or {}
316+ metadata ["user_id" ] = params .get ("daydream_user_id" )
317+ except (json .JSONDecodeError , AttributeError ):
318+ pass
319+ await runner_ws .send (text_data )
320+ elif bytes_data is not None :
321+ await runner_ws .send (bytes_data )
322+ elif job_msg_type == "websocket.disconnect" :
323+ raise WebSocketDisconnect ()
324+
325+ # --- Bidirectional proxy loop ---
285326 async def client_to_runner () -> None :
286327 while True :
287328 message = await client_ws .receive ()
@@ -437,6 +478,33 @@ async def websocket_handler(self, client_ws: WebSocket) -> None:
437478
438479 await client_ws .accept ()
439480
481+ # Initialize Kafka publisher (lazy, once per process)
482+ global kafka_publisher
483+ if kafka_publisher is None :
484+ kafka_publisher = KafkaPublisher ()
485+ await kafka_publisher .start ()
486+
487+ connection_start_time = time .time ()
488+ metadata : dict = {}
489+ connected_event_published = False
490+
491+ import json
492+
493+ fal_log_labels_raw = os .getenv ("FAL_LOG_LABELS" , "unknown" )
494+ try :
495+ fal_log_labels = json .loads (fal_log_labels_raw )
496+ except (json .JSONDecodeError , TypeError ):
497+ fal_log_labels = fal_log_labels_raw
498+
499+ connection_info = {
500+ "gpu_type" : LivepeerScopeApp .machine_type ,
501+ "fal_region" : os .getenv ("NOMAD_DC" , "unknown" ),
502+ "fal_runner_id" : os .getenv (
503+ "FAL_JOB_ID" , os .getenv ("FAL_RUNNER_ID" , "unknown" )
504+ ),
505+ "fal_log_labels" : fal_log_labels ,
506+ }
507+
440508 # Ensure any previous session data is cleaned up
441509 event = _get_cleanup_event ()
442510 await event .wait ()
@@ -448,7 +516,7 @@ async def websocket_handler(self, client_ws: WebSocket) -> None:
448516 while True :
449517 print (f"Connecting proxy to runner websocket at { RUNNER_LOCAL_WS_URL } " )
450518 try :
451- await _proxy_ws (client_ws )
519+ await _proxy_ws (client_ws , metadata = metadata )
452520 except (
453521 ConnectionClosed ,
454522 InvalidStatus ,
@@ -457,6 +525,23 @@ async def websocket_handler(self, client_ws: WebSocket) -> None:
457525 ) as exc :
458526 print (f"Livepeer fal ws runner connection failed: { exc } " )
459527
528+ # Publish websocket_connected once after handshake metadata is available
529+ if (
530+ not connected_event_published
531+ and metadata .get ("manifest_id" )
532+ and kafka_publisher
533+ and kafka_publisher .is_running
534+ ):
535+ await kafka_publisher .publish (
536+ "websocket_connected" ,
537+ {
538+ "user_id" : metadata .get ("user_id" ),
539+ "connection_id" : metadata .get ("manifest_id" ),
540+ "connection_info" : connection_info ,
541+ },
542+ )
543+ connected_event_published = True
544+
460545 now = time .monotonic ()
461546 cutoff = now - RUNNER_FAILURE_WINDOW_SECONDS
462547 failure_timestamps .append (now )
@@ -478,6 +563,22 @@ async def websocket_handler(self, client_ws: WebSocket) -> None:
478563 except Exception as exc :
479564 print (f"Livepeer fal ws proxy error: { type (exc ).__name__ } : { exc } " )
480565 finally :
566+ # Publish websocket_disconnected event
567+ if kafka_publisher and kafka_publisher .is_running :
568+ end_time = time .time ()
569+ elapsed_ms = int ((end_time - connection_start_time ) * 1000 )
570+ await kafka_publisher .publish (
571+ "websocket_disconnected" ,
572+ {
573+ "user_id" : metadata .get ("user_id" ),
574+ "connection_id" : metadata .get ("manifest_id" ),
575+ "connection_info" : connection_info ,
576+ "duration_ms" : elapsed_ms ,
577+ "session_start_time_ms" : int (connection_start_time * 1000 ),
578+ "session_end_time_ms" : int (end_time * 1000 ),
579+ },
580+ )
581+
481582 await run_cleanup ()
482583 with suppress (Exception ):
483584 await client_ws .close ()
0 commit comments