aiqa-client 0.3.7__tar.gz → 0.4.0__tar.gz

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.
Files changed (22) hide show
  1. {aiqa_client-0.3.7/aiqa_client.egg-info → aiqa_client-0.4.0}/PKG-INFO +1 -1
  2. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/__init__.py +2 -3
  3. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/aiqa_exporter.py +55 -7
  4. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/client.py +5 -8
  5. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/constants.py +1 -0
  6. {aiqa_client-0.3.7 → aiqa_client-0.4.0/aiqa_client.egg-info}/PKG-INFO +1 -1
  7. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/pyproject.toml +1 -1
  8. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/LICENSE +0 -0
  9. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/MANIFEST.in +0 -0
  10. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/README.md +0 -0
  11. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/experiment_runner.py +0 -0
  12. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/object_serialiser.py +0 -0
  13. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/py.typed +0 -0
  14. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/test_experiment_runner.py +0 -0
  15. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/test_tracing.py +0 -0
  16. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa/tracing.py +0 -0
  17. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa_client.egg-info/SOURCES.txt +0 -0
  18. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa_client.egg-info/dependency_links.txt +0 -0
  19. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa_client.egg-info/requires.txt +0 -0
  20. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/aiqa_client.egg-info/top_level.txt +0 -0
  21. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/setup.cfg +0 -0
  22. {aiqa_client-0.3.7 → aiqa_client-0.4.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiqa-client
3
- Version: 0.3.7
3
+ Version: 0.4.0
4
4
  Summary: OpenTelemetry-based Python client for tracing functions and sending traces to the AIQA server
5
5
  Author-email: AIQA <info@aiqa.dev>
6
6
  License: MIT
@@ -36,8 +36,7 @@ from .tracing import (
36
36
  )
37
37
  from .client import get_aiqa_client
38
38
  from .experiment_runner import ExperimentRunner
39
-
40
- __version__ = "0.3.7"
39
+ from .constants import VERSION
41
40
 
42
41
  __all__ = [
43
42
  "WithTracing",
@@ -55,6 +54,6 @@ __all__ = [
55
54
  "set_conversation_id",
56
55
  "set_component_tag",
57
56
  "get_span",
58
- "__version__",
57
+ "VERSION",
59
58
  ]
60
59
 
@@ -9,12 +9,12 @@ import logging
9
9
  import threading
10
10
  import time
11
11
  import io
12
+ import asyncio
12
13
  from typing import List, Dict, Any, Optional
13
14
  from opentelemetry.sdk.trace import ReadableSpan
14
15
  from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
15
16
 
16
- from .constants import AIQA_TRACER_NAME
17
- from . import __version__
17
+ from .constants import AIQA_TRACER_NAME, VERSION
18
18
 
19
19
  logger = logging.getLogger("AIQA")
20
20
 
@@ -32,6 +32,7 @@ class AIQASpanExporter(SpanExporter):
32
32
  flush_interval_seconds: float = 5.0,
33
33
  max_batch_size_bytes: int = 5 * 1024 * 1024, # 5MB default
34
34
  max_buffer_spans: int = 10000, # Maximum spans to buffer (prevents unbounded growth)
35
+ startup_delay_seconds: Optional[float] = None,
35
36
  ):
36
37
  """
37
38
  Initialize the AIQA span exporter.
@@ -41,12 +42,28 @@ class AIQASpanExporter(SpanExporter):
41
42
  api_key: API key for authentication (defaults to AIQA_API_KEY env var)
42
43
  flush_interval_seconds: How often to flush spans to the server
43
44
  max_batch_size_bytes: Maximum size of a single batch in bytes (default: 5mb)
45
+ max_buffer_spans: Maximum spans to buffer (prevents unbounded growth)
46
+ startup_delay_seconds: Delay before starting auto-flush (default: 10s, or AIQA_STARTUP_DELAY_SECONDS env var)
44
47
  """
45
48
  self._server_url = server_url
46
49
  self._api_key = api_key
47
50
  self.flush_interval_ms = flush_interval_seconds * 1000
48
51
  self.max_batch_size_bytes = max_batch_size_bytes
49
52
  self.max_buffer_spans = max_buffer_spans
53
+
54
+ # Get startup delay from parameter or environment variable (default: 10s)
55
+ if startup_delay_seconds is None:
56
+ env_delay = os.getenv("AIQA_STARTUP_DELAY_SECONDS")
57
+ if env_delay:
58
+ try:
59
+ startup_delay_seconds = float(env_delay)
60
+ except ValueError:
61
+ logger.warning(f"Invalid AIQA_STARTUP_DELAY_SECONDS value '{env_delay}', using default 10.0")
62
+ startup_delay_seconds = 10.0
63
+ else:
64
+ startup_delay_seconds = 10.0
65
+ self.startup_delay_seconds = startup_delay_seconds
66
+
50
67
  self.buffer: List[Dict[str, Any]] = []
51
68
  self.buffer_span_keys: set = set() # Track (traceId, spanId) tuples to prevent duplicates (Python 3.8 compatible)
52
69
  self.buffer_lock = threading.Lock()
@@ -56,7 +73,7 @@ class AIQASpanExporter(SpanExporter):
56
73
 
57
74
  logger.info(
58
75
  f"Initializing AIQASpanExporter: server_url={self.server_url or 'not set'}, "
59
- f"flush_interval={flush_interval_seconds}s"
76
+ f"flush_interval={flush_interval_seconds}s, startup_delay={startup_delay_seconds}s"
60
77
  )
61
78
  self._start_auto_flush()
62
79
 
@@ -198,7 +215,7 @@ class AIQASpanExporter(SpanExporter):
198
215
  """
199
216
  return {
200
217
  "name": AIQA_TRACER_NAME,
201
- "version": __version__,
218
+ "version": VERSION,
202
219
  }
203
220
 
204
221
  def _time_to_tuple(self, nanoseconds: int) -> tuple:
@@ -377,16 +394,38 @@ class AIQASpanExporter(SpanExporter):
377
394
  raise
378
395
 
379
396
  def _start_auto_flush(self) -> None:
380
- """Start the auto-flush timer."""
397
+ """Start the auto-flush timer with startup delay."""
381
398
  if self.shutdown_requested:
382
399
  logger.warning("_start_auto_flush() called but shutdown already requested")
383
400
  return
384
401
 
385
- logger.info(f"Starting auto-flush thread with interval {self.flush_interval_ms / 1000.0}s")
402
+ logger.info(
403
+ f"Starting auto-flush thread with interval {self.flush_interval_ms / 1000.0}s, "
404
+ f"startup delay {self.startup_delay_seconds}s"
405
+ )
386
406
 
387
407
  def flush_worker():
388
408
  import asyncio
389
409
  logger.debug("Auto-flush worker thread started")
410
+
411
+ # Wait for startup delay before beginning flush operations
412
+ # This gives the container/application time to stabilize, which helps avoid startup issues (seen with AWS ECS, Dec 2025).
413
+ if self.startup_delay_seconds > 0:
414
+ logger.info(f"Auto-flush waiting {self.startup_delay_seconds}s before first flush (startup delay)")
415
+ # Sleep in small increments to allow for early shutdown
416
+ sleep_interval = 0.5
417
+ remaining_delay = self.startup_delay_seconds
418
+ while remaining_delay > 0 and not self.shutdown_requested:
419
+ sleep_time = min(sleep_interval, remaining_delay)
420
+ time.sleep(sleep_time)
421
+ remaining_delay -= sleep_time
422
+
423
+ if self.shutdown_requested:
424
+ logger.debug("Auto-flush startup delay interrupted by shutdown")
425
+ return
426
+
427
+ logger.info("Auto-flush startup delay complete, beginning flush operations")
428
+
390
429
  loop = asyncio.new_event_loop()
391
430
  asyncio.set_event_loop(loop)
392
431
 
@@ -439,8 +478,10 @@ class AIQASpanExporter(SpanExporter):
439
478
  else:
440
479
  logger.debug("_send_spans() no API key provided")
441
480
 
481
+ # Use timeout to prevent hanging on unreachable servers
482
+ timeout = aiohttp.ClientTimeout(total=30.0, connect=10.0)
442
483
  errors = []
443
- async with aiohttp.ClientSession() as session:
484
+ async with aiohttp.ClientSession(timeout=timeout) as session:
444
485
  for batch_idx, batch in enumerate(batches):
445
486
  try:
446
487
  logger.debug(f"_send_spans() sending batch {batch_idx + 1}/{len(batches)} with {len(batch)} spans to {url}")
@@ -458,6 +499,12 @@ class AIQASpanExporter(SpanExporter):
458
499
  # Continue with other batches even if one fails
459
500
  continue
460
501
  logger.debug(f"_send_spans() batch {batch_idx + 1} successfully sent {len(batch)} spans")
502
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
503
+ # Network errors and timeouts - log but don't fail completely
504
+ error_msg = f"Network error in batch {batch_idx + 1}: {type(e).__name__}: {e}"
505
+ logger.warning(f"_send_spans() {error_msg} - will retry on next flush")
506
+ errors.append((batch_idx + 1, error_msg))
507
+ # Continue with other batches
461
508
  except RuntimeError as e:
462
509
  if self._is_interpreter_shutdown_error(e):
463
510
  if self.shutdown_requested:
@@ -476,6 +523,7 @@ class AIQASpanExporter(SpanExporter):
476
523
  # Continue with other batches
477
524
 
478
525
  # If any batches failed, raise an exception with details
526
+ # Spans will be restored to buffer for retry on next flush
479
527
  if errors:
480
528
  error_summary = "; ".join([f"batch {idx}: {msg}" for idx, msg in errors])
481
529
  raise Exception(f"Failed to send some spans: {error_summary}")
@@ -7,9 +7,6 @@ from opentelemetry import trace
7
7
  from opentelemetry.sdk.trace import TracerProvider
8
8
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
9
9
 
10
- if TYPE_CHECKING:
11
- from .aiqa_exporter import AIQASpanExporter
12
-
13
10
  logger = logging.getLogger("AIQA")
14
11
 
15
12
  # Compatibility import for TraceIdRatioBased sampler
@@ -46,7 +43,7 @@ class AIQAClient:
46
43
  if cls._instance is None:
47
44
  cls._instance = super().__new__(cls)
48
45
  cls._instance._provider: Optional[TracerProvider] = None
49
- cls._instance._exporter: Optional[AIQASpanExporter] = None
46
+ cls._instance._exporter = None # reduce circular import issues by not importing for typecheck here
50
47
  cls._instance._enabled: bool = True
51
48
  cls._instance._initialized: bool = False
52
49
  return cls._instance
@@ -62,12 +59,12 @@ class AIQAClient:
62
59
  self._provider = value
63
60
 
64
61
  @property
65
- def exporter(self) -> Optional[AIQASpanExporter]:
62
+ def exporter(self) -> Optional[Any]:
66
63
  """Get the span exporter."""
67
64
  return self._exporter
68
65
 
69
66
  @exporter.setter
70
- def exporter(self, value: Optional[AIQASpanExporter]) -> None:
67
+ def exporter(self, value: Optional[Any]) -> None:
71
68
  """Set the span exporter."""
72
69
  self._exporter = value
73
70
 
@@ -252,10 +249,10 @@ def get_aiqa_tracer() -> trace.Tracer:
252
249
  """
253
250
  try:
254
251
  # Import here to avoid circular import
255
- from . import __version__
252
+ from . import VERSION
256
253
  # Compatibility: version parameter may not be supported in older OpenTelemetry versions
257
254
  # Try with version parameter (newer OpenTelemetry versions)
258
- return trace.get_tracer(AIQA_TRACER_NAME, version=__version__)
255
+ return trace.get_tracer(AIQA_TRACER_NAME, version=VERSION)
259
256
  except Exception as e:
260
257
  # Log issue but still return a tracer
261
258
  logger.info(f"Issue getting AIQA tracer with version: {e}, using fallback")
@@ -3,3 +3,4 @@ Constants used across the AIQA client package.
3
3
  """
4
4
 
5
5
  AIQA_TRACER_NAME = "aiqa-tracer"
6
+ VERSION = "0.4.0" # automatically updated by set-version-json.sh
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiqa-client
3
- Version: 0.3.7
3
+ Version: 0.4.0
4
4
  Summary: OpenTelemetry-based Python client for tracing functions and sending traces to the AIQA server
5
5
  Author-email: AIQA <info@aiqa.dev>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aiqa-client"
7
- version = "0.3.7"
7
+ version = "0.4.0"
8
8
  description = "OpenTelemetry-based Python client for tracing functions and sending traces to the AIQA server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes