aiqa-client 0.3.6__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.6/aiqa_client.egg-info → aiqa_client-0.4.0}/PKG-INFO +5 -5
  2. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/README.md +4 -4
  3. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/__init__.py +3 -11
  4. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/aiqa_exporter.py +79 -21
  5. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/client.py +43 -68
  6. aiqa_client-0.4.0/aiqa/constants.py +6 -0
  7. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/tracing.py +4 -27
  8. {aiqa_client-0.3.6 → aiqa_client-0.4.0/aiqa_client.egg-info}/PKG-INFO +5 -5
  9. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa_client.egg-info/SOURCES.txt +1 -0
  10. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/pyproject.toml +1 -1
  11. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/LICENSE +0 -0
  12. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/MANIFEST.in +0 -0
  13. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/experiment_runner.py +0 -0
  14. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/object_serialiser.py +0 -0
  15. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/py.typed +0 -0
  16. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/test_experiment_runner.py +0 -0
  17. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa/test_tracing.py +0 -0
  18. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa_client.egg-info/dependency_links.txt +0 -0
  19. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa_client.egg-info/requires.txt +0 -0
  20. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/aiqa_client.egg-info/top_level.txt +0 -0
  21. {aiqa_client-0.3.6 → aiqa_client-0.4.0}/setup.cfg +0 -0
  22. {aiqa_client-0.3.6 → 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.6
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 shutdown_tracing
137
+ from aiqa import flush_tracing
138
138
  import asyncio
139
139
 
140
140
  async def main():
141
141
  # Your code here
142
- await shutdown_tracing()
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.set_enabled(False)
157
+ client.enabled = False
158
158
 
159
159
  # Re-enable tracing
160
- client.set_enabled(True)
160
+ client.enabled = True
161
161
 
162
162
  # Check if tracing is enabled
163
163
  if client.enabled:
@@ -97,12 +97,12 @@ asyncio.run(main())
97
97
  To ensure all spans are sent before process exit:
98
98
 
99
99
  ```python
100
- from aiqa import shutdown_tracing
100
+ from aiqa import flush_tracing
101
101
  import asyncio
102
102
 
103
103
  async def main():
104
104
  # Your code here
105
- await shutdown_tracing()
105
+ await flush_tracing()
106
106
 
107
107
  asyncio.run(main())
108
108
  ```
@@ -117,10 +117,10 @@ from aiqa import get_aiqa_client
117
117
  client = get_aiqa_client()
118
118
 
119
119
  # Disable tracing (spans won't be created or exported)
120
- client.set_enabled(False)
120
+ client.enabled = False
121
121
 
122
122
  # Re-enable tracing
123
- client.set_enabled(True)
123
+ client.enabled = True
124
124
 
125
125
  # Check if tracing is enabled
126
126
  if client.enabled:
@@ -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, set_enabled
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
- "__version__",
57
+ "VERSION",
66
58
  ]
67
59
 
@@ -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
- "name": self._get_instrumentation_name(),
177
- "version": self._get_instrumentation_version(),
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(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
+ )
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}")
@@ -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 .aiqa_exporter import AIQASpanExporter
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: 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
 
@@ -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
- self._enabled = enabled
96
- if enabled:
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 is_enabled(self) -> bool:
102
- """Check if tracing is enabled."""
103
- return self._enabled
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 (for backward compatibility with direct access)
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
- # Avoid double-adding if get_aiqa_client() is called multiple times
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() to ensure version is set.
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 __version__
273
-
252
+ from . import VERSION
274
253
  # Compatibility: version parameter may not be supported in older OpenTelemetry versions
275
- try:
276
- # Try with version parameter (newer OpenTelemetry versions)
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
- logger.error(f"Error getting AIQA tracer: {e}")
283
- # Return a basic tracer as fallback to prevent crashes
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)
@@ -0,0 +1,6 @@
1
+ """
2
+ Constants used across the AIQA client package.
3
+ """
4
+
5
+ AIQA_TRACER_NAME = "aiqa-tracer"
6
+ VERSION = "0.4.0" # automatically updated by set-version-json.sh
@@ -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, AIQA_TRACER_NAME, get_component_tag, set_component_tag as _set_component_tag, get_aiqa_tracer
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
- "get_provider", "get_exporter", "flush_tracing", "shutdown_tracing", "WithTracing",
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.6
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 shutdown_tracing
137
+ from aiqa import flush_tracing
138
138
  import asyncio
139
139
 
140
140
  async def main():
141
141
  # Your code here
142
- await shutdown_tracing()
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.set_enabled(False)
157
+ client.enabled = False
158
158
 
159
159
  # Re-enable tracing
160
- client.set_enabled(True)
160
+ client.enabled = True
161
161
 
162
162
  # Check if tracing is enabled
163
163
  if client.enabled:
@@ -6,6 +6,7 @@ setup.py
6
6
  aiqa/__init__.py
7
7
  aiqa/aiqa_exporter.py
8
8
  aiqa/client.py
9
+ aiqa/constants.py
9
10
  aiqa/experiment_runner.py
10
11
  aiqa/object_serialiser.py
11
12
  aiqa/py.typed
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aiqa-client"
7
- version = "0.3.6"
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