3434logger = logging .getLogger ("replane" )
3535
3636
37+ def _setup_debug_logging () -> None :
38+ """Configure the replane logger for debug output."""
39+ logger .setLevel (logging .DEBUG )
40+ # Only add handler if none exist to avoid duplicates
41+ if not logger .handlers :
42+ handler = logging .StreamHandler ()
43+ handler .setLevel (logging .DEBUG )
44+ formatter = logging .Formatter (
45+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s" ,
46+ datefmt = "%Y-%m-%d %H:%M:%S" ,
47+ )
48+ handler .setFormatter (formatter )
49+ logger .addHandler (handler )
50+
51+
3752class AsyncReplaneClient :
3853 """Asynchronous Replane client with background SSE streaming.
3954
@@ -68,6 +83,7 @@ def __init__(
6883 initialization_timeout_ms : int = 5000 ,
6984 retry_delay_ms : int = 200 ,
7085 inactivity_timeout_ms : int = 30000 ,
86+ debug : bool = False ,
7187 ) -> None :
7288 """Initialize the async Replane client.
7389
@@ -81,13 +97,35 @@ def __init__(
8197 initialization_timeout_ms: Timeout for initial connection.
8298 retry_delay_ms: Initial delay between retries.
8399 inactivity_timeout_ms: Max time without SSE events before reconnect.
100+ debug: Enable debug logging to see all client activity.
84101
85102 Raises:
86103 MissingDependencyError: If httpx is not installed.
87104 """
88105 if httpx is None :
89106 raise MissingDependencyError ("httpx" , "async client" )
90107
108+ # Configure debug logging
109+ self ._debug = debug
110+ if debug :
111+ _setup_debug_logging ()
112+ logger .debug (
113+ "Initializing AsyncReplaneClient: base_url=%s, "
114+ "request_timeout_ms=%d, initialization_timeout_ms=%d, "
115+ "retry_delay_ms=%d, inactivity_timeout_ms=%d" ,
116+ base_url ,
117+ request_timeout_ms ,
118+ initialization_timeout_ms ,
119+ retry_delay_ms ,
120+ inactivity_timeout_ms ,
121+ )
122+ if context :
123+ logger .debug ("Default context: %s" , context )
124+ if fallbacks :
125+ logger .debug ("Fallback configs: %s" , list (fallbacks .keys ()))
126+ if required :
127+ logger .debug ("Required configs: %s" , required )
128+
91129 self ._base_url = base_url .rstrip ("/" )
92130 self ._sdk_key = sdk_key
93131 self ._context = context or {}
@@ -131,6 +169,8 @@ async def connect(self, *, wait: bool = True) -> None:
131169 if self ._closed :
132170 raise ClientClosedError ()
133171
172+ logger .debug ("connect() called, wait=%s" , wait )
173+
134174 self ._http_client = httpx .AsyncClient (
135175 timeout = httpx .Timeout (self ._request_timeout , read = None ),
136176 )
@@ -139,6 +179,7 @@ async def connect(self, *, wait: bool = True) -> None:
139179 self ._run_stream (),
140180 name = "replane-sse" ,
141181 )
182+ logger .debug ("SSE background task started" )
142183
143184 if wait :
144185 await self .wait_for_init ()
@@ -195,14 +236,25 @@ def get(
195236 raise ClientClosedError ()
196237
197238 merged_context = {** self ._context , ** (context or {})}
239+ logger .debug ("get(%r) with context: %s" , name , merged_context or "(none)" )
198240
199241 if name not in self ._configs :
200242 if default is not None :
243+ logger .debug ("Config %r not found, returning default: %r" , name , default )
201244 return default
245+ logger .debug ("Config %r not found, no default provided" , name )
202246 raise ConfigNotFoundError (name )
203247
204248 config = self ._configs [name ]
205- return evaluate_config (config , merged_context )
249+ result = evaluate_config (config , merged_context )
250+ logger .debug (
251+ "Config %r: base_value=%r, overrides=%d, result=%r" ,
252+ name ,
253+ config .value ,
254+ len (config .overrides ),
255+ result ,
256+ )
257+ return result
206258
207259 def subscribe (
208260 self ,
@@ -255,17 +307,22 @@ def unsubscribe() -> None:
255307
256308 async def close (self ) -> None :
257309 """Close the client and stop the SSE connection."""
310+ logger .debug ("close() called" )
258311 self ._closed = True
259312
260313 if self ._stream_task :
314+ logger .debug ("Cancelling SSE task..." )
261315 self ._stream_task .cancel ()
262316 try :
263317 await self ._stream_task
264318 except asyncio .CancelledError :
265319 pass
320+ logger .debug ("SSE task cancelled" )
266321
267322 if self ._http_client :
323+ logger .debug ("Closing HTTP client..." )
268324 await self ._http_client .aclose ()
325+ logger .debug ("HTTP client closed" )
269326
270327 async def __aenter__ (self ) -> AsyncReplaneClient :
271328 await self .connect ()
@@ -331,21 +388,27 @@ async def _connect_stream(self) -> None:
331388 "Cache-Control" : "no-cache" ,
332389 }
333390
391+ logger .debug ("Connecting to SSE: %s" , url )
392+
334393 async with self ._http_client .stream (
335394 "POST" ,
336395 url ,
337396 json = {},
338397 headers = headers ,
339398 ) as response :
399+ logger .debug ("Response status: %d" , response .status_code )
340400 if response .status_code == 401 :
401+ logger .debug ("Authentication failed (401)" )
341402 raise AuthenticationError ()
342403 elif response .status_code != 200 :
343404 body = await response .aread ()
405+ logger .debug ("Error response body: %s" , body [:500 ])
344406 raise from_http_status (
345407 response .status_code ,
346408 body .decode ("utf-8" , errors = "replace" ),
347409 )
348410
411+ logger .debug ("SSE connection established, processing stream..." )
349412 await self ._process_stream (response )
350413
351414 async def _process_stream (self , response : httpx .Response ) -> None :
@@ -361,29 +424,44 @@ async def _process_stream(self, response: httpx.Response) -> None:
361424
362425 async def _handle_event (self , event : Any ) -> None :
363426 """Handle a parsed SSE event."""
427+ logger .debug ("SSE event received: type=%s" , event .event )
364428 if event .event == "init" :
365429 await self ._handle_init (event .data )
366430 elif event .event == "config_change" :
367431 await self ._handle_config_change (event .data )
432+ else :
433+ logger .debug ("Unknown event type: %s, data=%s" , event .event , event .data )
368434
369435 async def _handle_init (self , data : dict [str , Any ]) -> None :
370436 """Handle the init event with all configs."""
371437 configs_data = data .get ("configs" , [])
438+ logger .debug ("Processing init event with %d configs" , len (configs_data ))
372439
373440 async with self ._lock :
374441 for config_data in configs_data :
375442 config = parse_config (config_data )
376443 self ._configs [config .name ] = config
444+ logger .debug (
445+ "Loaded config: %s (value=%r, overrides=%d)" ,
446+ config .name ,
447+ config .value ,
448+ len (config .overrides ),
449+ )
377450
378451 # Check required configs
379452 missing = self ._required - set (self ._configs .keys ())
380453 if missing :
454+ logger .debug ("Missing required configs: %s" , sorted (missing ))
381455 self ._init_error = ConfigNotFoundError (
382456 f"Missing required configs: { ', ' .join (sorted (missing ))} "
383457 )
384458
385459 self ._initialized .set ()
386- logger .debug ("Initialized with %d configs" , len (configs_data ))
460+ logger .debug (
461+ "Initialization complete: %d configs loaded, config names: %s" ,
462+ len (self ._configs ),
463+ list (self ._configs .keys ()),
464+ )
387465
388466 async def _handle_config_change (self , data : dict [str , Any ]) -> None :
389467 """Handle a config change event."""
0 commit comments