aiqa-client 0.3.6__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 +3 -11
- aiqa/aiqa_exporter.py +79 -21
- aiqa/client.py +43 -68
- aiqa/constants.py +6 -0
- aiqa/tracing.py +4 -27
- {aiqa_client-0.3.6.dist-info → aiqa_client-0.4.0.dist-info}/METADATA +5 -5
- aiqa_client-0.4.0.dist-info/RECORD +15 -0
- aiqa_client-0.3.6.dist-info/RECORD +0 -14
- {aiqa_client-0.3.6.dist-info → aiqa_client-0.4.0.dist-info}/WHEEL +0 -0
- {aiqa_client-0.3.6.dist-info → aiqa_client-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {aiqa_client-0.3.6.dist-info → aiqa_client-0.4.0.dist-info}/top_level.txt +0 -0
aiqa/__init__.py
CHANGED
|
@@ -22,12 +22,9 @@ Example:
|
|
|
22
22
|
from .tracing import (
|
|
23
23
|
WithTracing,
|
|
24
24
|
flush_tracing,
|
|
25
|
-
shutdown_tracing,
|
|
26
25
|
set_span_attribute,
|
|
27
26
|
set_span_name,
|
|
28
27
|
get_active_span,
|
|
29
|
-
get_provider,
|
|
30
|
-
get_exporter,
|
|
31
28
|
get_active_trace_id,
|
|
32
29
|
get_span_id,
|
|
33
30
|
create_span_from_trace_id,
|
|
@@ -37,22 +34,17 @@ from .tracing import (
|
|
|
37
34
|
set_component_tag,
|
|
38
35
|
get_span,
|
|
39
36
|
)
|
|
40
|
-
from .client import get_aiqa_client
|
|
37
|
+
from .client import get_aiqa_client
|
|
41
38
|
from .experiment_runner import ExperimentRunner
|
|
42
|
-
|
|
43
|
-
__version__ = "0.3.6"
|
|
39
|
+
from .constants import VERSION
|
|
44
40
|
|
|
45
41
|
__all__ = [
|
|
46
42
|
"WithTracing",
|
|
47
43
|
"flush_tracing",
|
|
48
|
-
"shutdown_tracing",
|
|
49
44
|
"set_span_attribute",
|
|
50
45
|
"set_span_name",
|
|
51
46
|
"get_active_span",
|
|
52
|
-
"get_provider",
|
|
53
|
-
"get_exporter",
|
|
54
47
|
"get_aiqa_client",
|
|
55
|
-
"set_enabled",
|
|
56
48
|
"ExperimentRunner",
|
|
57
49
|
"get_active_trace_id",
|
|
58
50
|
"get_span_id",
|
|
@@ -62,6 +54,6 @@ __all__ = [
|
|
|
62
54
|
"set_conversation_id",
|
|
63
55
|
"set_component_tag",
|
|
64
56
|
"get_span",
|
|
65
|
-
"
|
|
57
|
+
"VERSION",
|
|
66
58
|
]
|
|
67
59
|
|
aiqa/aiqa_exporter.py
CHANGED
|
@@ -9,10 +9,13 @@ 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
|
|
|
17
|
+
from .constants import AIQA_TRACER_NAME, VERSION
|
|
18
|
+
|
|
16
19
|
logger = logging.getLogger("AIQA")
|
|
17
20
|
|
|
18
21
|
|
|
@@ -28,6 +31,8 @@ class AIQASpanExporter(SpanExporter):
|
|
|
28
31
|
api_key: Optional[str] = None,
|
|
29
32
|
flush_interval_seconds: float = 5.0,
|
|
30
33
|
max_batch_size_bytes: int = 5 * 1024 * 1024, # 5MB default
|
|
34
|
+
max_buffer_spans: int = 10000, # Maximum spans to buffer (prevents unbounded growth)
|
|
35
|
+
startup_delay_seconds: Optional[float] = None,
|
|
31
36
|
):
|
|
32
37
|
"""
|
|
33
38
|
Initialize the AIQA span exporter.
|
|
@@ -37,11 +42,28 @@ class AIQASpanExporter(SpanExporter):
|
|
|
37
42
|
api_key: API key for authentication (defaults to AIQA_API_KEY env var)
|
|
38
43
|
flush_interval_seconds: How often to flush spans to the server
|
|
39
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)
|
|
40
47
|
"""
|
|
41
48
|
self._server_url = server_url
|
|
42
49
|
self._api_key = api_key
|
|
43
50
|
self.flush_interval_ms = flush_interval_seconds * 1000
|
|
44
51
|
self.max_batch_size_bytes = max_batch_size_bytes
|
|
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
|
+
|
|
45
67
|
self.buffer: List[Dict[str, Any]] = []
|
|
46
68
|
self.buffer_span_keys: set = set() # Track (traceId, spanId) tuples to prevent duplicates (Python 3.8 compatible)
|
|
47
69
|
self.buffer_lock = threading.Lock()
|
|
@@ -51,7 +73,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
51
73
|
|
|
52
74
|
logger.info(
|
|
53
75
|
f"Initializing AIQASpanExporter: server_url={self.server_url or 'not set'}, "
|
|
54
|
-
f"flush_interval={flush_interval_seconds}s"
|
|
76
|
+
f"flush_interval={flush_interval_seconds}s, startup_delay={startup_delay_seconds}s"
|
|
55
77
|
)
|
|
56
78
|
self._start_auto_flush()
|
|
57
79
|
|
|
@@ -88,7 +110,13 @@ class AIQASpanExporter(SpanExporter):
|
|
|
88
110
|
with self.buffer_lock:
|
|
89
111
|
serialized_spans = []
|
|
90
112
|
duplicates_count = 0
|
|
113
|
+
dropped_count = 0
|
|
91
114
|
for span in spans:
|
|
115
|
+
# Check if buffer is full (prevent unbounded growth)
|
|
116
|
+
if len(self.buffer) >= self.max_buffer_spans:
|
|
117
|
+
dropped_count += 1
|
|
118
|
+
continue
|
|
119
|
+
|
|
92
120
|
serialized = self._serialize_span(span)
|
|
93
121
|
span_key = (serialized["traceId"], serialized["spanId"])
|
|
94
122
|
if span_key not in self.buffer_span_keys:
|
|
@@ -100,6 +128,12 @@ class AIQASpanExporter(SpanExporter):
|
|
|
100
128
|
|
|
101
129
|
self.buffer.extend(serialized_spans)
|
|
102
130
|
buffer_size = len(self.buffer)
|
|
131
|
+
|
|
132
|
+
if dropped_count > 0:
|
|
133
|
+
logger.warning(
|
|
134
|
+
f"WARNING: Buffer full ({buffer_size} spans), dropped {dropped_count} span(s). "
|
|
135
|
+
f"Consider increasing max_buffer_spans or fixing server connectivity."
|
|
136
|
+
)
|
|
103
137
|
|
|
104
138
|
if duplicates_count > 0:
|
|
105
139
|
logger.debug(
|
|
@@ -172,10 +206,16 @@ class AIQASpanExporter(SpanExporter):
|
|
|
172
206
|
"traceFlags": span_context.trace_flags,
|
|
173
207
|
"duration": self._time_to_tuple(span.end_time - span.start_time) if span.end_time else None,
|
|
174
208
|
"ended": span.end_time is not None,
|
|
175
|
-
"instrumentationLibrary":
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
209
|
+
"instrumentationLibrary": self._get_instrumentation_library(span),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
def _get_instrumentation_library(self, span: ReadableSpan) -> Dict[str, Any]:
|
|
213
|
+
"""
|
|
214
|
+
Get instrumentation library information from the span: just use the package version.
|
|
215
|
+
"""
|
|
216
|
+
return {
|
|
217
|
+
"name": AIQA_TRACER_NAME,
|
|
218
|
+
"version": VERSION,
|
|
179
219
|
}
|
|
180
220
|
|
|
181
221
|
def _time_to_tuple(self, nanoseconds: int) -> tuple:
|
|
@@ -183,19 +223,6 @@ class AIQASpanExporter(SpanExporter):
|
|
|
183
223
|
seconds = int(nanoseconds // 1_000_000_000)
|
|
184
224
|
nanos = int(nanoseconds % 1_000_000_000)
|
|
185
225
|
return (seconds, nanos)
|
|
186
|
-
|
|
187
|
-
def _get_instrumentation_name(self) -> str:
|
|
188
|
-
"""Get instrumentation library name - always 'aiqa-tracer'."""
|
|
189
|
-
from .client import AIQA_TRACER_NAME
|
|
190
|
-
return AIQA_TRACER_NAME
|
|
191
|
-
|
|
192
|
-
def _get_instrumentation_version(self) -> Optional[str]:
|
|
193
|
-
"""Get instrumentation library version from __version__."""
|
|
194
|
-
try:
|
|
195
|
-
from . import __version__
|
|
196
|
-
return __version__
|
|
197
|
-
except (ImportError, AttributeError):
|
|
198
|
-
return None
|
|
199
226
|
|
|
200
227
|
def _build_request_headers(self) -> Dict[str, str]:
|
|
201
228
|
"""Build HTTP headers for span requests."""
|
|
@@ -367,16 +394,38 @@ class AIQASpanExporter(SpanExporter):
|
|
|
367
394
|
raise
|
|
368
395
|
|
|
369
396
|
def _start_auto_flush(self) -> None:
|
|
370
|
-
"""Start the auto-flush timer."""
|
|
397
|
+
"""Start the auto-flush timer with startup delay."""
|
|
371
398
|
if self.shutdown_requested:
|
|
372
399
|
logger.warning("_start_auto_flush() called but shutdown already requested")
|
|
373
400
|
return
|
|
374
401
|
|
|
375
|
-
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
|
+
)
|
|
376
406
|
|
|
377
407
|
def flush_worker():
|
|
378
408
|
import asyncio
|
|
379
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
|
+
|
|
380
429
|
loop = asyncio.new_event_loop()
|
|
381
430
|
asyncio.set_event_loop(loop)
|
|
382
431
|
|
|
@@ -429,8 +478,10 @@ class AIQASpanExporter(SpanExporter):
|
|
|
429
478
|
else:
|
|
430
479
|
logger.debug("_send_spans() no API key provided")
|
|
431
480
|
|
|
481
|
+
# Use timeout to prevent hanging on unreachable servers
|
|
482
|
+
timeout = aiohttp.ClientTimeout(total=30.0, connect=10.0)
|
|
432
483
|
errors = []
|
|
433
|
-
async with aiohttp.ClientSession() as session:
|
|
484
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
434
485
|
for batch_idx, batch in enumerate(batches):
|
|
435
486
|
try:
|
|
436
487
|
logger.debug(f"_send_spans() sending batch {batch_idx + 1}/{len(batches)} with {len(batch)} spans to {url}")
|
|
@@ -448,6 +499,12 @@ class AIQASpanExporter(SpanExporter):
|
|
|
448
499
|
# Continue with other batches even if one fails
|
|
449
500
|
continue
|
|
450
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
|
|
451
508
|
except RuntimeError as e:
|
|
452
509
|
if self._is_interpreter_shutdown_error(e):
|
|
453
510
|
if self.shutdown_requested:
|
|
@@ -466,6 +523,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
466
523
|
# Continue with other batches
|
|
467
524
|
|
|
468
525
|
# If any batches failed, raise an exception with details
|
|
526
|
+
# Spans will be restored to buffer for retry on next flush
|
|
469
527
|
if errors:
|
|
470
528
|
error_summary = "; ".join([f"batch {idx}: {msg}" for idx, msg in errors])
|
|
471
529
|
raise Exception(f"Failed to send some spans: {error_summary}")
|
aiqa/client.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import logging
|
|
4
4
|
from functools import lru_cache
|
|
5
|
-
from typing import Optional
|
|
5
|
+
from typing import Optional, TYPE_CHECKING, Any
|
|
6
6
|
from opentelemetry import trace
|
|
7
7
|
from opentelemetry.sdk.trace import TracerProvider
|
|
8
8
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
@@ -12,7 +12,7 @@ logger = logging.getLogger("AIQA")
|
|
|
12
12
|
# Compatibility import for TraceIdRatioBased sampler
|
|
13
13
|
# In older OpenTelemetry versions it was TraceIdRatioBasedSampler
|
|
14
14
|
# In newer versions (>=1.24.0) it's TraceIdRatioBased
|
|
15
|
-
TraceIdRatioBased = None
|
|
15
|
+
TraceIdRatioBased: Optional[Any] = None
|
|
16
16
|
try:
|
|
17
17
|
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
|
|
18
18
|
except ImportError:
|
|
@@ -28,10 +28,7 @@ except ImportError:
|
|
|
28
28
|
# Set to None so we can check later
|
|
29
29
|
TraceIdRatioBased = None
|
|
30
30
|
|
|
31
|
-
from .
|
|
32
|
-
|
|
33
|
-
AIQA_TRACER_NAME = "aiqa-tracer"
|
|
34
|
-
|
|
31
|
+
from .constants import AIQA_TRACER_NAME
|
|
35
32
|
|
|
36
33
|
class AIQAClient:
|
|
37
34
|
"""
|
|
@@ -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
|
|
|
@@ -78,32 +75,38 @@ class AIQAClient:
|
|
|
78
75
|
|
|
79
76
|
@enabled.setter
|
|
80
77
|
def enabled(self, value: bool) -> None:
|
|
81
|
-
"""Set the enabled state.
|
|
82
|
-
self._enabled = value
|
|
83
|
-
|
|
84
|
-
def set_enabled(self, enabled: bool) -> None:
|
|
85
|
-
"""
|
|
86
|
-
Enable or disable AIQA tracing.
|
|
78
|
+
"""Set the enabled state.
|
|
87
79
|
|
|
88
80
|
When disabled:
|
|
89
81
|
- Tracing does not create spans
|
|
90
82
|
- Export does not send spans
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
enabled: True to enable tracing, False to disable
|
|
94
83
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
logger.info("AIQA tracing enabled")
|
|
98
|
-
else:
|
|
99
|
-
logger.info("AIQA tracing disabled")
|
|
84
|
+
logger.info(f"AIQA tracing {'enabled' if value else 'disabled'}")
|
|
85
|
+
self._enabled = value
|
|
100
86
|
|
|
101
|
-
def
|
|
102
|
-
"""
|
|
103
|
-
|
|
87
|
+
def shutdown(self) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Shutdown the tracer provider and exporter.
|
|
90
|
+
It is not necessary to call this function.
|
|
91
|
+
Use this to clean up resources at the end of all tracing.
|
|
92
|
+
|
|
93
|
+
This will also set enabled=False to prevent further tracing attempts.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
logger.info("AIQA tracing shutting down")
|
|
97
|
+
# Disable tracing to prevent attempts to use shut-down system
|
|
98
|
+
self.enabled = False
|
|
99
|
+
if self._provider:
|
|
100
|
+
self._provider.shutdown()
|
|
101
|
+
if self._exporter:
|
|
102
|
+
self._exporter.shutdown()
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Error shutting down tracing: {e}")
|
|
105
|
+
# Still disable even if shutdown had errors
|
|
106
|
+
self.enabled = False
|
|
104
107
|
|
|
105
108
|
|
|
106
|
-
# Global singleton instance
|
|
109
|
+
# Global singleton instance
|
|
107
110
|
client: AIQAClient = AIQAClient()
|
|
108
111
|
|
|
109
112
|
# Component tag to add to all spans (can be set via AIQA_COMPONENT_TAG env var or programmatically)
|
|
@@ -154,24 +157,19 @@ def get_aiqa_client() -> AIQAClient:
|
|
|
154
157
|
logger.warning("AIQA tracing is disabled. Your application will continue to run without tracing.")
|
|
155
158
|
return client
|
|
156
159
|
|
|
157
|
-
def _init_tracing():
|
|
160
|
+
def _init_tracing() -> None:
|
|
158
161
|
"""Initialize tracing system and load configuration from environment variables."""
|
|
159
162
|
global client
|
|
160
163
|
if client._initialized:
|
|
161
164
|
return
|
|
162
165
|
|
|
163
166
|
try:
|
|
164
|
-
# Check for required environment variables
|
|
165
167
|
server_url = os.getenv("AIQA_SERVER_URL")
|
|
166
168
|
api_key = os.getenv("AIQA_API_KEY")
|
|
167
169
|
|
|
168
170
|
if not server_url or not api_key:
|
|
169
171
|
client.enabled = False
|
|
170
|
-
missing_vars = []
|
|
171
|
-
if not server_url:
|
|
172
|
-
missing_vars.append("AIQA_SERVER_URL")
|
|
173
|
-
if not api_key:
|
|
174
|
-
missing_vars.append("AIQA_API_KEY")
|
|
172
|
+
missing_vars = [var for var, val in [("AIQA_SERVER_URL", server_url), ("AIQA_API_KEY", api_key)] if not val]
|
|
175
173
|
logger.warning(
|
|
176
174
|
f"AIQA tracing is disabled: missing required environment variables: {', '.join(missing_vars)}"
|
|
177
175
|
)
|
|
@@ -218,10 +216,12 @@ def _init_tracing():
|
|
|
218
216
|
client._initialized = True # Mark as initialized even on error to prevent retry loops
|
|
219
217
|
raise
|
|
220
218
|
|
|
221
|
-
def _attach_aiqa_processor(provider: TracerProvider):
|
|
219
|
+
def _attach_aiqa_processor(provider: TracerProvider) -> None:
|
|
222
220
|
"""Attach AIQA span processor to the provider. Idempotent - safe to call multiple times."""
|
|
221
|
+
from .aiqa_exporter import AIQASpanExporter
|
|
222
|
+
|
|
223
223
|
try:
|
|
224
|
-
#
|
|
224
|
+
# Check if already attached
|
|
225
225
|
for p in provider._active_span_processor._span_processors:
|
|
226
226
|
if isinstance(getattr(p, "exporter", None), AIQASpanExporter):
|
|
227
227
|
logger.debug("AIQA span processor already attached, skipping")
|
|
@@ -241,44 +241,19 @@ def _attach_aiqa_processor(provider: TracerProvider):
|
|
|
241
241
|
raise
|
|
242
242
|
|
|
243
243
|
|
|
244
|
-
def set_enabled(enabled: bool) -> None:
|
|
245
|
-
"""
|
|
246
|
-
Enable or disable AIQA tracing.
|
|
247
|
-
|
|
248
|
-
When disabled:
|
|
249
|
-
- Tracing does not create spans
|
|
250
|
-
- Export does not send spans
|
|
251
|
-
|
|
252
|
-
Args:
|
|
253
|
-
enabled: True to enable tracing, False to disable
|
|
254
|
-
|
|
255
|
-
Example:
|
|
256
|
-
from aiqa import get_aiqa_client
|
|
257
|
-
|
|
258
|
-
client = get_aiqa_client()
|
|
259
|
-
client.set_enabled(False) # Disable tracing
|
|
260
|
-
"""
|
|
261
|
-
client = get_aiqa_client()
|
|
262
|
-
client.set_enabled(enabled)
|
|
263
|
-
|
|
264
244
|
|
|
265
|
-
def get_aiqa_tracer():
|
|
245
|
+
def get_aiqa_tracer() -> trace.Tracer:
|
|
266
246
|
"""
|
|
267
247
|
Get the AIQA tracer with version from __init__.py __version__.
|
|
268
|
-
This should be used instead of trace.get_tracer()
|
|
248
|
+
This should be used instead of trace.get_tracer() so that the version is set.
|
|
269
249
|
"""
|
|
270
250
|
try:
|
|
271
251
|
# Import here to avoid circular import
|
|
272
|
-
from . import
|
|
273
|
-
|
|
252
|
+
from . import VERSION
|
|
274
253
|
# Compatibility: version parameter may not be supported in older OpenTelemetry versions
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return trace.get_tracer(AIQA_TRACER_NAME, version=__version__)
|
|
278
|
-
except TypeError:
|
|
279
|
-
# Fall back to without version parameter (older versions)
|
|
280
|
-
return trace.get_tracer(AIQA_TRACER_NAME)
|
|
254
|
+
# Try with version parameter (newer OpenTelemetry versions)
|
|
255
|
+
return trace.get_tracer(AIQA_TRACER_NAME, version=VERSION)
|
|
281
256
|
except Exception as e:
|
|
282
|
-
|
|
283
|
-
|
|
257
|
+
# Log issue but still return a tracer
|
|
258
|
+
logger.info(f"Issue getting AIQA tracer with version: {e}, using fallback")
|
|
284
259
|
return trace.get_tracer(AIQA_TRACER_NAME)
|
aiqa/constants.py
ADDED
aiqa/tracing.py
CHANGED
|
@@ -14,7 +14,8 @@ from opentelemetry.sdk.trace import TracerProvider
|
|
|
14
14
|
from opentelemetry.trace import Status, StatusCode, SpanContext, TraceFlags
|
|
15
15
|
from opentelemetry.propagate import inject, extract
|
|
16
16
|
from .aiqa_exporter import AIQASpanExporter
|
|
17
|
-
from .client import get_aiqa_client,
|
|
17
|
+
from .client import get_aiqa_client, get_component_tag, set_component_tag as _set_component_tag, get_aiqa_tracer
|
|
18
|
+
from .constants import AIQA_TRACER_NAME
|
|
18
19
|
from .object_serialiser import serialize_for_span
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger("AIQA")
|
|
@@ -25,6 +26,7 @@ async def flush_tracing() -> None:
|
|
|
25
26
|
Flush all pending spans to the server.
|
|
26
27
|
Flushes also happen automatically every few seconds. So you only need to call this function
|
|
27
28
|
if you want to flush immediately, e.g. before exiting a process.
|
|
29
|
+
A common use is if you are tracing unit tests or experiment runs.
|
|
28
30
|
|
|
29
31
|
This flushes both the BatchSpanProcessor and the exporter buffer.
|
|
30
32
|
"""
|
|
@@ -35,25 +37,10 @@ async def flush_tracing() -> None:
|
|
|
35
37
|
await client.exporter.flush()
|
|
36
38
|
|
|
37
39
|
|
|
38
|
-
async def shutdown_tracing() -> None:
|
|
39
|
-
"""
|
|
40
|
-
Shutdown the tracer provider and exporter.
|
|
41
|
-
It is not necessary to call this function.
|
|
42
|
-
"""
|
|
43
|
-
try:
|
|
44
|
-
client = get_aiqa_client()
|
|
45
|
-
if client.provider:
|
|
46
|
-
client.provider.shutdown() # Synchronous method
|
|
47
|
-
if client.exporter:
|
|
48
|
-
client.exporter.shutdown() # Synchronous method
|
|
49
|
-
except Exception as e:
|
|
50
|
-
logger.error(f"Error shutting down tracing: {e}")
|
|
51
|
-
|
|
52
|
-
|
|
53
40
|
# Export provider and exporter accessors for advanced usage
|
|
54
41
|
|
|
55
42
|
__all__ = [
|
|
56
|
-
"
|
|
43
|
+
"flush_tracing", "WithTracing",
|
|
57
44
|
"set_span_attribute", "set_span_name", "get_active_span",
|
|
58
45
|
"get_active_trace_id", "get_span_id", "create_span_from_trace_id", "inject_trace_context", "extract_trace_context",
|
|
59
46
|
"set_conversation_id", "set_component_tag", "set_token_usage", "set_provider_and_model", "get_span", "submit_feedback"
|
|
@@ -969,16 +956,6 @@ def set_component_tag(tag: str) -> None:
|
|
|
969
956
|
"""
|
|
970
957
|
_set_component_tag(tag)
|
|
971
958
|
|
|
972
|
-
def get_provider() -> Optional[TracerProvider]:
|
|
973
|
-
"""Get the tracer provider for advanced usage."""
|
|
974
|
-
client = get_aiqa_client()
|
|
975
|
-
return client.provider
|
|
976
|
-
|
|
977
|
-
def get_exporter() -> Optional[AIQASpanExporter]:
|
|
978
|
-
"""Get the exporter for advanced usage."""
|
|
979
|
-
client = get_aiqa_client()
|
|
980
|
-
return client.exporter
|
|
981
|
-
|
|
982
959
|
|
|
983
960
|
def get_active_trace_id() -> Optional[str]:
|
|
984
961
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiqa-client
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -134,12 +134,12 @@ asyncio.run(main())
|
|
|
134
134
|
To ensure all spans are sent before process exit:
|
|
135
135
|
|
|
136
136
|
```python
|
|
137
|
-
from aiqa import
|
|
137
|
+
from aiqa import flush_tracing
|
|
138
138
|
import asyncio
|
|
139
139
|
|
|
140
140
|
async def main():
|
|
141
141
|
# Your code here
|
|
142
|
-
await
|
|
142
|
+
await flush_tracing()
|
|
143
143
|
|
|
144
144
|
asyncio.run(main())
|
|
145
145
|
```
|
|
@@ -154,10 +154,10 @@ from aiqa import get_aiqa_client
|
|
|
154
154
|
client = get_aiqa_client()
|
|
155
155
|
|
|
156
156
|
# Disable tracing (spans won't be created or exported)
|
|
157
|
-
client.
|
|
157
|
+
client.enabled = False
|
|
158
158
|
|
|
159
159
|
# Re-enable tracing
|
|
160
|
-
client.
|
|
160
|
+
client.enabled = True
|
|
161
161
|
|
|
162
162
|
# Check if tracing is enabled
|
|
163
163
|
if client.enabled:
|
|
@@ -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,14 +0,0 @@
|
|
|
1
|
-
aiqa/__init__.py,sha256=N3ZE2kr3r6H14vHwUrqof7ifKmYTb7oiYPWp-Ks4aVE,1609
|
|
2
|
-
aiqa/aiqa_exporter.py,sha256=L9GHHPkQwyFKhjw0Wwu260X7ilp67tcczW6T7nC_WfQ,27237
|
|
3
|
-
aiqa/client.py,sha256=peXKElZCNl_FIdyCIfATcquDzSC_zJmS_FZDRnoiZ0s,9886
|
|
4
|
-
aiqa/experiment_runner.py,sha256=ZEDwECstAv4lWXpcdB9WSxfDQj43iqkGzB_YzoY933M,12053
|
|
5
|
-
aiqa/object_serialiser.py,sha256=pgcBVw5sZH8f7N6n3-qOvEcbNhuPS5yq7qdhaNT6Sks,15236
|
|
6
|
-
aiqa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
aiqa/test_experiment_runner.py,sha256=LM8BuCrzBZL0Wyu_ierK0tNLsOUxxMTAHbAGW2G0qp0,5562
|
|
8
|
-
aiqa/test_tracing.py,sha256=mSVrhRQ6Dz5djlSUkCt097sIr84562w6E0BnuQDpMrI,8347
|
|
9
|
-
aiqa/tracing.py,sha256=7ZALbVo6_iidoo6JiV8roKP5CWlJ0E500B7SAhDOCPo,50440
|
|
10
|
-
aiqa_client-0.3.6.dist-info/licenses/LICENSE,sha256=kIzkzLuzG0HHaWYm4F4W5FeJ1Yxut3Ec6bhLWyw798A,1062
|
|
11
|
-
aiqa_client-0.3.6.dist-info/METADATA,sha256=kEYDE9GuSBOUFTA7IdlAUkiNNRPE1V-9sV1vEz67gQs,7517
|
|
12
|
-
aiqa_client-0.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
-
aiqa_client-0.3.6.dist-info/top_level.txt,sha256=nwcsuVVSuWu27iLxZd4n1evVzv1W6FVTrSnCXCc-NQs,5
|
|
14
|
-
aiqa_client-0.3.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|