aiqa-client 0.1.2__py3-none-any.whl → 0.1.4__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 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
- flush_tracing,
7
+ flush_spans,
25
8
  shutdown_tracing,
26
9
  set_span_attribute,
27
10
  set_span_name,
28
11
  get_active_span,
29
- get_provider,
30
- get_exporter,
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.2"
16
+ __version__ = "0.1.4"
44
17
 
45
18
  __all__ = [
46
19
  "WithTracing",
47
- "flush_tracing",
20
+ "flush_spans",
48
21
  "shutdown_tracing",
49
22
  "set_span_attribute",
50
23
  "set_span_name",
51
24
  "get_active_span",
52
- "get_provider",
53
- "get_exporter",
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("AIQA")
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
- logger.debug(f"AIQA export() called with {len(spans)} spans")
72
- # Serialize and add to buffer, deduplicating by (traceId, spanId)
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
- if duplicates_count > 0:
90
- logger.debug(
91
- f"export() added {len(serialized_spans)} span(s) to buffer, skipped {duplicates_count} duplicate(s). "
92
- f"Total buffered: {buffer_size}"
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": self._get_instrumentation_name(),
162
- "version": self._get_instrumentation_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. Rebuilds the span keys tracking set.
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, clear their keys to free memory
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, data=data, headers=headers) as response:
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, clear their keys to free memory
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