aiqa-client 0.3.5__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.5/aiqa_client.egg-info → aiqa_client-0.3.7}/PKG-INFO +30 -3
  2. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/README.md +29 -2
  3. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/__init__.py +1 -7
  4. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/aiqa_exporter.py +39 -17
  5. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/client.py +121 -29
  6. aiqa_client-0.3.7/aiqa/constants.py +5 -0
  7. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/tracing.py +24 -35
  8. {aiqa_client-0.3.5 → aiqa_client-0.3.7/aiqa_client.egg-info}/PKG-INFO +30 -3
  9. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa_client.egg-info/SOURCES.txt +1 -0
  10. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/pyproject.toml +1 -1
  11. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/LICENSE +0 -0
  12. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/MANIFEST.in +0 -0
  13. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/experiment_runner.py +0 -0
  14. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/object_serialiser.py +0 -0
  15. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/py.typed +0 -0
  16. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/test_experiment_runner.py +0 -0
  17. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa/test_tracing.py +0 -0
  18. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa_client.egg-info/dependency_links.txt +0 -0
  19. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa_client.egg-info/requires.txt +0 -0
  20. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/aiqa_client.egg-info/top_level.txt +0 -0
  21. {aiqa_client-0.3.5 → aiqa_client-0.3.7}/setup.cfg +0 -0
  22. {aiqa_client-0.3.5 → 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.5
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
@@ -67,6 +67,9 @@ export AIQA_SERVER_URL="http://localhost:3000"
67
67
  export AIQA_API_KEY="your-api-key"
68
68
  ```
69
69
 
70
+ **Note:** If `AIQA_SERVER_URL` or `AIQA_API_KEY` are not set, tracing will be automatically disabled. You'll see one warning message at the start, and your application will continue to run without tracing.
71
+ You can check if tracing is enabled via `get_aiqa_client().enabled`.
72
+
70
73
  ## Usage
71
74
 
72
75
  ### Basic Usage
@@ -131,16 +134,40 @@ asyncio.run(main())
131
134
  To ensure all spans are sent before process exit:
132
135
 
133
136
  ```python
134
- from aiqa import shutdown_tracing
137
+ from aiqa import flush_tracing
135
138
  import asyncio
136
139
 
137
140
  async def main():
138
141
  # Your code here
139
- await shutdown_tracing()
142
+ await flush_tracing()
140
143
 
141
144
  asyncio.run(main())
142
145
  ```
143
146
 
147
+ ### Enabling/Disabling Tracing
148
+
149
+ You can programmatically enable or disable tracing:
150
+
151
+ ```python
152
+ from aiqa import get_aiqa_client
153
+
154
+ client = get_aiqa_client()
155
+
156
+ # Disable tracing (spans won't be created or exported)
157
+ client.enabled = False
158
+
159
+ # Re-enable tracing
160
+ client.enabled = True
161
+
162
+ # Check if tracing is enabled
163
+ if client.enabled:
164
+ print("Tracing is enabled")
165
+ ```
166
+
167
+ When tracing is disabled:
168
+ - Spans are not created (functions execute normally without tracing overhead)
169
+ - Spans are not exported to the server
170
+
144
171
  ### Setting Span Attributes and Names
145
172
 
146
173
  ```python
@@ -30,6 +30,9 @@ export AIQA_SERVER_URL="http://localhost:3000"
30
30
  export AIQA_API_KEY="your-api-key"
31
31
  ```
32
32
 
33
+ **Note:** If `AIQA_SERVER_URL` or `AIQA_API_KEY` are not set, tracing will be automatically disabled. You'll see one warning message at the start, and your application will continue to run without tracing.
34
+ You can check if tracing is enabled via `get_aiqa_client().enabled`.
35
+
33
36
  ## Usage
34
37
 
35
38
  ### Basic Usage
@@ -94,16 +97,40 @@ asyncio.run(main())
94
97
  To ensure all spans are sent before process exit:
95
98
 
96
99
  ```python
97
- from aiqa import shutdown_tracing
100
+ from aiqa import flush_tracing
98
101
  import asyncio
99
102
 
100
103
  async def main():
101
104
  # Your code here
102
- await shutdown_tracing()
105
+ await flush_tracing()
103
106
 
104
107
  asyncio.run(main())
105
108
  ```
106
109
 
110
+ ### Enabling/Disabling Tracing
111
+
112
+ You can programmatically enable or disable tracing:
113
+
114
+ ```python
115
+ from aiqa import get_aiqa_client
116
+
117
+ client = get_aiqa_client()
118
+
119
+ # Disable tracing (spans won't be created or exported)
120
+ client.enabled = False
121
+
122
+ # Re-enable tracing
123
+ client.enabled = True
124
+
125
+ # Check if tracing is enabled
126
+ if client.enabled:
127
+ print("Tracing is enabled")
128
+ ```
129
+
130
+ When tracing is disabled:
131
+ - Spans are not created (functions execute normally without tracing overhead)
132
+ - Spans are not exported to the server
133
+
107
134
  ### Setting Span Attributes and Names
108
135
 
109
136
  ```python
@@ -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,
@@ -40,17 +37,14 @@ from .tracing import (
40
37
  from .client import get_aiqa_client
41
38
  from .experiment_runner import ExperimentRunner
42
39
 
43
- __version__ = "0.3.5"
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
49
  "ExperimentRunner",
56
50
  "get_active_trace_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()
@@ -71,12 +76,30 @@ class AIQASpanExporter(SpanExporter):
71
76
  if not spans:
72
77
  logger.debug("export() called with empty spans list")
73
78
  return SpanExportResult.SUCCESS
79
+
80
+ # Check if AIQA tracing is enabled
81
+ try:
82
+ from .client import get_aiqa_client
83
+ client = get_aiqa_client()
84
+ if not client.enabled:
85
+ logger.debug(f"AIQA export() skipped: tracing is disabled")
86
+ return SpanExportResult.SUCCESS
87
+ except Exception:
88
+ # If we can't check enabled status, proceed (fail open)
89
+ pass
90
+
74
91
  logger.debug(f"AIQA export() called with {len(spans)} spans")
75
92
  # Serialize and add to buffer, deduplicating by (traceId, spanId)
76
93
  with self.buffer_lock:
77
94
  serialized_spans = []
78
95
  duplicates_count = 0
96
+ dropped_count = 0
79
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
+
80
103
  serialized = self._serialize_span(span)
81
104
  span_key = (serialized["traceId"], serialized["spanId"])
82
105
  if span_key not in self.buffer_span_keys:
@@ -88,6 +111,12 @@ class AIQASpanExporter(SpanExporter):
88
111
 
89
112
  self.buffer.extend(serialized_spans)
90
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
+ )
91
120
 
92
121
  if duplicates_count > 0:
93
122
  logger.debug(
@@ -160,10 +189,16 @@ class AIQASpanExporter(SpanExporter):
160
189
  "traceFlags": span_context.trace_flags,
161
190
  "duration": self._time_to_tuple(span.end_time - span.start_time) if span.end_time else None,
162
191
  "ended": span.end_time is not None,
163
- "instrumentationLibrary": {
164
- "name": self._get_instrumentation_name(),
165
- "version": self._get_instrumentation_version(),
166
- },
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__,
167
202
  }
168
203
 
169
204
  def _time_to_tuple(self, nanoseconds: int) -> tuple:
@@ -171,19 +206,6 @@ class AIQASpanExporter(SpanExporter):
171
206
  seconds = int(nanoseconds // 1_000_000_000)
172
207
  nanos = int(nanoseconds % 1_000_000_000)
173
208
  return (seconds, nanos)
174
-
175
- def _get_instrumentation_name(self) -> str:
176
- """Get instrumentation library name - always 'aiqa-tracer'."""
177
- from .client import AIQA_TRACER_NAME
178
- return AIQA_TRACER_NAME
179
-
180
- def _get_instrumentation_version(self) -> Optional[str]:
181
- """Get instrumentation library version from __version__."""
182
- try:
183
- from . import __version__
184
- return __version__
185
- except (ImportError, AttributeError):
186
- return None
187
209
 
188
210
  def _build_request_headers(self) -> Dict[str, str]:
189
211
  """Build HTTP headers for span requests."""
@@ -2,16 +2,20 @@
2
2
  import os
3
3
  import logging
4
4
  from functools import lru_cache
5
+ from typing import Optional, TYPE_CHECKING, Any
5
6
  from opentelemetry import trace
6
7
  from opentelemetry.sdk.trace import TracerProvider
7
8
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
8
9
 
10
+ if TYPE_CHECKING:
11
+ from .aiqa_exporter import AIQASpanExporter
12
+
9
13
  logger = logging.getLogger("AIQA")
10
14
 
11
15
  # Compatibility import for TraceIdRatioBased sampler
12
16
  # In older OpenTelemetry versions it was TraceIdRatioBasedSampler
13
17
  # In newer versions (>=1.24.0) it's TraceIdRatioBased
14
- TraceIdRatioBased = None
18
+ TraceIdRatioBased: Optional[Any] = None
15
19
  try:
16
20
  from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
17
21
  except ImportError:
@@ -27,14 +31,86 @@ except ImportError:
27
31
  # Set to None so we can check later
28
32
  TraceIdRatioBased = None
29
33
 
30
- from .aiqa_exporter import AIQASpanExporter
34
+ from .constants import AIQA_TRACER_NAME
35
+
36
+ class AIQAClient:
37
+ """
38
+ Singleton client for AIQA tracing.
39
+
40
+ This class manages the tracing provider, exporter, and enabled state.
41
+ Access via get_aiqa_client() which returns the singleton instance.
42
+ """
43
+ _instance: Optional['AIQAClient'] = None
44
+
45
+ def __new__(cls):
46
+ if cls._instance is None:
47
+ cls._instance = super().__new__(cls)
48
+ cls._instance._provider: Optional[TracerProvider] = None
49
+ cls._instance._exporter: Optional[AIQASpanExporter] = None
50
+ cls._instance._enabled: bool = True
51
+ cls._instance._initialized: bool = False
52
+ return cls._instance
53
+
54
+ @property
55
+ def provider(self) -> Optional[TracerProvider]:
56
+ """Get the tracer provider."""
57
+ return self._provider
58
+
59
+ @provider.setter
60
+ def provider(self, value: Optional[TracerProvider]) -> None:
61
+ """Set the tracer provider."""
62
+ self._provider = value
63
+
64
+ @property
65
+ def exporter(self) -> Optional[AIQASpanExporter]:
66
+ """Get the span exporter."""
67
+ return self._exporter
68
+
69
+ @exporter.setter
70
+ def exporter(self, value: Optional[AIQASpanExporter]) -> None:
71
+ """Set the span exporter."""
72
+ self._exporter = value
73
+
74
+ @property
75
+ def enabled(self) -> bool:
76
+ """Check if tracing is enabled."""
77
+ return self._enabled
78
+
79
+ @enabled.setter
80
+ def enabled(self, value: bool) -> None:
81
+ """Set the enabled state.
82
+
83
+ When disabled:
84
+ - Tracing does not create spans
85
+ - Export does not send spans
86
+ """
87
+ logger.info(f"AIQA tracing {'enabled' if value else 'disabled'}")
88
+ self._enabled = value
89
+
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
31
110
 
32
- AIQA_TRACER_NAME = "aiqa-tracer"
33
111
 
34
- client = {
35
- "provider": None,
36
- "exporter": None,
37
- }
112
+ # Global singleton instance
113
+ client: AIQAClient = AIQAClient()
38
114
 
39
115
  # Component tag to add to all spans (can be set via AIQA_COMPONENT_TAG env var or programmatically)
40
116
  _component_tag: str = ""
@@ -52,14 +128,17 @@ def set_component_tag(tag: str | None) -> None:
52
128
 
53
129
 
54
130
  @lru_cache(maxsize=1)
55
- def get_aiqa_client():
131
+ def get_aiqa_client() -> AIQAClient:
56
132
  """
57
- Initialize and return the AIQA client.
133
+ Initialize and return the AIQA client singleton.
58
134
 
59
135
  This function must be called before using any AIQA tracing functionality to ensure
60
136
  that environment variables (such as AIQA_SERVER_URL, AIQA_API_KEY, AIQA_COMPONENT_TAG)
61
137
  are properly loaded and the tracing system is initialized.
62
138
 
139
+ The client object manages the tracing system state. Tracing is done by the WithTracing
140
+ decorator. Experiments are run by the ExperimentRunner class.
141
+
63
142
  The function is idempotent - calling it multiple times is safe and will only
64
143
  initialize once.
65
144
 
@@ -67,7 +146,7 @@ def get_aiqa_client():
67
146
  from aiqa import get_aiqa_client, WithTracing
68
147
 
69
148
  # Initialize client (loads env vars)
70
- get_aiqa_client()
149
+ client = get_aiqa_client()
71
150
 
72
151
  @WithTracing
73
152
  def my_function():
@@ -79,12 +158,27 @@ def get_aiqa_client():
79
158
  except Exception as e:
80
159
  logger.error(f"Failed to initialize AIQA tracing: {e}")
81
160
  logger.warning("AIQA tracing is disabled. Your application will continue to run without tracing.")
82
- # optionally return a richer client object; for now you just need init
83
161
  return client
84
162
 
85
- def _init_tracing():
163
+ def _init_tracing() -> None:
86
164
  """Initialize tracing system and load configuration from environment variables."""
165
+ global client
166
+ if client._initialized:
167
+ return
168
+
87
169
  try:
170
+ server_url = os.getenv("AIQA_SERVER_URL")
171
+ api_key = os.getenv("AIQA_API_KEY")
172
+
173
+ if not server_url or not api_key:
174
+ client.enabled = False
175
+ missing_vars = [var for var, val in [("AIQA_SERVER_URL", server_url), ("AIQA_API_KEY", api_key)] if not val]
176
+ logger.warning(
177
+ f"AIQA tracing is disabled: missing required environment variables: {', '.join(missing_vars)}"
178
+ )
179
+ client._initialized = True
180
+ return
181
+
88
182
  # Initialize component tag from environment variable
89
183
  set_component_tag(os.getenv("AIQA_COMPONENT_TAG", None))
90
184
 
@@ -114,21 +208,23 @@ def _init_tracing():
114
208
 
115
209
  # Idempotently add your processor
116
210
  _attach_aiqa_processor(provider)
117
- global client
118
- client["provider"] = provider
211
+ client.provider = provider
119
212
 
120
213
  # Log successful initialization
121
- server_url = os.getenv("AIQA_SERVER_URL", "not configured")
122
214
  logger.info(f"AIQA initialized and tracing (sampling rate: {sampling_rate:.2f}, server: {server_url})")
215
+ client._initialized = True
123
216
 
124
217
  except Exception as e:
125
218
  logger.error(f"Error initializing AIQA tracing: {e}")
219
+ client._initialized = True # Mark as initialized even on error to prevent retry loops
126
220
  raise
127
221
 
128
- def _attach_aiqa_processor(provider: TracerProvider):
222
+ def _attach_aiqa_processor(provider: TracerProvider) -> None:
129
223
  """Attach AIQA span processor to the provider. Idempotent - safe to call multiple times."""
224
+ from .aiqa_exporter import AIQASpanExporter
225
+
130
226
  try:
131
- # Avoid double-adding if get_aiqa_client() is called multiple times
227
+ # Check if already attached
132
228
  for p in provider._active_span_processor._span_processors:
133
229
  if isinstance(getattr(p, "exporter", None), AIQASpanExporter):
134
230
  logger.debug("AIQA span processor already attached, skipping")
@@ -140,7 +236,7 @@ def _attach_aiqa_processor(provider: TracerProvider):
140
236
  )
141
237
  provider.add_span_processor(BatchSpanProcessor(exporter))
142
238
  global client
143
- client["exporter"] = exporter
239
+ client.exporter = exporter
144
240
  logger.debug("AIQA span processor attached successfully")
145
241
  except Exception as e:
146
242
  logger.error(f"Error attaching AIQA span processor: {e}")
@@ -148,23 +244,19 @@ def _attach_aiqa_processor(provider: TracerProvider):
148
244
  raise
149
245
 
150
246
 
151
- def get_aiqa_tracer():
247
+
248
+ def get_aiqa_tracer() -> trace.Tracer:
152
249
  """
153
250
  Get the AIQA tracer with version from __init__.py __version__.
154
- 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.
155
252
  """
156
253
  try:
157
254
  # Import here to avoid circular import
158
255
  from . import __version__
159
-
160
256
  # Compatibility: version parameter may not be supported in older OpenTelemetry versions
161
- try:
162
- # Try with version parameter (newer OpenTelemetry versions)
163
- return trace.get_tracer(AIQA_TRACER_NAME, version=__version__)
164
- except TypeError:
165
- # Fall back to without version parameter (older versions)
166
- 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__)
167
259
  except Exception as e:
168
- logger.error(f"Error getting AIQA tracer: {e}")
169
- # 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")
170
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,35 +26,21 @@ 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
  """
31
33
  client = get_aiqa_client()
32
- if client.get("provider"):
33
- client["provider"].force_flush() # Synchronous method
34
- if client.get("exporter"):
35
- await client["exporter"].flush()
36
-
37
-
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.get("provider"):
46
- client["provider"].shutdown() # Synchronous method
47
- if client.get("exporter"):
48
- client["exporter"].shutdown() # Synchronous method
49
- except Exception as e:
50
- logger.error(f"Error shutting down tracing: {e}")
34
+ if client.provider:
35
+ client.provider.force_flush() # Synchronous method
36
+ if client.exporter:
37
+ await client.exporter.flush()
51
38
 
52
39
 
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"
@@ -640,7 +627,10 @@ def WithTracing(
640
627
  def _execute_with_span_sync(executor: Callable[[], Any], input_data: Any) -> Any:
641
628
  """Execute sync function within span context, handling input/output and exceptions."""
642
629
  # Ensure tracer provider is initialized before creating spans
643
- get_aiqa_client()
630
+ client = get_aiqa_client()
631
+ if not client.enabled:
632
+ return executor()
633
+
644
634
  with tracer.start_as_current_span(fn_name) as span:
645
635
  if not _setup_span(span, input_data):
646
636
  return executor()
@@ -656,7 +646,10 @@ def WithTracing(
656
646
  async def _execute_with_span_async(executor: Callable[[], Any], input_data: Any) -> Any:
657
647
  """Execute async function within span context, handling input/output and exceptions."""
658
648
  # Ensure tracer provider is initialized before creating spans
659
- get_aiqa_client()
649
+ client = get_aiqa_client()
650
+ if not client.enabled:
651
+ return await executor()
652
+
660
653
  with tracer.start_as_current_span(fn_name) as span:
661
654
  if not _setup_span(span, input_data):
662
655
  return await executor()
@@ -675,7 +668,10 @@ def WithTracing(
675
668
  def _execute_generator_sync(executor: Callable[[], Any], input_data: Any) -> Any:
676
669
  """Execute sync generator function, returning a traced generator."""
677
670
  # Ensure tracer provider is initialized before creating spans
678
- get_aiqa_client()
671
+ client = get_aiqa_client()
672
+ if not client.enabled:
673
+ return executor()
674
+
679
675
  # Create span but don't use 'with' - span will be closed by TracedGenerator
680
676
  span = tracer.start_span(fn_name)
681
677
  token = trace.context_api.attach(trace.context_api.set_span_in_context(span))
@@ -698,7 +694,10 @@ def WithTracing(
698
694
  async def _execute_generator_async(executor: Callable[[], Any], input_data: Any) -> Any:
699
695
  """Execute async generator function, returning a traced async generator."""
700
696
  # Ensure tracer provider is initialized before creating spans
701
- get_aiqa_client()
697
+ client = get_aiqa_client()
698
+ if not client.enabled:
699
+ return await executor()
700
+
702
701
  # Create span but don't use 'with' - span will be closed by TracedAsyncGenerator
703
702
  span = tracer.start_span(fn_name)
704
703
  token = trace.context_api.attach(trace.context_api.set_span_in_context(span))
@@ -957,16 +956,6 @@ def set_component_tag(tag: str) -> None:
957
956
  """
958
957
  _set_component_tag(tag)
959
958
 
960
- def get_provider() -> Optional[TracerProvider]:
961
- """Get the tracer provider for advanced usage."""
962
- client = get_aiqa_client()
963
- return client.get("provider")
964
-
965
- def get_exporter() -> Optional[AIQASpanExporter]:
966
- """Get the exporter for advanced usage."""
967
- client = get_aiqa_client()
968
- return client.get("exporter")
969
-
970
959
 
971
960
  def get_active_trace_id() -> Optional[str]:
972
961
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiqa-client
3
- Version: 0.3.5
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
@@ -67,6 +67,9 @@ export AIQA_SERVER_URL="http://localhost:3000"
67
67
  export AIQA_API_KEY="your-api-key"
68
68
  ```
69
69
 
70
+ **Note:** If `AIQA_SERVER_URL` or `AIQA_API_KEY` are not set, tracing will be automatically disabled. You'll see one warning message at the start, and your application will continue to run without tracing.
71
+ You can check if tracing is enabled via `get_aiqa_client().enabled`.
72
+
70
73
  ## Usage
71
74
 
72
75
  ### Basic Usage
@@ -131,16 +134,40 @@ asyncio.run(main())
131
134
  To ensure all spans are sent before process exit:
132
135
 
133
136
  ```python
134
- from aiqa import shutdown_tracing
137
+ from aiqa import flush_tracing
135
138
  import asyncio
136
139
 
137
140
  async def main():
138
141
  # Your code here
139
- await shutdown_tracing()
142
+ await flush_tracing()
140
143
 
141
144
  asyncio.run(main())
142
145
  ```
143
146
 
147
+ ### Enabling/Disabling Tracing
148
+
149
+ You can programmatically enable or disable tracing:
150
+
151
+ ```python
152
+ from aiqa import get_aiqa_client
153
+
154
+ client = get_aiqa_client()
155
+
156
+ # Disable tracing (spans won't be created or exported)
157
+ client.enabled = False
158
+
159
+ # Re-enable tracing
160
+ client.enabled = True
161
+
162
+ # Check if tracing is enabled
163
+ if client.enabled:
164
+ print("Tracing is enabled")
165
+ ```
166
+
167
+ When tracing is disabled:
168
+ - Spans are not created (functions execute normally without tracing overhead)
169
+ - Spans are not exported to the server
170
+
144
171
  ### Setting Span Attributes and Names
145
172
 
146
173
  ```python
@@ -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.5"
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