aiqa-client 0.3.6__tar.gz → 0.3.7__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.3.7}/PKG-INFO +5 -5
  2. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/README.md +4 -4
  3. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/__init__.py +2 -9
  4. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/aiqa_exporter.py +27 -17
  5. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/client.py +42 -64
  6. aiqa_client-0.3.7/aiqa/constants.py +5 -0
  7. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/tracing.py +4 -27
  8. {aiqa_client-0.3.6 → aiqa_client-0.3.7/aiqa_client.egg-info}/PKG-INFO +5 -5
  9. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa_client.egg-info/SOURCES.txt +1 -0
  10. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/pyproject.toml +1 -1
  11. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/LICENSE +0 -0
  12. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/MANIFEST.in +0 -0
  13. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/experiment_runner.py +0 -0
  14. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/object_serialiser.py +0 -0
  15. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/py.typed +0 -0
  16. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/test_experiment_runner.py +0 -0
  17. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa/test_tracing.py +0 -0
  18. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa_client.egg-info/dependency_links.txt +0 -0
  19. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa_client.egg-info/requires.txt +0 -0
  20. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/aiqa_client.egg-info/top_level.txt +0 -0
  21. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/setup.cfg +0 -0
  22. {aiqa_client-0.3.6 → aiqa_client-0.3.7}/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.3.7
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,18 @@ 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
39
 
43
- __version__ = "0.3.6"
40
+ __version__ = "0.3.7"
44
41
 
45
42
  __all__ = [
46
43
  "WithTracing",
47
44
  "flush_tracing",
48
- "shutdown_tracing",
49
45
  "set_span_attribute",
50
46
  "set_span_name",
51
47
  "get_active_span",
52
- "get_provider",
53
- "get_exporter",
54
48
  "get_aiqa_client",
55
- "set_enabled",
56
49
  "ExperimentRunner",
57
50
  "get_active_trace_id",
58
51
  "get_span_id",
@@ -13,6 +13,9 @@ from typing import List, Dict, Any, Optional
13
13
  from opentelemetry.sdk.trace import ReadableSpan
14
14
  from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
15
15
 
16
+ from .constants import AIQA_TRACER_NAME
17
+ from . import __version__
18
+
16
19
  logger = logging.getLogger("AIQA")
17
20
 
18
21
 
@@ -28,6 +31,7 @@ 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)
31
35
  ):
