aiqa-client 0.3.7__py3-none-any.whl → 0.4.0__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.
- aiqa/__init__.py +2 -3
- aiqa/aiqa_exporter.py +55 -7
- aiqa/client.py +5 -8
- aiqa/constants.py +1 -0
- {aiqa_client-0.3.7.dist-info → aiqa_client-0.4.0.dist-info}/METADATA +1 -1
- aiqa_client-0.4.0.dist-info/RECORD +15 -0
- aiqa_client-0.3.7.dist-info/RECORD +0 -15
- {aiqa_client-0.3.7.dist-info → aiqa_client-0.4.0.dist-info}/WHEEL +0 -0
- {aiqa_client-0.3.7.dist-info → aiqa_client-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {aiqa_client-0.3.7.dist-info → aiqa_client-0.4.0.dist-info}/top_level.txt +0 -0
aiqa/__init__.py
CHANGED
|
@@ -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
|
-
"
|
|
57
|
+
"VERSION",
|
|
59
58
|
]
|
|
60
59
|
|
aiqa/aiqa_exporter.py
CHANGED
|
@@ -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":
|
|
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(
|
|
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}")
|
aiqa/client.py
CHANGED
|
@@ -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
|
|
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[
|
|
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[
|
|
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
|
|
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=
|
|
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")
|
aiqa/constants.py
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
aiqa/__init__.py,sha256=MZGvPF4XM_EuBRiiAR6EA1bzctLpzmE6crcjuh3Ve6o,1459
|
|
2
|
+
aiqa/aiqa_exporter.py,sha256=-yPJscH0Wc9yIVetwJiOAiwEqFQnL6AYXo_FwsoYGaE,30482
|
|
3
|
+
aiqa/client.py,sha256=wYoVvOHoGnkc3qsEHL5vMRW13hOFPR2d9s_MPKGAbpE,9538
|
|
4
|
+
aiqa/constants.py,sha256=hXRiXeNgAqLOizNeSgucSAMkFO0wGMtpZ2qjKhUWWhA,153
|
|
5
|
+
aiqa/experiment_runner.py,sha256=ZEDwECstAv4lWXpcdB9WSxfDQj43iqkGzB_YzoY933M,12053
|
|
6
|
+
aiqa/object_serialiser.py,sha256=pgcBVw5sZH8f7N6n3-qOvEcbNhuPS5yq7qdhaNT6Sks,15236
|
|
7
|
+
aiqa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
aiqa/test_experiment_runner.py,sha256=LM8BuCrzBZL0Wyu_ierK0tNLsOUxxMTAHbAGW2G0qp0,5562
|
|
9
|
+
aiqa/test_tracing.py,sha256=mSVrhRQ6Dz5djlSUkCt097sIr84562w6E0BnuQDpMrI,8347
|
|
10
|
+
aiqa/tracing.py,sha256=SsuK6WNgk3LbWt1aQwPPIDhitBmtyU6GOsMRvouXpDw,49706
|
|
11
|
+
aiqa_client-0.4.0.dist-info/licenses/LICENSE,sha256=kIzkzLuzG0HHaWYm4F4W5FeJ1Yxut3Ec6bhLWyw798A,1062
|
|
12
|
+
aiqa_client-0.4.0.dist-info/METADATA,sha256=toA-KJzaC0mlWOsYhqHqj83ASP83sox_7wowMczaxrE,7505
|
|
13
|
+
aiqa_client-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
aiqa_client-0.4.0.dist-info/top_level.txt,sha256=nwcsuVVSuWu27iLxZd4n1evVzv1W6FVTrSnCXCc-NQs,5
|
|
15
|
+
aiqa_client-0.4.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
aiqa/__init__.py,sha256=dnCT31jRL0nUeSvHksUris3_7lxlxChfzshxY7_gHlk,1455
|
|
2
|
-
aiqa/aiqa_exporter.py,sha256=MbA7SkJoNm03dvrcGteU57Y0YNpVw8fzL_W-RI2lI0Q,27698
|
|
3
|
-
aiqa/client.py,sha256=TolaBb7ZnnD5SawclI9KMBsdGKaAosTxvLVeGlKseAA,9599
|
|
4
|
-
aiqa/constants.py,sha256=3QLmyhyVayKebM5N50P1oYbI0LtQmqxTp17UZnUeixc,89
|
|
5
|
-
aiqa/experiment_runner.py,sha256=ZEDwECstAv4lWXpcdB9WSxfDQj43iqkGzB_YzoY933M,12053
|
|
6
|
-
aiqa/object_serialiser.py,sha256=pgcBVw5sZH8f7N6n3-qOvEcbNhuPS5yq7qdhaNT6Sks,15236
|
|
7
|
-
aiqa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
aiqa/test_experiment_runner.py,sha256=LM8BuCrzBZL0Wyu_ierK0tNLsOUxxMTAHbAGW2G0qp0,5562
|
|
9
|
-
aiqa/test_tracing.py,sha256=mSVrhRQ6Dz5djlSUkCt097sIr84562w6E0BnuQDpMrI,8347
|
|
10
|
-
aiqa/tracing.py,sha256=SsuK6WNgk3LbWt1aQwPPIDhitBmtyU6GOsMRvouXpDw,49706
|
|
11
|
-
aiqa_client-0.3.7.dist-info/licenses/LICENSE,sha256=kIzkzLuzG0HHaWYm4F4W5FeJ1Yxut3Ec6bhLWyw798A,1062
|
|
12
|
-
aiqa_client-0.3.7.dist-info/METADATA,sha256=LSshQdYneT3PfZIy19TwBt6Ow8L7IlJYHmx3EPH32pg,7505
|
|
13
|
-
aiqa_client-0.3.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
aiqa_client-0.3.7.dist-info/top_level.txt,sha256=nwcsuVVSuWu27iLxZd4n1evVzv1W6FVTrSnCXCc-NQs,5
|
|
15
|
-
aiqa_client-0.3.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|