nebu 0.1.91__py3-none-any.whl → 0.1.93__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,7 @@ import socks
17
17
  from redis import ConnectionError, ResponseError
18
18
 
19
19
  from nebu.errors import RetriableError
20
+ from nebu.logging import logger
20
21
 
21
22
  # Define TypeVar for generic models
22
23
  T = TypeVar("T")
@@ -38,12 +39,12 @@ NEBU_EXECUTION_MODE = os.environ.get("NEBU_EXECUTION_MODE", "inline").lower()
38
39
  execution_mode = NEBU_EXECUTION_MODE
39
40
 
40
41
  if execution_mode not in ["inline", "subprocess"]:
41
- print(
42
+ logger.warning(
42
43
  f"Invalid NEBU_EXECUTION_MODE: {NEBU_EXECUTION_MODE}. Must be 'inline' or 'subprocess'. Defaulting to 'inline'."
43
44
  )
44
45
  execution_mode = "inline"
45
46
 
46
- print(f"Execution mode: {execution_mode}")
47
+ logger.info(f"Execution mode: {execution_mode}")
47
48
 
48
49
 
49
50
  # --- Function to Load/Reload User Code ---
@@ -69,16 +70,18 @@ def load_or_reload_user_code(
69
70
  loaded_module = None
70
71
  exec_namespace: Dict[str, Any] = {} # Use a local namespace for this load attempt
71
72
 
72
- print(f"[Code Loader] Attempting to load/reload module: '{module_path}'")
73
+ logger.info(f"[Code Loader] Attempting to load/reload module: '{module_path}'")
73
74
  os.environ[_NEBU_INSIDE_CONSUMER_ENV_VAR] = "1" # Set guard *before* import/reload
74
- print(f"[Code Loader] Set environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}=1")
75
+ logger.debug(
76
+ f"[Code Loader] Set environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}=1"
77
+ )
75
78
 
76
79
  try:
77
80
  current_mtime = os.path.getmtime(entrypoint_abs_path)
78
81
 
79
82
  # Execute included object sources FIRST (if any)
80
83
  if included_object_sources:
81
- print("[Code Loader] Executing @include object sources...")
84
+ logger.debug("[Code Loader] Executing @include object sources...")
82
85
  # Include necessary imports for the exec context
83
86
  exec("from pydantic import BaseModel, Field", exec_namespace)
84
87
  exec(
@@ -92,28 +95,34 @@ def load_or_reload_user_code(
92
95
  for i, (obj_source, args_sources) in enumerate(included_object_sources):
93
96
  try:
94
97
  exec(obj_source, exec_namespace)
95
- print(
98
+ logger.debug(
96
99
  f"[Code Loader] Successfully executed included object {i} base source"
97
100
  )
98
101
  for j, arg_source in enumerate(args_sources):
99
102
  try:
100
103
  exec(arg_source, exec_namespace)
101
- print(
104
+ logger.debug(
102
105
  f"[Code Loader] Successfully executed included object {i} arg {j} source"
103
106
  )
104
107
  except Exception as e_arg:
105
- print(
108
+ logger.error(
106
109
  f"Error executing included object {i} arg {j} source: {e_arg}"
107
110
  )
108
- traceback.print_exc() # Log specific error but continue? Or fail reload?
111
+ logger.exception(
112
+ f"Traceback for included object {i} arg {j} source error:"
113
+ )
109
114
  except Exception as e_base:
110
- print(f"Error executing included object {i} base source: {e_base}")
111
- traceback.print_exc() # Log specific error but continue? Or fail reload?
112
- print("[Code Loader] Finished executing included object sources.")
115
+ logger.error(
116
+ f"Error executing included object {i} base source: {e_base}"
117
+ )
118
+ logger.exception(
119
+ f"Traceback for included object {i} base source error:"
120
+ )
121
+ logger.debug("[Code Loader] Finished executing included object sources.")
113
122
 
114
123
  # Check if module is already loaded and needs reload
115
124
  if module_path in sys.modules:
116
- print(
125
+ logger.info(
117
126
  f"[Code Loader] Module '{module_path}' already imported. Reloading..."
118
127
  )
119
128
  # Pass the exec_namespace as globals? Usually reload works within its own context.
@@ -121,32 +130,34 @@ def load_or_reload_user_code(
121
130
  # reload might not pick that up easily. Might need a fresh import instead.
122
131
  # Let's try reload first.
123
132
  loaded_module = importlib.reload(sys.modules[module_path])
124
- print(f"[Code Loader] Successfully reloaded module: {module_path}")
133
+ logger.info(f"[Code Loader] Successfully reloaded module: {module_path}")
125
134
  else:
126
135
  # Import the main module
127
136
  loaded_module = importlib.import_module(module_path)
128
- print(
137
+ logger.info(
129
138
  f"[Code Loader] Successfully imported module for the first time: {module_path}"
130
139
  )
131
140
 
132
141
  # Get the target function from the loaded/reloaded module
133
142
  loaded_target_func = getattr(loaded_module, function_name)
134
- print(
143
+ logger.info(
135
144
  f"[Code Loader] Successfully loaded function '{function_name}' from module '{module_path}'"
136
145
  )
137
146
 
138
147
  # Get the init function if specified
139
148
  if init_func_name:
140
149
  loaded_init_func = getattr(loaded_module, init_func_name)
141
- print(
150
+ logger.info(
142
151
  f"[Code Loader] Successfully loaded init function '{init_func_name}' from module '{module_path}'"
143
152
  )
144
153
  # Execute init_func
145
- print(f"[Code Loader] Executing init_func: {init_func_name}...")
154
+ logger.info(f"[Code Loader] Executing init_func: {init_func_name}...")
146
155
  loaded_init_func() # Call the function
147
- print(f"[Code Loader] Successfully executed init_func: {init_func_name}")
156
+ logger.info(
157
+ f"[Code Loader] Successfully executed init_func: {init_func_name}"
158
+ )
148
159
 
149
- print("[Code Loader] Code load/reload successful.")
160
+ logger.info("[Code Loader] Code load/reload successful.")
150
161
  return (
151
162
  loaded_target_func,
152
163
  loaded_init_func,
@@ -156,37 +167,39 @@ def load_or_reload_user_code(
156
167
  )
157
168
 
158
169
  except FileNotFoundError:
159
- print(
170
+ logger.error(
160
171
  f"[Code Loader] Error: Entrypoint file not found at '{entrypoint_abs_path}'. Cannot load/reload."
161
172
  )
162
173
  return None, None, None, {}, 0.0 # Indicate failure
163
174
  except ImportError as e:
164
- print(f"[Code Loader] Error importing/reloading module '{module_path}': {e}")
165
- traceback.print_exc()
175
+ logger.error(
176
+ f"[Code Loader] Error importing/reloading module '{module_path}': {e}"
177
+ )
178
+ logger.exception("Import/Reload Error Traceback:")
166
179
  return None, None, None, {}, 0.0 # Indicate failure
167
180
  except AttributeError as e:
168
- print(
181
+ logger.error(
169
182
  f"[Code Loader] Error accessing function '{function_name}' or '{init_func_name}' in module '{module_path}': {e}"
170
183
  )
171
- traceback.print_exc()
184
+ logger.exception("Attribute Error Traceback:")
172
185
  return None, None, None, {}, 0.0 # Indicate failure
173
186
  except Exception as e:
174
- print(f"[Code Loader] Unexpected error during code load/reload: {e}")
175
- traceback.print_exc()
187
+ logger.error(f"[Code Loader] Unexpected error during code load/reload: {e}")
188
+ logger.exception("Unexpected Code Load/Reload Error Traceback:")
176
189
  return None, None, None, {}, 0.0 # Indicate failure
177
190
  finally:
178
191
  # Unset the guard environment variable
179
192
  os.environ.pop(_NEBU_INSIDE_CONSUMER_ENV_VAR, None)
180
- print(
193
+ logger.debug(
181
194
  f"[Code Loader] Unset environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}"
182
195
  )
183
196
 
184
197
 
185
198
  # Print all environment variables before starting
186
- print("===== ENVIRONMENT VARIABLES =====")
199
+ logger.debug("===== ENVIRONMENT VARIABLES =====")
187
200
  for key, value in sorted(os.environ.items()):
188
- print(f"{key}={value}")
189
- print("=================================")
201
+ logger.debug(f"{key}={value}")
202
+ logger.debug("=================================")
190
203
 
191
204
  # --- Get Environment Variables ---
192
205
  try:
@@ -224,7 +237,7 @@ try:
224
237
  break
225
238
 
226
239
  if not _function_name or not _entrypoint_rel_path:
227
- print(
240
+ logger.critical(
228
241
  "FATAL: FUNCTION_NAME or NEBU_ENTRYPOINT_MODULE_PATH environment variables not set"
229
242
  )
230
243
  sys.exit(1)
@@ -242,12 +255,12 @@ try:
242
255
  if os.path.exists(potential_path):
243
256
  entrypoint_abs_path = potential_path
244
257
  found_path = True
245
- print(
258
+ logger.info(
246
259
  f"[Consumer] Found entrypoint absolute path via PYTHONPATH: {entrypoint_abs_path}"
247
260
  )
248
261
  break
249
262
  if not found_path:
250
- print(
263
+ logger.critical(
251
264
  f"FATAL: Could not find entrypoint file via relative path '{_entrypoint_rel_path}' or in PYTHONPATH."
252
265
  )
253
266
  # Attempting abspath anyway for the error message in load function
@@ -260,17 +273,17 @@ try:
260
273
  if _module_path.endswith(".__init__"):
261
274
  _module_path = _module_path[: -len(".__init__")]
262
275
  elif _module_path == "__init__":
263
- print(
276
+ logger.critical(
264
277
  f"FATAL: Entrypoint '{_entrypoint_rel_path}' resolves to ambiguous top-level __init__. Please use a named file or package."
265
278
  )
266
279
  sys.exit(1)
267
280
  if not _module_path:
268
- print(
281
+ logger.critical(
269
282
  f"FATAL: Could not derive a valid module path from entrypoint '{_entrypoint_rel_path}'"
270
283
  )
271
284
  sys.exit(1)
272
285
 
273
- print(
286
+ logger.info(
274
287
  f"[Consumer] Initializing. Entrypoint: '{_entrypoint_rel_path}', Module: '{_module_path}', Function: '{_function_name}', Init: '{_init_func_name}'"
275
288
  )
276
289
 
@@ -290,30 +303,30 @@ try:
290
303
  )
291
304
 
292
305
  if target_function is None or imported_module is None:
293
- print("FATAL: Initial load of user code failed. Exiting.")
306
+ logger.critical("FATAL: Initial load of user code failed. Exiting.")
294
307
  sys.exit(1)
295
- print(
308
+ logger.info(
296
309
  f"[Consumer] Initial code load successful. Last modified time: {last_load_mtime}"
297
310
  )
298
311
 
299
312
 
300
313
  except Exception as e:
301
- print(f"FATAL: Error during initial environment setup or code load: {e}")
302
- traceback.print_exc()
314
+ logger.critical(f"FATAL: Error during initial environment setup or code load: {e}")
315
+ logger.exception("Initial Setup/Load Error Traceback:")
303
316
  sys.exit(1)
304
317
 
305
318
  # Get Redis connection parameters from environment
306
319
  REDIS_URL = os.environ.get("REDIS_URL", "")
307
320
 
308
321
  if not all([REDIS_URL, REDIS_CONSUMER_GROUP, REDIS_STREAM]):
309
- print("Missing required Redis environment variables")
322
+ logger.critical("Missing required Redis environment variables")
310
323
  sys.exit(1)
311
324
 
312
325
  # Configure SOCKS proxy before connecting to Redis
313
326
  # Use the proxy settings provided by tailscaled
314
327
  socks.set_default_proxy(socks.SOCKS5, "localhost", 1055)
315
328
  socket.socket = socks.socksocket
316
- print("Configured SOCKS5 proxy for socket connections via localhost:1055")
329
+ logger.info("Configured SOCKS5 proxy for socket connections via localhost:1055")
317
330
 
318
331
  # Connect to Redis
319
332
  try:
@@ -324,10 +337,10 @@ try:
324
337
  ) # Added decode_responses for convenience
325
338
  r.ping() # Test connection
326
339
  redis_info = REDIS_URL.split("@")[-1] if "@" in REDIS_URL else REDIS_URL
327
- print(f"Connected to Redis via SOCKS proxy at {redis_info}")
340
+ logger.info(f"Connected to Redis via SOCKS proxy at {redis_info}")
328
341
  except Exception as e:
329
- print(f"Failed to connect to Redis via SOCKS proxy: {e}")
330
- traceback.print_exc()
342
+ logger.critical(f"Failed to connect to Redis via SOCKS proxy: {e}")
343
+ logger.exception("Redis Connection Error Traceback:")
331
344
  sys.exit(1)
332
345
 
333
346
  # Create consumer group if it doesn't exist
@@ -336,13 +349,15 @@ try:
336
349
  assert isinstance(REDIS_STREAM, str)
337
350
  assert isinstance(REDIS_CONSUMER_GROUP, str)
338
351
  r.xgroup_create(REDIS_STREAM, REDIS_CONSUMER_GROUP, id="0", mkstream=True)
339
- print(f"Created consumer group {REDIS_CONSUMER_GROUP} for stream {REDIS_STREAM}")
352
+ logger.info(
353
+ f"Created consumer group {REDIS_CONSUMER_GROUP} for stream {REDIS_STREAM}"
354
+ )
340
355
  except ResponseError as e:
341
356
  if "BUSYGROUP" in str(e):
342
- print(f"Consumer group {REDIS_CONSUMER_GROUP} already exists")
357
+ logger.info(f"Consumer group {REDIS_CONSUMER_GROUP} already exists")
343
358
  else:
344
- print(f"Error creating consumer group: {e}")
345
- traceback.print_exc()
359
+ logger.error(f"Error creating consumer group: {e}")
360
+ logger.exception("Consumer Group Creation Error Traceback:")
346
361
 
347
362
 
348
363
  # Function to process messages
@@ -353,16 +368,16 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
353
368
 
354
369
  # --- Subprocess Execution Path ---
355
370
  if execution_mode == "subprocess":
356
- print(f"Processing message {message_id} in subprocess...")
371
+ logger.info(f"Processing message {message_id} in subprocess...")
357
372
  process = None # Initialize process variable
358
373
 
359
374
  # Helper function to read and print stream lines
360
375
  def stream_reader(stream: IO[str], prefix: str):
361
376
  try:
362
377
  for line in iter(stream.readline, ""):
363
- print(f"{prefix}: {line.strip()}", flush=True)
378
+ logger.debug(f"{prefix}: {line.strip()}")
364
379
  except Exception as e:
365
- print(f"Error reading stream {prefix}: {e}")
380
+ logger.error(f"Error reading stream {prefix}: {e}")
366
381
  finally:
367
382
  stream.close()
368
383
 
@@ -410,12 +425,12 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
410
425
  process.stdin.close() # Signal end of input
411
426
  except (BrokenPipeError, OSError) as e:
412
427
  # Handle cases where the process might have exited early
413
- print(
428
+ logger.warning(
414
429
  f"Warning: Failed to write full input to subprocess {message_id}: {e}. It might have exited prematurely."
415
430
  )
416
431
  # Continue to wait and check return code
417
432
  else:
418
- print(
433
+ logger.error(
419
434
  f"Error: Subprocess stdin stream not available for {message_id}. Cannot send input."
420
435
  )
421
436
  # Handle this case - perhaps terminate and report error?
@@ -431,19 +446,19 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
431
446
  stderr_thread.join()
432
447
 
433
448
  if return_code == 0:
434
- print(
449
+ logger.info(
435
450
  f"Subprocess for {message_id} completed successfully (return code 0)."
436
451
  )
437
452
  # Assume success handling (ack/response) was done by the worker
438
453
  elif return_code == 3:
439
- print(
454
+ logger.warning(
440
455
  f"Subprocess for {message_id} reported a retriable error (exit code 3). Message will not be acknowledged."
441
456
  )
442
457
  # Optionally send an error response here, though the worker already did.
443
458
  # _send_error_response(...)
444
459
  # DO NOT Acknowledge the message here, let it be retried.
445
460
  else:
446
- print(
461
+ logger.error(
447
462
  f"Subprocess for {message_id} failed with exit code {return_code}."
448
463
  )
449
464
  # Worker likely failed, send generic error and ACK here
@@ -459,14 +474,14 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
459
474
  assert isinstance(REDIS_STREAM, str)
460
475
  assert isinstance(REDIS_CONSUMER_GROUP, str)
461
476
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
462
- print(f"Acknowledged failed subprocess message {message_id}")
477
+ logger.info(f"Acknowledged failed subprocess message {message_id}")
463
478
  except Exception as e_ack:
464
- print(
479
+ logger.critical(
465
480
  f"CRITICAL: Failed to acknowledge failed subprocess message {message_id}: {e_ack}"
466
481
  )
467
482
 
468
483
  except FileNotFoundError:
469
- print(
484
+ logger.critical(
470
485
  "FATAL: Worker script 'nebu.processors.consumer_process_worker' not found. Check PYTHONPATH."
471
486
  )
472
487
  # Send error and ack if possible
@@ -481,19 +496,19 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
481
496
  assert isinstance(REDIS_STREAM, str)
482
497
  assert isinstance(REDIS_CONSUMER_GROUP, str)
483
498
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
484
- print(
499
+ logger.info(
485
500
  f"Acknowledged message {message_id} after worker script not found failure"
486
501
  )
487
502
  except Exception as e_ack:
488
- print(
503
+ logger.critical(
489
504
  f"CRITICAL: Failed to acknowledge message {message_id} after worker script not found failure: {e_ack}"
490
505
  )
491
506
 
492
507
  except Exception as e:
493
- print(
508
+ logger.error(
494
509
  f"Error launching or managing subprocess for message {message_id}: {e}"
495
510
  )
496
- traceback.print_exc()
511
+ logger.exception("Subprocess Launch/Manage Error Traceback:")
497
512
  # Also send an error and acknowledge
498
513
  _send_error_response(
499
514
  message_id,
@@ -506,22 +521,22 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
506
521
  assert isinstance(REDIS_STREAM, str)
507
522
  assert isinstance(REDIS_CONSUMER_GROUP, str)
508
523
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
509
- print(
524
+ logger.info(
510
525
  f"Acknowledged message {message_id} after subprocess launch/manage failure"
511
526
  )
512
527
  except Exception as e_ack:
513
- print(
528
+ logger.critical(
514
529
  f"CRITICAL: Failed to acknowledge message {message_id} after subprocess launch/manage failure: {e_ack}"
515
530
  )
516
531
  # Ensure process is terminated if it's still running after an error
517
532
  if process and process.poll() is None:
518
- print(
533
+ logger.warning(
519
534
  f"Terminating potentially lingering subprocess for {message_id}..."
520
535
  )
521
536
  process.terminate()
522
537
  process.wait(timeout=5) # Give it a moment to terminate
523
538
  if process.poll() is None:
524
- print(
539
+ logger.warning(
525
540
  f"Subprocess for {message_id} did not terminate gracefully, killing."
526
541
  )
527
542
  process.kill()
@@ -549,7 +564,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
549
564
 
550
565
  # --- Inline Execution Path (Original Logic) ---
551
566
  if target_function is None or imported_module is None:
552
- print(
567
+ logger.error(
553
568
  f"Error processing message {message_id}: User code (target_function or module) is not loaded. Skipping."
554
569
  )
555
570
  _send_error_response(
@@ -564,9 +579,11 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
564
579
  assert isinstance(REDIS_STREAM, str)
565
580
  assert isinstance(REDIS_CONSUMER_GROUP, str)
566
581
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
567
- print(f"Acknowledged message {message_id} due to code load failure.")
582
+ logger.warning(
583
+ f"Acknowledged message {message_id} due to code load failure."
584
+ )
568
585
  except Exception as e_ack:
569
- print(
586
+ logger.critical(
570
587
  f"CRITICAL: Failed to acknowledge message {message_id} after code load failure: {e_ack}"
571
588
  )
572
589
  return # Skip processing
@@ -590,7 +607,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
590
607
  f"Expected parsed payload to be a dictionary, but got {type(raw_payload)}"
591
608
  )
592
609
 
593
- print(f">> Raw payload: {raw_payload}")
610
+ logger.debug(f">> Raw payload: {raw_payload}")
594
611
 
595
612
  kind = raw_payload.get("kind", "")
596
613
  msg_id = raw_payload.get("id", "")
@@ -613,11 +630,11 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
613
630
  handle = raw_payload.get("handle")
614
631
  adapter = raw_payload.get("adapter")
615
632
  api_key = raw_payload.get("api_key")
616
- print(">> Extracted API key:", api_key)
633
+ logger.debug(f">> Extracted API key length: {len(api_key) if api_key else 0}")
617
634
 
618
635
  # --- Health Check Logic (Keep as is) ---
619
636
  if kind == "HealthCheck":
620
- print(f"Received HealthCheck message {message_id}")
637
+ logger.info(f"Received HealthCheck message {message_id}")
621
638
  health_response = {
622
639
  "kind": "StreamResponseMessage", # Respond with a standard message kind
623
640
  "id": message_id,
@@ -630,13 +647,13 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
630
647
  # Assert type again closer to usage for type checker clarity
631
648
  assert isinstance(return_stream, str)
632
649
  r.xadd(return_stream, {"data": json.dumps(health_response)})
633
- print(f"Sent health check response to {return_stream}")
650
+ logger.info(f"Sent health check response to {return_stream}")
634
651
 
635
652
  # Assert types again closer to usage for type checker clarity
636
653
  assert isinstance(REDIS_STREAM, str)
637
654
  assert isinstance(REDIS_CONSUMER_GROUP, str)
638
655
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
639
- print(f"Acknowledged HealthCheck message {message_id}")
656
+ logger.info(f"Acknowledged HealthCheck message {message_id}")
640
657
  return # Exit early for health checks
641
658
  # --- End Health Check Logic ---
642
659
 
@@ -649,7 +666,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
649
666
  else:
650
667
  content = content_raw
651
668
 
652
- print(f"Content: {content}")
669
+ # print(f"Content: {content}")
653
670
 
654
671
  # --- Construct Input Object using Imported Types ---
655
672
  input_obj: Any = None
@@ -677,24 +694,26 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
677
694
  # Check in local_namespace from included objects as fallback
678
695
  content_model_class = local_namespace.get(content_type_name)
679
696
  if content_model_class is None:
680
- print(
697
+ logger.warning(
681
698
  f"Warning: Content type class '{content_type_name}' not found in imported module or includes."
682
699
  )
683
700
  else:
684
- print(f"Found content model class: {content_model_class}")
701
+ logger.debug(
702
+ f"Found content model class: {content_model_class}"
703
+ )
685
704
  except AttributeError:
686
- print(
705
+ logger.warning(
687
706
  f"Warning: Content type class '{content_type_name}' not found in imported module."
688
707
  )
689
708
  except Exception as e:
690
- print(
709
+ logger.warning(
691
710
  f"Warning: Error resolving content type class '{content_type_name}': {e}"
692
711
  )
693
712
 
694
713
  if content_model_class:
695
714
  try:
696
715
  content_model = content_model_class.model_validate(content)
697
- print(f"Validated content model: {content_model}")
716
+ # print(f"Validated content model: {content_model}")
698
717
  input_obj = message_class(
699
718
  kind=kind,
700
719
  id=msg_id,
@@ -708,7 +727,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
708
727
  api_key=api_key,
709
728
  )
710
729
  except Exception as e:
711
- print(
730
+ logger.error(
712
731
  f"Error validating/creating content model '{content_type_name}': {e}. Falling back."
713
732
  )
714
733
  # Fallback to raw content in Message
@@ -754,53 +773,54 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
754
773
  input_type_class = local_namespace.get(param_type_name)
755
774
  if input_type_class is None:
756
775
  if param_type_name: # Only warn if a name was expected
757
- print(
776
+ logger.warning(
758
777
  f"Warning: Input type class '{param_type_name}' not found. Passing raw content."
759
778
  )
760
779
  input_obj = content
761
780
  else:
762
- print(f"Found input model class: {input_type_class}")
781
+ logger.debug(f"Found input model class: {input_type_class}")
763
782
  input_obj = input_type_class.model_validate(content)
764
- print(f"Validated input model: {input_obj}")
783
+ logger.debug(f"Validated input model: {input_obj}")
765
784
  except AttributeError:
766
- print(
785
+ logger.warning(
767
786
  f"Warning: Input type class '{param_type_name}' not found in imported module."
768
787
  )
769
788
  input_obj = content
770
789
  except Exception as e:
771
- print(
790
+ logger.error(
772
791
  f"Error resolving/validating input type '{param_type_name}': {e}. Passing raw content."
773
792
  )
774
793
  input_obj = content
775
794
 
776
795
  except NameError as e:
777
- print(
796
+ logger.error(
778
797
  f"Error: Required class (e.g., Message or parameter type) not found. Import failed? {e}"
779
798
  )
780
799
  # Can't proceed without types, re-raise or handle error response
781
800
  raise RuntimeError(f"Required class not found: {e}") from e
782
801
  except Exception as e:
783
- print(f"Error constructing input object: {e}")
802
+ logger.error(f"Error constructing input object: {e}")
784
803
  raise # Re-raise unexpected errors during input construction
785
804
 
786
805
  # print(f"Input object: {input_obj}") # Reduce verbosity
806
+ # logger.debug(f"Input object: {input_obj}") # Could use logger.debug if needed
787
807
 
788
808
  # Execute the function
789
- print("Executing function...")
809
+ logger.info("Executing function...")
790
810
  result = target_function(input_obj)
791
- # print(f"Raw Result: {result}") # Debugging
811
+ # logger.debug(f"Raw Result: {result}") # Debugging
792
812
 
793
813
  result_content = None # Default to None
794
814
  if result is not None: # Only process if there's a result
795
815
  try:
796
816
  if hasattr(result, "model_dump"):
797
- print("[Consumer] Result has model_dump, using it.")
817
+ logger.debug("[Consumer] Result has model_dump, using it.")
798
818
  # Use 'json' mode to ensure serializability where possible
799
819
  result_content = result.model_dump(mode="json")
800
- # print(f"[Consumer] Result after model_dump: {result_content}") # Debugging
820
+ # logger.debug(f"[Consumer] Result after model_dump: {result_content}") # Debugging
801
821
  else:
802
822
  # Try standard json.dumps as a fallback to check serializability
803
- print(
823
+ logger.debug(
804
824
  "[Consumer] Result has no model_dump, attempting json.dumps check."
805
825
  )
806
826
  try:
@@ -808,9 +828,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
808
828
  json.dumps(result)
809
829
  # If the above line doesn't raise TypeError, assign the original result
810
830
  result_content = result
811
- # print(f"[Consumer] Result assigned directly after json.dumps check passed: {result_content}") # Debugging
831
+ # logger.debug(f"[Consumer] Result assigned directly after json.dumps check passed: {result_content}") # Debugging
812
832
  except TypeError as e:
813
- print(
833
+ logger.warning(
814
834
  f"[Consumer] Warning: Result is not JSON serializable: {e}. Discarding result."
815
835
  )
816
836
  result_content = None # Explicitly set to None on failure
@@ -818,10 +838,10 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
818
838
  except (
819
839
  Exception
820
840
  ) as e: # Catch other potential model_dump errors or unexpected issues
821
- print(
841
+ logger.warning(
822
842
  f"[Consumer] Warning: Unexpected error during result processing/serialization: {e}. Discarding result."
823
843
  )
824
- traceback.print_exc()
844
+ logger.exception("Result Processing/Serialization Error Traceback:")
825
845
  result_content = None
826
846
 
827
847
  # Prepare the response (ensure 'content' key exists even if None)
@@ -840,7 +860,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
840
860
  if return_stream:
841
861
  assert isinstance(return_stream, str)
842
862
  r.xadd(return_stream, {"data": json.dumps(response)})
843
- print(f"Processed message {message_id}, result sent to {return_stream}")
863
+ logger.info(
864
+ f"Processed message {message_id}, result sent to {return_stream}"
865
+ )
844
866
 
845
867
  # Acknowledge the message
846
868
  assert isinstance(REDIS_STREAM, str)
@@ -848,17 +870,17 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
848
870
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
849
871
 
850
872
  except RetriableError as e:
851
- print(f"Retriable error processing message {message_id}: {e}")
852
- traceback.print_exc()
873
+ logger.warning(f"Retriable error processing message {message_id}: {e}")
874
+ logger.exception("Retriable Error Traceback:")
853
875
  _send_error_response(
854
876
  message_id, str(e), traceback.format_exc(), return_stream, user_id
855
877
  )
856
878
  # DO NOT Acknowledge the message for retriable errors
857
- print(f"Message {message_id} will be retried later.")
879
+ logger.info(f"Message {message_id} will be retried later.")
858
880
 
859
881
  except Exception as e:
860
- print(f"Error processing message {message_id}: {e}")
861
- traceback.print_exc()
882
+ logger.error(f"Error processing message {message_id}: {e}")
883
+ logger.exception("Message Processing Error Traceback:")
862
884
  _send_error_response(
863
885
  message_id, str(e), traceback.format_exc(), return_stream, user_id
864
886
  )
@@ -868,9 +890,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
868
890
  assert isinstance(REDIS_STREAM, str)
869
891
  assert isinstance(REDIS_CONSUMER_GROUP, str)
870
892
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
871
- print(f"Acknowledged failed message {message_id}")
893
+ logger.info(f"Acknowledged failed message {message_id}")
872
894
  except Exception as e_ack:
873
- print(
895
+ logger.critical(
874
896
  f"CRITICAL: Failed to acknowledge failed message {message_id}: {e_ack}"
875
897
  )
876
898
 
@@ -905,16 +927,20 @@ def _send_error_response(
905
927
  try:
906
928
  assert isinstance(error_destination, str)
907
929
  r.xadd(error_destination, {"data": json.dumps(error_response)})
908
- print(f"Sent error response for message {message_id} to {error_destination}")
930
+ logger.info(
931
+ f"Sent error response for message {message_id} to {error_destination}"
932
+ )
909
933
  except Exception as e_redis:
910
- print(
934
+ logger.critical(
911
935
  f"CRITICAL: Failed to send error response for {message_id} to Redis: {e_redis}"
912
936
  )
913
- traceback.print_exc()
937
+ logger.exception("Redis Error Response Send Error Traceback:")
914
938
 
915
939
 
916
940
  # Main loop
917
- print(f"Starting consumer for stream {REDIS_STREAM} in group {REDIS_CONSUMER_GROUP}")
941
+ logger.info(
942
+ f"Starting consumer for stream {REDIS_STREAM} in group {REDIS_CONSUMER_GROUP}"
943
+ )
918
944
  consumer_name = f"consumer-{os.getpid()}-{socket.gethostname()}" # More unique name
919
945
  MIN_IDLE_TIME_MS = 60000 # Minimum idle time in milliseconds (e.g., 60 seconds)
920
946
  CLAIM_COUNT = 10 # Max messages to claim at once
@@ -924,25 +950,25 @@ disable_hot_reload = os.environ.get("NEBU_DISABLE_HOT_RELOAD", "0").lower() in [
924
950
  "1",
925
951
  "true",
926
952
  ]
927
- print(
953
+ logger.info(
928
954
  f"[Consumer] Hot code reloading is {'DISABLED' if disable_hot_reload else 'ENABLED'}."
929
955
  )
930
956
 
931
957
  try:
932
958
  while True:
933
- print(
959
+ logger.debug(
934
960
  f"[{datetime.now(timezone.utc).isoformat()}] --- Top of main loop ---"
935
961
  ) # Added log
936
962
  # --- Check for Code Updates ---
937
963
  if not disable_hot_reload:
938
- print(
964
+ logger.debug(
939
965
  f"[{datetime.now(timezone.utc).isoformat()}] Checking for code updates..."
940
966
  ) # Added log
941
967
  if entrypoint_abs_path: # Should always be set after init
942
968
  try:
943
969
  current_mtime = os.path.getmtime(entrypoint_abs_path)
944
970
  if current_mtime > last_load_mtime:
945
- print(
971
+ logger.info(
946
972
  f"[Consumer] Detected change in entrypoint file: {entrypoint_abs_path}. Reloading code..."
947
973
  )
948
974
  (
@@ -963,7 +989,7 @@ try:
963
989
  reloaded_target_func is not None
964
990
  and reloaded_module is not None
965
991
  ):
966
- print(
992
+ logger.info(
967
993
  "[Consumer] Code reload successful. Updating functions."
968
994
  )
969
995
  target_function = reloaded_target_func
@@ -974,13 +1000,13 @@ try:
974
1000
  )
975
1001
  last_load_mtime = new_mtime
976
1002
  else:
977
- print(
1003
+ logger.warning(
978
1004
  "[Consumer] Code reload failed. Continuing with previously loaded code."
979
1005
  )
980
1006
  # Optionally: Send an alert/log prominently that reload failed
981
1007
 
982
1008
  except FileNotFoundError:
983
- print(
1009
+ logger.error(
984
1010
  f"[Consumer] Error: Entrypoint file '{entrypoint_abs_path}' not found during check. Cannot reload."
985
1011
  )
986
1012
  # Mark as non-runnable? Or just log?
@@ -988,23 +1014,25 @@ try:
988
1014
  imported_module = None
989
1015
  last_load_mtime = 0 # Reset mtime to force check next time
990
1016
  except Exception as e_reload_check:
991
- print(f"[Consumer] Error checking/reloading code: {e_reload_check}")
992
- traceback.print_exc()
1017
+ logger.error(
1018
+ f"[Consumer] Error checking/reloading code: {e_reload_check}"
1019
+ )
1020
+ logger.exception("Code Reload Check Error Traceback:")
993
1021
  else:
994
- print(
1022
+ logger.warning(
995
1023
  "[Consumer] Warning: Entrypoint absolute path not set, cannot check for code updates."
996
1024
  )
997
- print(
1025
+ logger.debug(
998
1026
  f"[{datetime.now(timezone.utc).isoformat()}] Finished checking for code updates."
999
1027
  ) # Added log
1000
1028
  else:
1001
1029
  # Log that hot reload is skipped if it's disabled
1002
- print(
1030
+ logger.debug(
1003
1031
  f"[{datetime.now(timezone.utc).isoformat()}] Hot reload check skipped (NEBU_DISABLE_HOT_RELOAD=1)."
1004
1032
  )
1005
1033
 
1006
1034
  # --- Claim Old Pending Messages ---
1007
- print(
1035
+ logger.debug(
1008
1036
  f"[{datetime.now(timezone.utc).isoformat()}] Checking for pending messages to claim..."
1009
1037
  ) # Added log
1010
1038
  try:
@@ -1063,67 +1091,71 @@ try:
1063
1091
  claimed_messages = [(REDIS_STREAM, claimed_messages_list)]
1064
1092
 
1065
1093
  if claimed_messages:
1066
- print(
1067
- f"[{datetime.now(timezone.utc).isoformat()}] Claimed {claimed_messages} pending message(s). Processing..."
1068
- )
1069
1094
  # Process claimed messages immediately
1070
1095
  # Cast messages to expected type to satisfy type checker
1071
1096
  typed_messages = cast(
1072
1097
  List[Tuple[str, List[Tuple[str, Dict[str, str]]]]],
1073
1098
  claimed_messages,
1074
1099
  )
1100
+ # Log after casting and before processing
1101
+ num_claimed = len(typed_messages[0][1]) if typed_messages else 0
1102
+ logger.info(
1103
+ f"[{datetime.now(timezone.utc).isoformat()}] Claimed {num_claimed} pending message(s). Processing..."
1104
+ )
1075
1105
  stream_name_str, stream_messages = typed_messages[0]
1076
1106
  for (
1077
1107
  message_id_str,
1078
1108
  message_data_str_dict,
1079
1109
  ) in stream_messages:
1080
- print(f"[Consumer] Processing claimed message {message_id_str}")
1110
+ logger.info(
1111
+ f"[Consumer] Processing claimed message {message_id_str}"
1112
+ )
1081
1113
  process_message(message_id_str, message_data_str_dict)
1082
1114
  # After processing claimed messages, loop back to check for more potentially
1083
1115
  # This avoids immediately blocking on XREADGROUP if there were claimed messages
1084
1116
  continue
1085
1117
  else: # Added log
1086
- print(
1118
+ logger.debug(
1087
1119
  f"[{datetime.now(timezone.utc).isoformat()}] No pending messages claimed."
1088
1120
  ) # Added log
1089
1121
 
1090
1122
  except ResponseError as e_claim:
1091
1123
  # Handle specific errors like NOGROUP gracefully if needed
1092
1124
  if "NOGROUP" in str(e_claim):
1093
- print(
1125
+ logger.critical(
1094
1126
  f"Consumer group {REDIS_CONSUMER_GROUP} not found during xautoclaim. Exiting."
1095
1127
  )
1096
1128
  sys.exit(1)
1097
1129
  else:
1098
- print(f"[Consumer] Error during XAUTOCLAIM: {e_claim}")
1130
+ logger.error(f"[Consumer] Error during XAUTOCLAIM: {e_claim}")
1099
1131
  # Decide if this is fatal or recoverable
1100
- print(
1132
+ logger.error(
1101
1133
  f"[{datetime.now(timezone.utc).isoformat()}] Error during XAUTOCLAIM: {e_claim}"
1102
1134
  ) # Added log
1103
1135
  time.sleep(5) # Wait before retrying claim
1104
1136
  except ConnectionError as e_claim_conn:
1105
- print(
1137
+ logger.error(
1106
1138
  f"Redis connection error during XAUTOCLAIM: {e_claim_conn}. Will attempt reconnect in main loop."
1107
1139
  )
1108
1140
  # Let the main ConnectionError handler below deal with reconnection
1109
- print(
1141
+ logger.error(
1110
1142
  f"[{datetime.now(timezone.utc).isoformat()}] Redis connection error during XAUTOCLAIM: {e_claim_conn}. Will attempt reconnect."
1111
1143
  ) # Added log
1112
1144
  time.sleep(5) # Avoid tight loop on connection errors during claim
1113
1145
  except Exception as e_claim_other:
1114
- print(
1146
+ logger.error(
1115
1147
  f"[Consumer] Unexpected error during XAUTOCLAIM/processing claimed messages: {e_claim_other}"
1116
1148
  )
1117
- print(
1149
+ logger.error(
1118
1150
  f"[{datetime.now(timezone.utc).isoformat()}] Unexpected error during XAUTOCLAIM/processing claimed: {e_claim_other}"
1119
1151
  ) # Added log
1120
- traceback.print_exc()
1152
+ logger.exception("XAUTOCLAIM/Processing Error Traceback:")
1121
1153
  time.sleep(5) # Wait before retrying
1122
1154
 
1123
1155
  # --- Read New Messages from Redis Stream ---
1124
1156
  if target_function is None:
1125
1157
  # If code failed to load initially or during reload, wait before retrying
1126
- print(
1158
+ logger.warning(
1127
1159
  "[Consumer] Target function not loaded, waiting 5s before checking again..."
1128
1160
  )
1129
1161
  time.sleep(5)
@@ -1135,7 +1167,7 @@ try:
1135
1167
  streams_arg: Dict[str, str] = {REDIS_STREAM: ">"}
1136
1168
 
1137
1169
  # With decode_responses=True, redis-py expects str types here
1138
- print(
1170
+ logger.debug(
1139
1171
  f"[{datetime.now(timezone.utc).isoformat()}] Calling xreadgroup (block=5000ms)..."
1140
1172
  ) # Added log
1141
1173
  messages = r.xreadgroup(
@@ -1147,10 +1179,10 @@ try:
1147
1179
  )
1148
1180
 
1149
1181
  if not messages:
1150
- print(
1182
+ logger.trace(
1151
1183
  f"[{datetime.now(timezone.utc).isoformat()}] xreadgroup timed out (no new messages)."
1152
1184
  ) # Added log
1153
- # print("[Consumer] No new messages.") # Reduce verbosity
1185
+ # logger.debug("[Consumer] No new messages.") # Reduce verbosity
1154
1186
  continue
1155
1187
  # Removed the else block here
1156
1188
 
@@ -1166,7 +1198,7 @@ try:
1166
1198
  num_msgs = len(stream_messages)
1167
1199
 
1168
1200
  # Log reception and count before processing
1169
- print(
1201
+ logger.info(
1170
1202
  f"[{datetime.now(timezone.utc).isoformat()}] xreadgroup returned {num_msgs} message(s). Processing..."
1171
1203
  ) # Moved and combined log
1172
1204
 
@@ -1185,32 +1217,32 @@ try:
1185
1217
  process_message(message_id_str, message_data_str_dict)
1186
1218
 
1187
1219
  except ConnectionError as e:
1188
- print(f"Redis connection error: {e}. Reconnecting in 5s...")
1220
+ logger.error(f"Redis connection error: {e}. Reconnecting in 5s...")
1189
1221
  time.sleep(5)
1190
1222
  # Attempt to reconnect explicitly
1191
1223
  try:
1192
- print("Attempting Redis reconnection...")
1224
+ logger.info("Attempting Redis reconnection...")
1193
1225
  # Close existing potentially broken connection? `r.close()` if available
1194
1226
  r = redis.from_url(REDIS_URL, decode_responses=True)
1195
1227
  r.ping()
1196
- print("Reconnected to Redis.")
1228
+ logger.info("Reconnected to Redis.")
1197
1229
  except Exception as recon_e:
1198
- print(f"Failed to reconnect to Redis: {recon_e}")
1230
+ logger.error(f"Failed to reconnect to Redis: {recon_e}")
1199
1231
  # Keep waiting
1200
1232
 
1201
1233
  except ResponseError as e:
1202
- print(f"Redis command error: {e}")
1234
+ logger.error(f"Redis command error: {e}")
1203
1235
  # Should we exit or retry?
1204
1236
  if "NOGROUP" in str(e):
1205
- print("Consumer group seems to have disappeared. Exiting.")
1237
+ logger.critical("Consumer group seems to have disappeared. Exiting.")
1206
1238
  sys.exit(1)
1207
1239
  time.sleep(1)
1208
1240
 
1209
1241
  except Exception as e:
1210
- print(f"Unexpected error in main loop: {e}")
1211
- traceback.print_exc()
1242
+ logger.error(f"Unexpected error in main loop: {e}")
1243
+ logger.exception("Main Loop Error Traceback:")
1212
1244
  time.sleep(1)
1213
1245
 
1214
1246
  finally:
1215
- print("Consumer loop exited.")
1247
+ logger.info("Consumer loop exited.")
1216
1248
  # Any other cleanup needed?