32
36
  """
33
37
  Initialize the AIQA span exporter.
@@ -42,6 +46,7 @@ class AIQASpanExporter(SpanExporter):
42
46
  self._api_key = api_key
43
47
  self.flush_interval_ms = flush_interval_seconds * 1000
44
48
  self.max_batch_size_bytes = max_batch_size_bytes
49
+ self.max_buffer_spans = max_buffer_spans
45
50
  self.buffer: List[Dict[str, Any]] = []
46
51
  self.buffer_span_keys: set = set() # Track (traceId, spanId) tuples to prevent duplicates (Python 3.8 compatible)
47
52
  self.buffer_lock = threading.Lock()
@@ -88,7 +93,13 @@ class AIQASpanExporter(SpanExporter):
88
93
  with self.buffer_lock:
89
94
  serialized_spans = []
90
95
  duplicates_count = 0
96
+ dropped_count = 0
91
97
  for span in spans:
98
+ # Check if buffer is full (prevent unbounded growth)
99
+ if len(self.buffer) >= self.max_buffer_spans:
100
+ dropped_count += 1
101
+ continue
102
+
92
103
  serialized = self._serialize_span(span)
93
104
  span_key = (serialized["traceId"], serialized["spanId"])
94
105
  if span_key not in self.buffer_span_keys:
@@ -100,6 +111,12 @@ class AIQASpanExporter(SpanExporter):
100
111
 
101
112
  self.buffer.extend(serialized_spans)
102
113
  buffer_size = len(self.buffer)
114
+
115
+ if dropped_count > 0:
116
+ logger.warning(
117
+ f"WARNING: Buffer full ({buffer_size} spans), dropped {dropped_count} span(s). "
118
+ f"Consider increasing max_buffer_spans or fixing server connectivity."
119
+ )
103
120
 
104
121
  if duplicates_count > 0:
105
122
  logger.debug(
@@ -172,10 +189,16 @@ class AIQASpanExporter(SpanExporter):
172
189
  "traceFlags": span_context.trace_flags,
173
190
  "duration": self._time_to_tuple(span.end_time - span.start_time) if span.end_time else None,
174
191
  "ended": span.end_time is not None,
175
- "instrumentationLibrary": {
176
- "name": self._get_instrumentation_name(),
177
- "version": self._get_instrumentation_version(),
178
- },
192
+ "instrumentationLibrary": self._get_instrumentation_library(span),
193
+ }
194
+
195
+ def _get_instrumentation_library(self, span: ReadableSpan) -> Dict[str, Any]:
196
+ """
197
+ Get instrumentation library information from the span: just use the package version.
198
+ """
199
+ return {
200
+ "name": AIQA_TRACER_NAME,
201
+ "version": __version__,
179
202
  }
180
203
 
181
204
  def _time_to_tuple(self, nanoseconds: int) -> tuple:
@@ -183,19 +206,6 @@ class AIQASpanExporter(SpanExporter):
183
206
  seconds = int(nanoseconds // 1_000_000_000)
184
207
  nanos = int(nanoseconds % 1_000_000_000)
185
208
  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
209
 
200
210
  def _build_request_headers(self) -> Dict[str, str]:
201
211
  """Build HTTP headers for span requests."""
@@ -2,17 +2,20 @@
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
9
9
 
10
+ if TYPE_CHECKING:
11
+ from .aiqa_exporter import AIQASpanExporter
12
+
10
13
  logger = logging.getLogger("AIQA")
11
14
 
12
15
  # Compatibility import for TraceIdRatioBased sampler
13
16
  # In older OpenTelemetry versions it was TraceIdRatioBasedSampler
14
17
  # In newer versions (>=1.24.0) it's TraceIdRatioBased
15
- TraceIdRatioBased = None
18
+ TraceIdRatioBased: Optional[Any] = None
16
19
  try:
17
20
  from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
18
21
  except ImportError:
@@ -28,10 +31,7 @@ except ImportError:
28
31
  # Set to None so we can check later
29
32
  TraceIdRatioBased = None
30
33
 
31
- from .aiqa_exporter import AIQASpanExporter
32
-
33
- AIQA_TRACER_NAME = "aiqa-tracer"
34
-
34
+ from .constants import AIQA_TRACER_NAME
35
35
 
36
36
  class AIQAClient:
37
37
  """
@@ -78,32 +78,38 @@ class AIQAClient:
78
78
 
79
79
  @enabled.setter
80
80
  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.
81
+ """Set the enabled state.
87
82
 
88
83
  When disabled:
89
84
  - Tracing does not create spans
90
85
  - Export does not send spans
91
-
92
- Args:
93
- enabled: True to enable tracing, False to disable
94
86
  """
95
- self._enabled = enabled
96
- if enabled:
97
- logger.info("AIQA tracing enabled")
98
- else:
99
- logger.info("AIQA tracing disabled")
87
+ logger.info(f"AIQA tracing {'enabled' if value else 'disabled'}")
88
+ self._enabled = value
100
89
 
101
- def is_enabled(self) -> bool:
102
- """Check if tracing is enabled."""
103
- return self._enabled
90
+ def shutdown(self) -> None:
91
+ """
92
+ Shutdown the tracer provider and exporter.
93
+ It is not necessary to call this function.
94
+ Use this to clean up resources at the end of all tracing.
95
+
96
+ This will also set enabled=False to prevent further tracing attempts.
97
+ """
98
+ try:
99
+ logger.info("AIQA tracing shutting down")
100
+ # Disable tracing to prevent attempts to use shut-down system
101
+ self.enabled = False
102
+ if self._provider:
103
+ self._provider.shutdown()
104
+ if self._exporter:
105
+ self._exporter.shutdown()
106
+ except Exception as e:
107
+ logger.error(f"Error shutting down tracing: {e}")
108
+ # Still disable even if shutdown had errors
109
+ self.enabled = False
104
110
 
