aiqa-client 0.1.2__py3-none-any.whl → 0.1.3__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 +7 -44
- aiqa/aiqa_exporter.py +21 -79
- aiqa/tracing.py +160 -1107
- {aiqa_client-0.1.2.dist-info → aiqa_client-0.1.3.dist-info}/METADATA +4 -94
- aiqa_client-0.1.3.dist-info/RECORD +9 -0
- aiqa/client.py +0 -170
- aiqa/experiment_runner.py +0 -336
- aiqa/object_serialiser.py +0 -361
- aiqa/test_experiment_runner.py +0 -176
- aiqa/test_tracing.py +0 -230
- aiqa_client-0.1.2.dist-info/RECORD +0 -14
- {aiqa_client-0.1.2.dist-info → aiqa_client-0.1.3.dist-info}/WHEEL +0 -0
- {aiqa_client-0.1.2.dist-info → aiqa_client-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {aiqa_client-0.1.2.dist-info → aiqa_client-0.1.3.dist-info}/top_level.txt +0 -0
aiqa/__init__.py
CHANGED
|
@@ -1,66 +1,29 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Python client for AIQA server - OpenTelemetry tracing decorators.
|
|
3
|
-
|
|
4
|
-
IMPORTANT: Before using any AIQA functionality, you must call get_aiqa_client() to initialize
|
|
5
|
-
the client and load environment variables (AIQA_SERVER_URL, AIQA_API_KEY, AIQA_COMPONENT_TAG, etc.).
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
from dotenv import load_dotenv
|
|
9
|
-
from aiqa import get_aiqa_client, WithTracing
|
|
10
|
-
|
|
11
|
-
# Load environment variables from .env file (if using one)
|
|
12
|
-
load_dotenv()
|
|
13
|
-
|
|
14
|
-
# Initialize client (must be called before using WithTracing or other functions)
|
|
15
|
-
get_aiqa_client()
|
|
16
|
-
|
|
17
|
-
@WithTracing
|
|
18
|
-
def my_function():
|
|
19
|
-
return "Hello, AIQA!"
|
|
20
3
|
"""
|
|
21
4
|
|
|
22
5
|
from .tracing import (
|
|
23
6
|
WithTracing,
|
|
24
|
-
|
|
7
|
+
flush_spans,
|
|
25
8
|
shutdown_tracing,
|
|
26
9
|
set_span_attribute,
|
|
27
10
|
set_span_name,
|
|
28
11
|
get_active_span,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
get_trace_id,
|
|
32
|
-
get_span_id,
|
|
33
|
-
create_span_from_trace_id,
|
|
34
|
-
inject_trace_context,
|
|
35
|
-
extract_trace_context,
|
|
36
|
-
set_conversation_id,
|
|
37
|
-
set_component_tag,
|
|
38
|
-
get_span,
|
|
12
|
+
provider,
|
|
13
|
+
exporter,
|
|
39
14
|
)
|
|
40
|
-
from .client import get_aiqa_client
|
|
41
|
-
from .experiment_runner import ExperimentRunner
|
|
42
15
|
|
|
43
|
-
__version__ = "0.1.
|
|
16
|
+
__version__ = "0.1.3"
|
|
44
17
|
|
|
45
18
|
__all__ = [
|
|
46
19
|
"WithTracing",
|
|
47
|
-
"
|
|
20
|
+
"flush_spans",
|
|
48
21
|
"shutdown_tracing",
|
|
49
22
|
"set_span_attribute",
|
|
50
23
|
"set_span_name",
|
|
51
24
|
"get_active_span",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"get_aiqa_client",
|
|
55
|
-
"ExperimentRunner",
|
|
56
|
-
"get_trace_id",
|
|
57
|
-
"get_span_id",
|
|
58
|
-
"create_span_from_trace_id",
|
|
59
|
-
"inject_trace_context",
|
|
60
|
-
"extract_trace_context",
|
|
61
|
-
"set_conversation_id",
|
|
62
|
-
"set_component_tag",
|
|
63
|
-
"get_span",
|
|
25
|
+
"provider",
|
|
26
|
+
"exporter",
|
|
64
27
|
"__version__",
|
|
65
28
|
]
|
|
66
29
|
|
aiqa/aiqa_exporter.py
CHANGED
|
@@ -8,12 +8,11 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import threading
|
|
10
10
|
import time
|
|
11
|
-
import io
|
|
12
11
|
from typing import List, Dict, Any, Optional
|
|
13
12
|
from opentelemetry.sdk.trace import ReadableSpan
|
|
14
13
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
15
14
|
|
|
16
|
-
logger = logging.getLogger(
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class AIQASpanExporter(SpanExporter):
|
|
@@ -40,7 +39,6 @@ class AIQASpanExporter(SpanExporter):
|
|
|
40
39
|
self._api_key = api_key
|
|
41
40
|
self.flush_interval_ms = flush_interval_seconds * 1000
|
|
42
41
|
self.buffer: List[Dict[str, Any]] = []
|
|
43
|
-
self.buffer_span_keys: set = set() # Track (traceId, spanId) tuples to prevent duplicates (Python 3.8 compatible)
|
|
44
42
|
self.buffer_lock = threading.Lock()
|
|
45
43
|
self.flush_lock = threading.Lock()
|
|
46
44
|
self.shutdown_requested = False
|
|
@@ -63,39 +61,21 @@ class AIQASpanExporter(SpanExporter):
|
|
|
63
61
|
def export(self, spans: List[ReadableSpan]) -> SpanExportResult:
|
|
64
62
|
"""
|
|
65
63
|
Export spans to the AIQA server. Adds spans to buffer for async flushing.
|
|
66
|
-
Deduplicates spans based on (traceId, spanId) to prevent repeated exports.
|
|
67
64
|
"""
|
|
68
65
|
if not spans:
|
|
69
66
|
logger.debug("export() called with empty spans list")
|
|
70
67
|
return SpanExportResult.SUCCESS
|
|
71
|
-
|
|
72
|
-
# Serialize and add to buffer
|
|
68
|
+
|
|
69
|
+
# Serialize and add to buffer
|
|
73
70
|
with self.buffer_lock:
|
|
74
|
-
serialized_spans = []
|
|
75
|
-
duplicates_count = 0
|
|
76
|
-
for span in spans:
|
|
77
|
-
serialized = self._serialize_span(span)
|
|
78
|
-
span_key = (serialized["traceId"], serialized["spanId"])
|
|
79
|
-
if span_key not in self.buffer_span_keys:
|
|
80
|
-
serialized_spans.append(serialized)
|
|
81
|
-
self.buffer_span_keys.add(span_key)
|
|
82
|
-
else:
|
|
83
|
-
duplicates_count += 1
|
|
84
|
-
logger.debug(f"export() skipping duplicate span: traceId={serialized['traceId']}, spanId={serialized['spanId']}")
|
|
85
|
-
|
|
71
|
+
serialized_spans = [self._serialize_span(span) for span in spans]
|
|
86
72
|
self.buffer.extend(serialized_spans)
|
|
87
73
|
buffer_size = len(self.buffer)
|
|
88
74
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
else:
|
|
95
|
-
logger.debug(
|
|
96
|
-
f"export() added {len(spans)} span(s) to buffer. "
|
|
97
|
-
f"Total buffered: {buffer_size}"
|
|
98
|
-
)
|
|
75
|
+
logger.debug(
|
|
76
|
+
f"export() added {len(spans)} span(s) to buffer. "
|
|
77
|
+
f"Total buffered: {buffer_size}"
|
|
78
|
+
)
|
|
99
79
|
|
|
100
80
|
return SpanExportResult.SUCCESS
|
|
101
81
|
|
|
@@ -158,8 +138,8 @@ class AIQASpanExporter(SpanExporter):
|
|
|
158
138
|
"duration": self._time_to_tuple(span.end_time - span.start_time) if span.end_time else None,
|
|
159
139
|
"ended": span.end_time is not None,
|
|
160
140
|
"instrumentationLibrary": {
|
|
161
|
-
"name":
|
|
162
|
-
"version":
|
|
141
|
+
"name": span.instrumentation_info.name if hasattr(span, "instrumentation_info") else "",
|
|
142
|
+
"version": span.instrumentation_info.version if hasattr(span, "instrumentation_info") else None,
|
|
163
143
|
},
|
|
164
144
|
}
|
|
165
145
|
|
|
@@ -168,19 +148,6 @@ class AIQASpanExporter(SpanExporter):
|
|
|
168
148
|
seconds = int(nanoseconds // 1_000_000_000)
|
|
169
149
|
nanos = int(nanoseconds % 1_000_000_000)
|
|
170
150
|
return (seconds, nanos)
|
|
171
|
-
|
|
172
|
-
def _get_instrumentation_name(self) -> str:
|
|
173
|
-
"""Get instrumentation library name - always 'aiqa-tracer'."""
|
|
174
|
-
from .client import AIQA_TRACER_NAME
|
|
175
|
-
return AIQA_TRACER_NAME
|
|
176
|
-
|
|
177
|
-
def _get_instrumentation_version(self) -> Optional[str]:
|
|
178
|
-
"""Get instrumentation library version from __version__."""
|
|
179
|
-
try:
|
|
180
|
-
from . import __version__
|
|
181
|
-
return __version__
|
|
182
|
-
except (ImportError, AttributeError):
|
|
183
|
-
return None
|
|
184
151
|
|
|
185
152
|
def _build_request_headers(self) -> Dict[str, str]:
|
|
186
153
|
"""Build HTTP headers for span requests."""
|
|
@@ -210,38 +177,24 @@ class AIQASpanExporter(SpanExporter):
|
|
|
210
177
|
Atomically extract and remove all spans from buffer (thread-safe).
|
|
211
178
|
Returns the extracted spans. This prevents race conditions where spans
|
|
212
179
|
are added between extraction and clearing.
|
|
213
|
-
Note: Does NOT clear buffer_span_keys - that should be done after successful send
|
|
214
|
-
to avoid unnecessary clearing/rebuilding on failures.
|
|
215
180
|
"""
|
|
216
181
|
with self.buffer_lock:
|
|
217
182
|
spans = self.buffer[:]
|
|
218
183
|
self.buffer.clear()
|
|
219
184
|
return spans
|
|
220
|
-
|
|
221
|
-
def _remove_span_keys_from_tracking(self, spans: List[Dict[str, Any]]) -> None:
|
|
222
|
-
"""
|
|
223
|
-
Remove span keys from tracking set (thread-safe). Called after successful send.
|
|
224
|
-
"""
|
|
225
|
-
with self.buffer_lock:
|
|
226
|
-
for span in spans:
|
|
227
|
-
span_key = (span["traceId"], span["spanId"])
|
|
228
|
-
self.buffer_span_keys.discard(span_key)
|
|
229
185
|
|
|
230
186
|
def _prepend_spans_to_buffer(self, spans: List[Dict[str, Any]]) -> None:
|
|
231
187
|
"""
|
|
232
188
|
Prepend spans back to buffer (thread-safe). Used to restore spans
|
|
233
|
-
if sending fails.
|
|
189
|
+
if sending fails.
|
|
234
190
|
"""
|
|
235
191
|
with self.buffer_lock:
|
|
236
192
|
self.buffer[:0] = spans
|
|
237
|
-
# Rebuild span keys set from current buffer contents
|
|
238
|
-
self.buffer_span_keys = {(span["traceId"], span["spanId"]) for span in self.buffer}
|
|
239
193
|
|
|
240
194
|
def _clear_buffer(self) -> None:
|
|
241
195
|
"""Clear the buffer (thread-safe)."""
|
|
242
196
|
with self.buffer_lock:
|
|
243
197
|
self.buffer.clear()
|
|
244
|
-
self.buffer_span_keys.clear()
|
|
245
198
|
|
|
246
199
|
async def flush(self) -> None:
|
|
247
200
|
"""
|
|
@@ -265,8 +218,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
265
218
|
logger.warning(
|
|
266
219
|
f"Skipping flush: AIQA_SERVER_URL is not set. {len(spans_to_flush)} span(s) will not be sent."
|
|
267
220
|
)
|
|
268
|
-
# Spans already removed from buffer,
|
|
269
|
-
self._remove_span_keys_from_tracking(spans_to_flush)
|
|
221
|
+
# Spans already removed from buffer, nothing to clear
|
|
270
222
|
return
|
|
271
223
|
|
|
272
224
|
logger.info(f"flush() sending {len(spans_to_flush)} span(s) to server")
|
|
@@ -274,8 +226,6 @@ class AIQASpanExporter(SpanExporter):
|
|
|
274
226
|
await self._send_spans(spans_to_flush)
|
|
275
227
|
logger.info(f"flush() successfully sent {len(spans_to_flush)} span(s) to server")
|
|
276
228
|
# Spans already removed from buffer during extraction
|
|
277
|
-
# Now clear their keys from tracking set to free memory
|
|
278
|
-
self._remove_span_keys_from_tracking(spans_to_flush)
|
|
279
229
|
except RuntimeError as error:
|
|
280
230
|
if self._is_interpreter_shutdown_error(error):
|
|
281
231
|
if self.shutdown_requested:
|
|
@@ -287,12 +237,12 @@ class AIQASpanExporter(SpanExporter):
|
|
|
287
237
|
# Put spans back for retry
|
|
288
238
|
self._prepend_spans_to_buffer(spans_to_flush)
|
|
289
239
|
raise
|
|
290
|
-
logger.error(f"Error flushing spans to server: {error}")
|
|
240
|
+
logger.error(f"Error flushing spans to server: {error}", exc_info=True)
|
|
291
241
|
# Put spans back for retry
|
|
292
242
|
self._prepend_spans_to_buffer(spans_to_flush)
|
|
293
243
|
raise
|
|
294
244
|
except Exception as error:
|
|
295
|
-
logger.error(f"Error flushing spans to server: {error}")
|
|
245
|
+
logger.error(f"Error flushing spans to server: {error}", exc_info=True)
|
|
296
246
|
# Put spans back for retry
|
|
297
247
|
self._prepend_spans_to_buffer(spans_to_flush)
|
|
298
248
|
if self.shutdown_requested:
|
|
@@ -321,7 +271,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
321
271
|
logger.debug(f"Auto-flush cycle #{cycle_count} completed, sleeping {self.flush_interval_ms / 1000.0}s")
|
|
322
272
|
time.sleep(self.flush_interval_ms / 1000.0)
|
|
323
273
|
except Exception as e:
|
|
324
|
-
logger.error(f"Error in auto-flush cycle #{cycle_count}: {e}")
|
|
274
|
+
logger.error(f"Error in auto-flush cycle #{cycle_count}: {e}", exc_info=True)
|
|
325
275
|
logger.debug(f"Auto-flush cycle #{cycle_count} error handled, sleeping {self.flush_interval_ms / 1000.0}s")
|
|
326
276
|
time.sleep(self.flush_interval_ms / 1000.0)
|
|
327
277
|
|
|
@@ -357,13 +307,9 @@ class AIQASpanExporter(SpanExporter):
|
|
|
357
307
|
logger.debug("_send_spans() no API key provided")
|
|
358
308
|
|
|
359
309
|
try:
|
|
360
|
-
# Pre-serialize JSON to bytes and wrap in BytesIO to avoid blocking event loop
|
|
361
|
-
json_bytes = json.dumps(spans).encode('utf-8')
|
|
362
|
-
data = io.BytesIO(json_bytes)
|
|
363
|
-
|
|
364
310
|
async with aiohttp.ClientSession() as session:
|
|
365
311
|
logger.debug(f"_send_spans() POST request starting to {url}")
|
|
366
|
-
async with session.post(url,
|
|
312
|
+
async with session.post(url, json=spans, headers=headers) as response:
|
|
367
313
|
logger.debug(f"_send_spans() received response: status={response.status}")
|
|
368
314
|
if not response.ok:
|
|
369
315
|
error_text = await response.text()
|
|
@@ -382,10 +328,10 @@ class AIQASpanExporter(SpanExporter):
|
|
|
382
328
|
else:
|
|
383
329
|
logger.warning(f"_send_spans() interrupted by interpreter shutdown: {e}")
|
|
384
330
|
raise
|
|
385
|
-
logger.error(f"_send_spans() RuntimeError: {type(e).__name__}: {e}")
|
|
331
|
+
logger.error(f"_send_spans() RuntimeError: {type(e).__name__}: {e}", exc_info=True)
|
|
386
332
|
raise
|
|
387
333
|
except Exception as e:
|
|
388
|
-
logger.error(f"_send_spans() exception: {type(e).__name__}: {e}")
|
|
334
|
+
logger.error(f"_send_spans() exception: {type(e).__name__}: {e}", exc_info=True)
|
|
389
335
|
raise
|
|
390
336
|
|
|
391
337
|
def _send_spans_sync(self, spans: List[Dict[str, Any]]) -> None:
|
|
@@ -414,7 +360,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
414
360
|
)
|
|
415
361
|
logger.debug(f"_send_spans_sync() successfully sent {len(spans)} spans")
|
|
416
362
|
except Exception as e:
|
|
417
|
-
logger.error(f"_send_spans_sync() exception: {type(e).__name__}: {e}")
|
|
363
|
+
logger.error(f"_send_spans_sync() exception: {type(e).__name__}: {e}", exc_info=True)
|
|
418
364
|
raise
|
|
419
365
|
|
|
420
366
|
def shutdown(self) -> None:
|
|
@@ -451,21 +397,17 @@ class AIQASpanExporter(SpanExporter):
|
|
|
451
397
|
f"shutdown() skipping final flush: AIQA_SERVER_URL is not set. "
|
|
452
398
|
f"{len(spans_to_flush)} span(s) will not be sent."
|
|
453
399
|
)
|
|
454
|
-
# Spans already removed from buffer
|
|
455
|
-
self._remove_span_keys_from_tracking(spans_to_flush)
|
|
400
|
+
# Spans already removed from buffer
|
|
456
401
|
else:
|
|
457
402
|
logger.info(f"shutdown() sending {len(spans_to_flush)} span(s) to server (synchronous)")
|
|
458
403
|
try:
|
|
459
404
|
self._send_spans_sync(spans_to_flush)
|
|
460
405
|
logger.info(f"shutdown() successfully sent {len(spans_to_flush)} span(s) to server")
|
|
461
406
|
# Spans already removed from buffer during extraction
|
|
462
|
-
# Clear their keys from tracking set to free memory
|
|
463
|
-
self._remove_span_keys_from_tracking(spans_to_flush)
|
|
464
407
|
except Exception as e:
|
|
465
|
-
logger.error(f"shutdown() failed to send spans: {e}")
|
|
408
|
+
logger.error(f"shutdown() failed to send spans: {e}", exc_info=True)
|
|
466
409
|
# Spans already removed, but process is exiting anyway
|
|
467
410
|
logger.warning(f"shutdown() {len(spans_to_flush)} span(s) were not sent due to error")
|
|
468
|
-
# Keys will remain in tracking set, but process is exiting so memory will be freed
|
|
469
411
|
else:
|
|
470
412
|
logger.debug("shutdown() no spans to flush")
|
|
471
413
|
|