aiqa-client 0.1.6__py3-none-any.whl → 0.2.0__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 -7
- aiqa/aiqa_exporter.py +1 -1
- aiqa/client.py +49 -0
- aiqa/tracing.py +346 -133
- {aiqa_client-0.1.6.dist-info → aiqa_client-0.2.0.dist-info}/METADATA +1 -1
- aiqa_client-0.2.0.dist-info/RECORD +10 -0
- aiqa_client-0.1.6.dist-info/RECORD +0 -9
- {aiqa_client-0.1.6.dist-info → aiqa_client-0.2.0.dist-info}/WHEEL +0 -0
- {aiqa_client-0.1.6.dist-info → aiqa_client-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {aiqa_client-0.1.6.dist-info → aiqa_client-0.2.0.dist-info}/top_level.txt +0 -0
aiqa/__init__.py
CHANGED
|
@@ -4,26 +4,26 @@ Python client for AIQA server - OpenTelemetry tracing decorators.
|
|
|
4
4
|
|
|
5
5
|
from .tracing import (
|
|
6
6
|
WithTracing,
|
|
7
|
-
|
|
7
|
+
flush_tracing,
|
|
8
8
|
shutdown_tracing,
|
|
9
9
|
set_span_attribute,
|
|
10
10
|
set_span_name,
|
|
11
11
|
get_active_span,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
get_provider,
|
|
13
|
+
get_exporter,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
__version__ = "0.
|
|
16
|
+
__version__ = "0.2.0"
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"WithTracing",
|
|
20
|
-
"
|
|
20
|
+
"flush_tracing",
|
|
21
21
|
"shutdown_tracing",
|
|
22
22
|
"set_span_attribute",
|
|
23
23
|
"set_span_name",
|
|
24
24
|
"get_active_span",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"get_provider",
|
|
26
|
+
"get_exporter",
|
|
27
27
|
"__version__",
|
|
28
28
|
]
|
|
29
29
|
|
aiqa/aiqa_exporter.py
CHANGED
|
@@ -65,7 +65,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
65
65
|
if not spans:
|
|
66
66
|
logger.debug("export() called with empty spans list")
|
|
67
67
|
return SpanExportResult.SUCCESS
|
|
68
|
-
|
|
68
|
+
logger.debug(f"AIQA export() called with {len(spans)} spans")
|
|
69
69
|
# Serialize and add to buffer
|
|
70
70
|
with self.buffer_lock:
|
|
71
71
|
serialized_spans = [self._serialize_span(span) for span in spans]
|
aiqa/client.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# aiqa/client.py
|
|
2
|
+
import os
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
6
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
7
|
+
|
|
8
|
+
from .aiqa_exporter import AIQASpanExporter
|
|
9
|
+
|
|
10
|
+
AIQA_TRACER_NAME = "aiqa-tracer"
|
|
11
|
+
|
|
12
|
+
client = {
|
|
13
|
+
"provider": None,
|
|
14
|
+
"exporter": None,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@lru_cache(maxsize=1)
|
|
18
|
+
def get_client():
|
|
19
|
+
global client
|
|
20
|
+
_init_tracing()
|
|
21
|
+
# optionally return a richer client object; for now you just need init
|
|
22
|
+
return client
|
|
23
|
+
|
|
24
|
+
def _init_tracing():
|
|
25
|
+
provider = trace.get_tracer_provider()
|
|
26
|
+
|
|
27
|
+
# If it's still the default proxy, install a real SDK provider
|
|
28
|
+
if not isinstance(provider, TracerProvider):
|
|
29
|
+
provider = TracerProvider()
|
|
30
|
+
trace.set_tracer_provider(provider)
|
|
31
|
+
|
|
32
|
+
# Idempotently add your processor
|
|
33
|
+
_attach_aiqa_processor(provider)
|
|
34
|
+
global client
|
|
35
|
+
client["provider"] = provider
|
|
36
|
+
|
|
37
|
+
def _attach_aiqa_processor(provider: TracerProvider):
|
|
38
|
+
# Avoid double-adding if get_client() is called multiple times
|
|
39
|
+
for p in provider._active_span_processor._span_processors:
|
|
40
|
+
if isinstance(getattr(p, "exporter", None), AIQASpanExporter):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
exporter = AIQASpanExporter(
|
|
44
|
+
server_url=os.getenv("AIQA_SERVER_URL"),
|
|
45
|
+
api_key=os.getenv("AIQA_API_KEY"),
|
|
46
|
+
)
|
|
47
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
48
|
+
global client
|
|
49
|
+
client["exporter"] = exporter
|
aiqa/tracing.py
CHANGED
|
@@ -18,31 +18,12 @@ from opentelemetry.semconv.resource import ResourceAttributes
|
|
|
18
18
|
from opentelemetry.trace import Status, StatusCode
|
|
19
19
|
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
20
20
|
from .aiqa_exporter import AIQASpanExporter
|
|
21
|
+
from .client import get_client, AIQA_TRACER_NAME
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
23
24
|
|
|
24
|
-
# Load environment variables
|
|
25
|
-
exporter = AIQASpanExporter()
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
# Use ALWAYS_ON sampler to ensure all spans are recorded (important for FastAPI async contexts)
|
|
29
|
-
provider = TracerProvider(
|
|
30
|
-
resource=Resource.create(
|
|
31
|
-
{
|
|
32
|
-
ResourceAttributes.SERVICE_NAME: os.getenv("OTEL_SERVICE_NAME", "aiqa-service"),
|
|
33
|
-
}
|
|
34
|
-
),
|
|
35
|
-
sampler=ALWAYS_ON
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
39
|
-
trace.set_tracer_provider(provider)
|
|
40
|
-
|
|
41
|
-
# Get a tracer instance
|
|
42
|
-
tracer = trace.get_tracer("aiqa-tracer")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
async def flush_spans() -> None:
|
|
26
|
+
async def flush_tracing() -> None:
|
|
46
27
|
"""
|
|
47
28
|
Flush all pending spans to the server.
|
|
48
29
|
Flushes also happen automatically every few seconds. So you only need to call this function
|
|
@@ -50,8 +31,11 @@ async def flush_spans() -> None:
|
|
|
50
31
|
|
|
51
32
|
This flushes both the BatchSpanProcessor and the exporter buffer.
|
|
52
33
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
client = get_client()
|
|
35
|
+
if client.get("provider"):
|
|
36
|
+
client["provider"].force_flush() # Synchronous method
|
|
37
|
+
if client.get("exporter"):
|
|
38
|
+
await client["exporter"].flush()
|
|
55
39
|
|
|
56
40
|
|
|
57
41
|
async def shutdown_tracing() -> None:
|
|
@@ -59,12 +43,18 @@ async def shutdown_tracing() -> None:
|
|
|
59
43
|
Shutdown the tracer provider and exporter.
|
|
60
44
|
It is not necessary to call this function.
|
|
61
45
|
"""
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
try:
|
|
47
|
+
client = get_client()
|
|
48
|
+
if client.get("provider"):
|
|
49
|
+
client["provider"].shutdown() # Synchronous method
|
|
50
|
+
if client.get("exporter"):
|
|
51
|
+
await client["exporter"].shutdown() # async method
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Error shutting down tracing: {e}", exc_info=True)
|
|
64
54
|
|
|
65
55
|
|
|
66
|
-
# Export provider and exporter for advanced usage
|
|
67
|
-
__all__ = ["
|
|
56
|
+
# Export provider and exporter accessors for advanced usage
|
|
57
|
+
__all__ = ["get_provider", "get_exporter", "flush_tracing", "shutdown_tracing", "WithTracing", "set_span_attribute", "set_span_name", "get_active_span"]
|
|
68
58
|
|
|
69
59
|
|
|
70
60
|
class TracingOptions:
|
|
@@ -129,6 +119,181 @@ def _prepare_input(args: tuple, kwargs: dict) -> Any:
|
|
|
129
119
|
return result
|
|
130
120
|
|
|
131
121
|
|
|
122
|
+
def _prepare_and_filter_input(
|
|
123
|
+
args: tuple,
|
|
124
|
+
kwargs: dict,
|
|
125
|
+
filter_input: Optional[Callable[[Any], Any]],
|
|
126
|
+
ignore_input: Optional[Any],
|
|
127
|
+
) -> Any:
|
|
128
|
+
"""Prepare and filter input for span attributes."""
|
|
129
|
+
input_data = _prepare_input(args, kwargs)
|
|
130
|
+
if filter_input:
|
|
131
|
+
input_data = filter_input(input_data)
|
|
132
|
+
if ignore_input and isinstance(input_data, dict):
|
|
133
|
+
for key in ignore_input:
|
|
134
|
+
if key in input_data:
|
|
135
|
+
del input_data[key]
|
|
136
|
+
return input_data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _prepare_and_filter_output(
|
|
140
|
+
result: Any,
|
|
141
|
+
filter_output: Optional[Callable[[Any], Any]],
|
|
142
|
+
ignore_output: Optional[Any],
|
|
143
|
+
) -> Any:
|
|
144
|
+
"""Prepare and filter output for span attributes."""
|
|
145
|
+
output_data = result
|
|
146
|
+
if filter_output:
|
|
147
|
+
output_data = filter_output(output_data)
|
|
148
|
+
if ignore_output and isinstance(output_data, dict):
|
|
149
|
+
output_data = output_data.copy()
|
|
150
|
+
for key in ignore_output:
|
|
151
|
+
if key in output_data:
|
|
152
|
+
del output_data[key]
|
|
153
|
+
return output_data
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _handle_span_exception(span: trace.Span, exception: Exception) -> None:
|
|
157
|
+
"""Record exception on span and set error status."""
|
|
158
|
+
error = exception if isinstance(exception, Exception) else Exception(str(exception))
|
|
159
|
+
span.record_exception(error)
|
|
160
|
+
span.set_status(Status(StatusCode.ERROR, str(error)))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TracedGenerator:
|
|
164
|
+
"""Wrapper for sync generators that traces iteration."""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
generator: Any,
|
|
169
|
+
span: trace.Span,
|
|
170
|
+
fn_name: str,
|
|
171
|
+
filter_output: Optional[Callable[[Any], Any]],
|
|
172
|
+
ignore_output: Optional[Any],
|
|
173
|
+
context_token: Any,
|
|
174
|
+
):
|
|
175
|
+
self._generator = generator
|
|
176
|
+
self._span = span
|
|
177
|
+
self._fn_name = fn_name
|
|
178
|
+
self._filter_output = filter_output
|
|
179
|
+
self._ignore_output = ignore_output
|
|
180
|
+
self._context_token = context_token
|
|
181
|
+
self._yielded_values = []
|
|
182
|
+
self._exhausted = False
|
|
183
|
+
|
|
184
|
+
def __iter__(self):
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
def __next__(self):
|
|
188
|
+
if self._exhausted:
|
|
189
|
+
raise StopIteration
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
value = next(self._generator)
|
|
193
|
+
self._yielded_values.append(value)
|
|
194
|
+
return value
|
|
195
|
+
except StopIteration:
|
|
196
|
+
self._exhausted = True
|
|
197
|
+
self._finalize_span_success()
|
|
198
|
+
trace.context_api.detach(self._context_token)
|
|
199
|
+
self._span.end()
|
|
200
|
+
raise
|
|
201
|
+
except Exception as exception:
|
|
202
|
+
self._exhausted = True
|
|
203
|
+
_handle_span_exception(self._span, exception)
|
|
204
|
+
trace.context_api.detach(self._context_token)
|
|
205
|
+
self._span.end()
|
|
206
|
+
raise
|
|
207
|
+
|
|
208
|
+
def _finalize_span_success(self):
|
|
209
|
+
"""Set output and success status on span."""
|
|
210
|
+
# Record summary of yielded values
|
|
211
|
+
output_data = {
|
|
212
|
+
"type": "generator",
|
|
213
|
+
"yielded_count": len(self._yielded_values),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Optionally include sample values (limit to avoid huge spans)
|
|
217
|
+
if self._yielded_values:
|
|
218
|
+
sample_size = min(10, len(self._yielded_values))
|
|
219
|
+
output_data["sample_values"] = [
|
|
220
|
+
_serialize_for_span(v) for v in self._yielded_values[:sample_size]
|
|
221
|
+
]
|
|
222
|
+
if len(self._yielded_values) > sample_size:
|
|
223
|
+
output_data["truncated"] = True
|
|
224
|
+
|
|
225
|
+
output_data = _prepare_and_filter_output(output_data, self._filter_output, self._ignore_output)
|
|
226
|
+
self._span.set_attribute("output", _serialize_for_span(output_data))
|
|
227
|
+
self._span.set_status(Status(StatusCode.OK))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TracedAsyncGenerator:
|
|
231
|
+
"""Wrapper for async generators that traces iteration."""
|
|
232
|
+
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
generator: Any,
|
|
236
|
+
span: trace.Span,
|
|
237
|
+
fn_name: str,
|
|
238
|
+
filter_output: Optional[Callable[[Any], Any]],
|
|
239
|
+
ignore_output: Optional[Any],
|
|
240
|
+
context_token: Any,
|
|
241
|
+
):
|
|
242
|
+
self._generator = generator
|
|
243
|
+
self._span = span
|
|
244
|
+
self._fn_name = fn_name
|
|
245
|
+
self._filter_output = filter_output
|
|
246
|
+
self._ignore_output = ignore_output
|
|
247
|
+
self._context_token = context_token
|
|
248
|
+
self._yielded_values = []
|
|
249
|
+
self._exhausted = False
|
|
250
|
+
|
|
251
|
+
def __aiter__(self):
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
async def __anext__(self):
|
|
255
|
+
if self._exhausted:
|
|
256
|
+
raise StopAsyncIteration
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
value = await self._generator.__anext__()
|
|
260
|
+
self._yielded_values.append(value)
|
|
261
|
+
return value
|
|
262
|
+
except StopAsyncIteration:
|
|
263
|
+
self._exhausted = True
|
|
264
|
+
self._finalize_span_success()
|
|
265
|
+
trace.context_api.detach(self._context_token)
|
|
266
|
+
self._span.end()
|
|
267
|
+
raise
|
|
268
|
+
except Exception as exception:
|
|
269
|
+
self._exhausted = True
|
|
270
|
+
_handle_span_exception(self._span, exception)
|
|
271
|
+
trace.context_api.detach(self._context_token)
|
|
272
|
+
self._span.end()
|
|
273
|
+
raise
|
|
274
|
+
|
|
275
|
+
def _finalize_span_success(self):
|
|
276
|
+
"""Set output and success status on span."""
|
|
277
|
+
# Record summary of yielded values
|
|
278
|
+
output_data = {
|
|
279
|
+
"type": "async_generator",
|
|
280
|
+
"yielded_count": len(self._yielded_values),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Optionally include sample values (limit to avoid huge spans)
|
|
284
|
+
if self._yielded_values:
|
|
285
|
+
sample_size = min(10, len(self._yielded_values))
|
|
286
|
+
output_data["sample_values"] = [
|
|
287
|
+
_serialize_for_span(v) for v in self._yielded_values[:sample_size]
|
|
288
|
+
]
|
|
289
|
+
if len(self._yielded_values) > sample_size:
|
|
290
|
+
output_data["truncated"] = True
|
|
291
|
+
|
|
292
|
+
output_data = _prepare_and_filter_output(output_data, self._filter_output, self._ignore_output)
|
|
293
|
+
self._span.set_attribute("output", _serialize_for_span(output_data))
|
|
294
|
+
self._span.set_status(Status(StatusCode.OK))
|
|
295
|
+
|
|
296
|
+
|
|
132
297
|
def WithTracing(
|
|
133
298
|
func: Optional[Callable] = None,
|
|
134
299
|
*,
|
|
@@ -142,7 +307,7 @@ def WithTracing(
|
|
|
142
307
|
Decorator to automatically create spans for function calls.
|
|
143
308
|
Records input/output as span attributes. Spans are automatically linked via OpenTelemetry context.
|
|
144
309
|
|
|
145
|
-
Works with
|
|
310
|
+
Works with synchronous functions, asynchronous functions, generator functions, and async generator functions.
|
|
146
311
|
|
|
147
312
|
Args:
|
|
148
313
|
func: The function to trace (when used as @WithTracing)
|
|
@@ -161,6 +326,16 @@ def WithTracing(
|
|
|
161
326
|
async def my_async_function(x, y):
|
|
162
327
|
return x + y
|
|
163
328
|
|
|
329
|
+
@WithTracing
|
|
330
|
+
def my_generator(n):
|
|
331
|
+
for i in range(n):
|
|
332
|
+
yield i * 2
|
|
333
|
+
|
|
334
|
+
@WithTracing
|
|
335
|
+
async def my_async_generator(n):
|
|
336
|
+
for i in range(n):
|
|
337
|
+
yield i * 2
|
|
338
|
+
|
|
164
339
|
@WithTracing(name="custom_name")
|
|
165
340
|
def another_function():
|
|
166
341
|
pass
|
|
@@ -174,64 +349,137 @@ def WithTracing(
|
|
|
174
349
|
return fn
|
|
175
350
|
|
|
176
351
|
is_async = inspect.iscoroutinefunction(fn)
|
|
352
|
+
is_generator = inspect.isgeneratorfunction(fn)
|
|
353
|
+
is_async_generator = inspect.isasyncgenfunction(fn) if hasattr(inspect, 'isasyncgenfunction') else False
|
|
354
|
+
|
|
355
|
+
tracer = trace.get_tracer(AIQA_TRACER_NAME)
|
|
356
|
+
|
|
357
|
+
def _setup_span(span: trace.Span, input_data: Any) -> bool:
|
|
358
|
+
"""Setup span with input data. Returns True if span is recording."""
|
|
359
|
+
if not span.is_recording():
|
|
360
|
+
logger.warning(f"Span {fn_name} is not recording - will not be exported")
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
logger.debug(f"Span {fn_name} is recording, trace_id={format(span.get_span_context().trace_id, '032x')}")
|
|
364
|
+
|
|
365
|
+
if input_data is not None:
|
|
366
|
+
span.set_attribute("input", _serialize_for_span(input_data))
|
|
367
|
+
|
|
368
|
+
trace_id = format(span.get_span_context().trace_id, "032x")
|
|
369
|
+
logger.debug(f"do traceable stuff {fn_name} {trace_id}")
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
def _finalize_span_success(span: trace.Span, result: Any) -> None:
|
|
373
|
+
"""Set output and success status on span."""
|
|
374
|
+
output_data = _prepare_and_filter_output(result, filter_output, ignore_output)
|
|
375
|
+
span.set_attribute("output", _serialize_for_span(output_data))
|
|
376
|
+
span.set_status(Status(StatusCode.OK))
|
|
377
|
+
|
|
378
|
+
def _execute_with_span_sync(executor: Callable[[], Any], input_data: Any) -> Any:
|
|
379
|
+
"""Execute sync function within span context, handling input/output and exceptions."""
|
|
380
|
+
with tracer.start_as_current_span(fn_name) as span:
|
|
381
|
+
if not _setup_span(span, input_data):
|
|
382
|
+
return executor()
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
result = executor()
|
|
386
|
+
_finalize_span_success(span, result)
|
|
387
|
+
return result
|
|
388
|
+
except Exception as exception:
|
|
389
|
+
_handle_span_exception(span, exception)
|
|
390
|
+
raise
|
|
391
|
+
|
|
392
|
+
async def _execute_with_span_async(executor: Callable[[], Any], input_data: Any) -> Any:
|
|
393
|
+
"""Execute async function within span context, handling input/output and exceptions."""
|
|
394
|
+
with tracer.start_as_current_span(fn_name) as span:
|
|
395
|
+
if not _setup_span(span, input_data):
|
|
396
|
+
return await executor()
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
result = await executor()
|
|
400
|
+
_finalize_span_success(span, result)
|
|
401
|
+
logger.debug(f"Span {fn_name} completed successfully, is_recording={span.is_recording()}")
|
|
402
|
+
return result
|
|
403
|
+
except Exception as exception:
|
|
404
|
+
_handle_span_exception(span, exception)
|
|
405
|
+
raise
|
|
406
|
+
finally:
|
|
407
|
+
logger.debug(f"Span {fn_name} context exiting, is_recording={span.is_recording()}")
|
|
408
|
+
|
|
409
|
+
def _execute_generator_sync(executor: Callable[[], Any], input_data: Any) -> Any:
|
|
410
|
+
"""Execute sync generator function, returning a traced generator."""
|
|
411
|
+
# Create span but don't use 'with' - span will be closed by TracedGenerator
|
|
412
|
+
span = tracer.start_span(fn_name)
|
|
413
|
+
token = trace.context_api.attach(trace.context_api.set_span_in_context(span))
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
if not _setup_span(span, input_data):
|
|
417
|
+
generator = executor()
|
|
418
|
+
trace.context_api.detach(token)
|
|
419
|
+
span.end()
|
|
420
|
+
return generator
|
|
421
|
+
|
|
422
|
+
generator = executor()
|
|
423
|
+
return TracedGenerator(generator, span, fn_name, filter_output, ignore_output, token)
|
|
424
|
+
except Exception as exception:
|
|
425
|
+
trace.context_api.detach(token)
|
|
426
|
+
_handle_span_exception(span, exception)
|
|
427
|
+
span.end()
|
|
428
|
+
raise
|
|
429
|
+
|
|
430
|
+
async def _execute_generator_async(executor: Callable[[], Any], input_data: Any) -> Any:
|
|
431
|
+
"""Execute async generator function, returning a traced async generator."""
|
|
432
|
+
# Create span but don't use 'with' - span will be closed by TracedAsyncGenerator
|
|
433
|
+
span = tracer.start_span(fn_name)
|
|
434
|
+
token = trace.context_api.attach(trace.context_api.set_span_in_context(span))
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
if not _setup_span(span, input_data):
|
|
438
|
+
generator = await executor()
|
|
439
|
+
trace.context_api.detach(token)
|
|
440
|
+
span.end()
|
|
441
|
+
return generator
|
|
442
|
+
|
|
443
|
+
generator = await executor()
|
|
444
|
+
return TracedAsyncGenerator(generator, span, fn_name, filter_output, ignore_output, token)
|
|
445
|
+
except Exception as exception:
|
|
446
|
+
trace.context_api.detach(token)
|
|
447
|
+
_handle_span_exception(span, exception)
|
|
448
|
+
span.end()
|
|
449
|
+
raise
|
|
177
450
|
|
|
178
|
-
if
|
|
451
|
+
if is_async_generator:
|
|
452
|
+
@wraps(fn)
|
|
453
|
+
async def async_gen_traced_fn(*args, **kwargs):
|
|
454
|
+
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input)
|
|
455
|
+
return await _execute_generator_async(
|
|
456
|
+
lambda: fn(*args, **kwargs),
|
|
457
|
+
input_data
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
async_gen_traced_fn._is_traced = True
|
|
461
|
+
logger.debug(f"Function {fn_name} is now traced (async generator)")
|
|
462
|
+
return async_gen_traced_fn
|
|
463
|
+
elif is_generator:
|
|
464
|
+
@wraps(fn)
|
|
465
|
+
def gen_traced_fn(*args, **kwargs):
|
|
466
|
+
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input)
|
|
467
|
+
return _execute_generator_sync(
|
|
468
|
+
lambda: fn(*args, **kwargs),
|
|
469
|
+
input_data
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
gen_traced_fn._is_traced = True
|
|
473
|
+
logger.debug(f"Function {fn_name} is now traced (generator)")
|
|
474
|
+
return gen_traced_fn
|
|
475
|
+
elif is_async:
|
|
179
476
|
@wraps(fn)
|
|
180
477
|
async def async_traced_fn(*args, **kwargs):
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
input_data
|
|
185
|
-
|
|
186
|
-
for key in ignore_input:
|
|
187
|
-
if key in input_data:
|
|
188
|
-
del input_data[key]
|
|
189
|
-
|
|
190
|
-
# Use start_as_current_span to ensure span is recorded by BatchSpanProcessor
|
|
191
|
-
with tracer.start_as_current_span(fn_name) as span:
|
|
192
|
-
# Check if span is recording - if not, spans won't be exported
|
|
193
|
-
if not span.is_recording():
|
|
194
|
-
logger.warning(f"Span {fn_name} is not recording - will not be exported")
|
|
195
|
-
return await fn(*args, **kwargs)
|
|
196
|
-
|
|
197
|
-
logger.debug(f"Span {fn_name} is recording, trace_id={format(span.get_span_context().trace_id, '032x')}")
|
|
198
|
-
|
|
199
|
-
if input_data is not None:
|
|
200
|
-
# Serialize for span attributes (OpenTelemetry only accepts primitives or JSON strings)
|
|
201
|
-
serialized_input = _serialize_for_span(input_data)
|
|
202
|
-
span.set_attribute("input", serialized_input)
|
|
203
|
-
|
|
204
|
-
try:
|
|
205
|
-
# Call the function within the span context
|
|
206
|
-
trace_id = format(span.get_span_context().trace_id, "032x")
|
|
207
|
-
logger.debug(f"do traceable stuff {fn_name} {trace_id}")
|
|
208
|
-
|
|
209
|
-
result = await fn(*args, **kwargs)
|
|
210
|
-
|
|
211
|
-
# Prepare output
|
|
212
|
-
output_data = result
|
|
213
|
-
if filter_output:
|
|
214
|
-
output_data = filter_output(output_data)
|
|
215
|
-
if ignore_output and isinstance(output_data, dict):
|
|
216
|
-
# Make a copy of output_data to avoid modifying the original
|
|
217
|
-
output_data = output_data.copy()
|
|
218
|
-
for key in ignore_output:
|
|
219
|
-
if key in output_data:
|
|
220
|
-
del output_data[key]
|
|
221
|
-
|
|
222
|
-
span.set_attribute("output", _serialize_for_span(output_data))
|
|
223
|
-
span.set_status(Status(StatusCode.OK))
|
|
224
|
-
|
|
225
|
-
logger.debug(f"Span {fn_name} completed successfully, is_recording={span.is_recording()}")
|
|
226
|
-
return result
|
|
227
|
-
except Exception as exception:
|
|
228
|
-
error = exception if isinstance(exception, Exception) else Exception(str(exception))
|
|
229
|
-
span.record_exception(error)
|
|
230
|
-
span.set_status(Status(StatusCode.ERROR, str(error)))
|
|
231
|
-
raise
|
|
232
|
-
finally:
|
|
233
|
-
# Log when span context exits - span should be ended automatically by context manager
|
|
234
|
-
logger.debug(f"Span {fn_name} context exiting, is_recording={span.is_recording()}")
|
|
478
|
+
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input)
|
|
479
|
+
return await _execute_with_span_async(
|
|
480
|
+
lambda: fn(*args, **kwargs),
|
|
481
|
+
input_data
|
|
482
|
+
)
|
|
235
483
|
|
|
236
484
|
async_traced_fn._is_traced = True
|
|
237
485
|
logger.debug(f"Function {fn_name} is now traced (async)")
|
|
@@ -239,56 +487,11 @@ def WithTracing(
|
|
|
239
487
|
else:
|
|
240
488
|
@wraps(fn)
|
|
241
489
|
def sync_traced_fn(*args, **kwargs):
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
input_data
|
|
246
|
-
|
|
247
|
-
for key in ignore_input:
|
|
248
|
-
if key in input_data:
|
|
249
|
-
del input_data[key]
|
|
250
|
-
|
|
251
|
-
# Use start_as_current_span to ensure span is recorded by BatchSpanProcessor
|
|
252
|
-
with tracer.start_as_current_span(fn_name) as span:
|
|
253
|
-
# Check if span is recording - if not, spans won't be exported
|
|
254
|
-
if not span.is_recording():
|
|
255
|
-
logger.warning(f"Span {fn_name} is not recording - will not be exported")
|
|
256
|
-
return fn(*args, **kwargs)
|
|
257
|
-
|
|
258
|
-
logger.debug(f"Span {fn_name} is recording, trace_id={format(span.get_span_context().trace_id, '032x')}")
|
|
259
|
-
|
|
260
|
-
if input_data is not None:
|
|
261
|
-
# Serialize for span attributes (OpenTelemetry only accepts primitives or JSON strings)
|
|
262
|
-
serialized_input = _serialize_for_span(input_data)
|
|
263
|
-
span.set_attribute("input", serialized_input)
|
|
264
|
-
|
|
265
|
-
try:
|
|
266
|
-
# Call the function within the span context
|
|
267
|
-
trace_id = format(span.get_span_context().trace_id, "032x")
|
|
268
|
-
logger.debug(f"do traceable stuff {fn_name} {trace_id}")
|
|
269
|
-
|
|
270
|
-
result = fn(*args, **kwargs)
|
|
271
|
-
|
|
272
|
-
# Prepare output
|
|
273
|
-
output_data = result
|
|
274
|
-
if filter_output:
|
|
275
|
-
output_data = filter_output(output_data)
|
|
276
|
-
if ignore_output and isinstance(output_data, dict):
|
|
277
|
-
# Make a copy of output_data to avoid modifying the original
|
|
278
|
-
output_data = output_data.copy()
|
|
279
|
-
for key in ignore_output:
|
|
280
|
-
if key in output_data:
|
|
281
|
-
del output_data[key]
|
|
282
|
-
|
|
283
|
-
span.set_attribute("output", _serialize_for_span(output_data))
|
|
284
|
-
span.set_status(Status(StatusCode.OK))
|
|
285
|
-
|
|
286
|
-
return result
|
|
287
|
-
except Exception as exception:
|
|
288
|
-
error = exception if isinstance(exception, Exception) else Exception(str(exception))
|
|
289
|
-
span.record_exception(error)
|
|
290
|
-
span.set_status(Status(StatusCode.ERROR, str(error)))
|
|
291
|
-
raise
|
|
490
|
+
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input)
|
|
491
|
+
return _execute_with_span_sync(
|
|
492
|
+
lambda: fn(*args, **kwargs),
|
|
493
|
+
input_data
|
|
494
|
+
)
|
|
292
495
|
|
|
293
496
|
sync_traced_fn._is_traced = True
|
|
294
497
|
logger.debug(f"Function {fn_name} is now traced (sync)")
|
|
@@ -328,3 +531,13 @@ def get_active_span() -> Optional[trace.Span]:
|
|
|
328
531
|
"""Get the currently active span."""
|
|
329
532
|
return trace.get_current_span()
|
|
330
533
|
|
|
534
|
+
def get_provider() -> Optional[TracerProvider]:
|
|
535
|
+
"""Get the tracer provider for advanced usage."""
|
|
536
|
+
client = get_client()
|
|
537
|
+
return client.get("provider")
|
|
538
|
+
|
|
539
|
+
def get_exporter() -> Optional[AIQASpanExporter]:
|
|
540
|
+
"""Get the exporter for advanced usage."""
|
|
541
|
+
client = get_client()
|
|
542
|
+
return client.get("exporter")
|
|
543
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
aiqa/__init__.py,sha256=4svrKrSGkW7IvrCTjF1-WFt7EA9n1eUU9iTCU7fz9k8,490
|
|
2
|
+
aiqa/aiqa_exporter.py,sha256=aEXMfdUL2OILt9H4CRkpoi9EgOqr1UthyQFrimZoDFk,19200
|
|
3
|
+
aiqa/client.py,sha256=1QSZhPdpRJilASuS7YtYlzcTNARY0O6lnQB7m2Jm-jA,1421
|
|
4
|
+
aiqa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
aiqa/tracing.py,sha256=rtMXbcU13nQYepV-KAP5Sugui8Ox-eQCgksRp3VlwpM,20367
|
|
6
|
+
aiqa_client-0.2.0.dist-info/licenses/LICENSE,sha256=kIzkzLuzG0HHaWYm4F4W5FeJ1Yxut3Ec6bhLWyw798A,1062
|
|
7
|
+
aiqa_client-0.2.0.dist-info/METADATA,sha256=SwFlYfBFONEfWjwFv6HNPkQmW99ktiObF21nVLGE0Dc,3772
|
|
8
|
+
aiqa_client-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
aiqa_client-0.2.0.dist-info/top_level.txt,sha256=nwcsuVVSuWu27iLxZd4n1evVzv1W6FVTrSnCXCc-NQs,5
|
|
10
|
+
aiqa_client-0.2.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
aiqa/__init__.py,sha256=KXcAhHf_aZCgs6bn5Gw3IHccunqZzjbj8md0RuWSRAg,470
|
|
2
|
-
aiqa/aiqa_exporter.py,sha256=vXyX6Q_iOjrDz3tCPOMXuBTQg7ocACdOOqzpkUqhy9g,19131
|
|
3
|
-
aiqa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
aiqa/tracing.py,sha256=mWYIZ2Pla91oUY5a6OXOJWUR2eYtjoQkYPaJdBTpho8,13522
|
|
5
|
-
aiqa_client-0.1.6.dist-info/licenses/LICENSE,sha256=kIzkzLuzG0HHaWYm4F4W5FeJ1Yxut3Ec6bhLWyw798A,1062
|
|
6
|
-
aiqa_client-0.1.6.dist-info/METADATA,sha256=Yu-H7t101WTN5rdwJl8-7AAVemL9hHZVxSGjZifurKk,3772
|
|
7
|
-
aiqa_client-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
aiqa_client-0.1.6.dist-info/top_level.txt,sha256=nwcsuVVSuWu27iLxZd4n1evVzv1W6FVTrSnCXCc-NQs,5
|
|
9
|
-
aiqa_client-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|