105
111
 
106
- # Global singleton instance (for backward compatibility with direct access)
112
+ # Global singleton instance
107
113
  client: AIQAClient = AIQAClient()
108
114
 
109
115
  # Component tag to add to all spans (can be set via AIQA_COMPONENT_TAG env var or programmatically)
@@ -154,24 +160,19 @@ def get_aiqa_client() -> AIQAClient:
154
160
  logger.warning("AIQA tracing is disabled. Your application will continue to run without tracing.")
155
161
  return client
156
162
 
157
- def _init_tracing():
163
+ def _init_tracing() -> None:
158
164
  """Initialize tracing system and load configuration from environment variables."""
159
165
  global client
160
166
  if client._initialized:
161
167
  return
162
168
 
163
169
  try:
164
- # Check for required environment variables
165
170
  server_url = os.getenv("AIQA_SERVER_URL")
166
171
  api_key = os.getenv("AIQA_API_KEY")
167
172
 
168
173
  if not server_url or not api_key:
169
174
  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")
175
+ missing_vars = [var for var, val in [("AIQA_SERVER_URL", server_url), ("AIQA_API_KEY", api_key)] if not val]
175
176
  logger.warning(
176
177
  f"AIQA tracing is disabled: missing required environment variables: {', '.join(missing_vars)}"
177
178
  )
@@ -218,10 +219,12 @@ def _init_tracing():
218
219
  client._initialized = True # Mark as initialized even on error to prevent retry loops
219
220
  raise
220
221
 
221
- def _attach_aiqa_processor(provider: TracerProvider):
222
+ def _attach_aiqa_processor(provider: TracerProvider) -> None:
222
223
  """Attach AIQA span processor to the provider. Idempotent - safe to call multiple times."""
224
+ from .aiqa_exporter import AIQASpanExporter
225
+
223
226
  try:
224
- # Avoid double-adding if get_aiqa_client() is called multiple times
227
+ # Check if already attached
225
228
  for p in provider._active_span_processor._span_processors:
226
229
  if isinstance(getattr(p, "exporter", None), AIQASpanExporter):
227
230
  logger.debug("AIQA span processor already attached, skipping")
@@ -241,44 +244,19 @@ def _attach_aiqa_processor(provider: TracerProvider):
241
244
  raise
242
245
 
243
246
 
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
247
 
264
-
265
- def get_aiqa_tracer():
248
+ def get_aiqa_tracer() -> trace.Tracer:
266
249
  """
267
250
  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.
251
+ This should be used instead of trace.get_tracer() so that the version is set.
269
252
  """
270
253
  try:
271
254
  # Import here to avoid circular import
272
255
  from . import __version__
273
-
274
256
  # 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)
257
+ # Try with version parameter (newer OpenTelemetry versions)
258
+ return trace.get_tracer(AIQA_TRACER_NAME, version=__version__)
281
259
  except Exception as e:
282
- logger.error(f"Error getting AIQA tracer: {e}")
283
- # Return a basic tracer as fallback to prevent crashes
260
+ # Log issue but still return a tracer
261
+ logger.info(f"Issue getting AIQA tracer with version: {e}, using fallback")
284
262
  return trace.get_tracer(AIQA_TRACER_NAME)
@@ -0,0 +1,5 @@
1
+ """
2
+ Constants used across the AIQA client package.
3
+ """
4
+
5
+ AIQA_TRACER_NAME = "aiqa-tracer"
@@ -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.3.7
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.3.7"
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