aiqa-client 0.1.2__tar.gz → 0.1.4__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.
- {aiqa_client-0.1.2/aiqa_client.egg-info → aiqa_client-0.1.4}/PKG-INFO +4 -94
- aiqa_client-0.1.4/README.md +120 -0
- aiqa_client-0.1.4/aiqa/__init__.py +29 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/aiqa/aiqa_exporter.py +21 -79
- aiqa_client-0.1.4/aiqa/tracing.py +319 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4/aiqa_client.egg-info}/PKG-INFO +4 -94
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/aiqa_client.egg-info/SOURCES.txt +0 -5
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/pyproject.toml +1 -1
- aiqa_client-0.1.2/README.md +0 -210
- aiqa_client-0.1.2/aiqa/__init__.py +0 -66
- aiqa_client-0.1.2/aiqa/client.py +0 -170
- aiqa_client-0.1.2/aiqa/experiment_runner.py +0 -336
- aiqa_client-0.1.2/aiqa/object_serialiser.py +0 -361
- aiqa_client-0.1.2/aiqa/test_experiment_runner.py +0 -176
- aiqa_client-0.1.2/aiqa/test_tracing.py +0 -230
- aiqa_client-0.1.2/aiqa/tracing.py +0 -1256
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/LICENSE +0 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/MANIFEST.in +0 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/aiqa/py.typed +0 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/aiqa_client.egg-info/dependency_links.txt +0 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/aiqa_client.egg-info/requires.txt +0 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/aiqa_client.egg-info/top_level.txt +0 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/setup.cfg +0 -0
- {aiqa_client-0.1.2 → aiqa_client-0.1.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiqa-client
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -72,15 +72,7 @@ export AIQA_API_KEY="your-api-key"
|
|
|
72
72
|
### Basic Usage
|
|
73
73
|
|
|
74
74
|
```python
|
|
75
|
-
from
|
|
76
|
-
from aiqa import get_aiqa_client, WithTracing
|
|
77
|
-
|
|
78
|
-
# Load environment variables from .env file (if using one)
|
|
79
|
-
load_dotenv()
|
|
80
|
-
|
|
81
|
-
# Initialize client (must be called before using WithTracing)
|
|
82
|
-
# This loads environment variables and initializes the tracing system
|
|
83
|
-
get_aiqa_client()
|
|
75
|
+
from aiqa import WithTracing
|
|
84
76
|
|
|
85
77
|
@WithTracing
|
|
86
78
|
def my_function(x, y):
|
|
@@ -116,12 +108,12 @@ def my_function(data):
|
|
|
116
108
|
Spans are automatically flushed every 5 seconds. To flush immediately:
|
|
117
109
|
|
|
118
110
|
```python
|
|
119
|
-
from aiqa import
|
|
111
|
+
from aiqa import flush_spans
|
|
120
112
|
import asyncio
|
|
121
113
|
|
|
122
114
|
async def main():
|
|
123
115
|
# Your code here
|
|
124
|
-
await
|
|
116
|
+
await flush_spans()
|
|
125
117
|
|
|
126
118
|
asyncio.run(main())
|
|
127
119
|
```
|
|
@@ -152,87 +144,6 @@ def my_function():
|
|
|
152
144
|
# ... rest of function
|
|
153
145
|
```
|
|
154
146
|
|
|
155
|
-
### Grouping Traces by Conversation
|
|
156
|
-
|
|
157
|
-
To group multiple traces together that are part of the same conversation or session:
|
|
158
|
-
|
|
159
|
-
```python
|
|
160
|
-
from aiqa import WithTracing, set_conversation_id
|
|
161
|
-
|
|
162
|
-
@WithTracing
|
|
163
|
-
def handle_user_request(user_id: str, session_id: str):
|
|
164
|
-
# Set conversation ID to group all traces for this user session
|
|
165
|
-
set_conversation_id(f"user_{user_id}_session_{session_id}")
|
|
166
|
-
# All spans created in this function and its children will have this gen_ai.conversation.id
|
|
167
|
-
# ... rest of function
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
The `gen_ai.conversation.id` attribute allows you to filter and group traces in the AIQA server by conversation, making it easier to analyze multi-step interactions or user sessions. See the [OpenTelemetry GenAI Events specification](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/) for more details.
|
|
171
|
-
|
|
172
|
-
### Trace ID Propagation Across Services/Agents
|
|
173
|
-
|
|
174
|
-
To link traces across different services or agents, you can extract and propagate trace IDs:
|
|
175
|
-
|
|
176
|
-
#### Getting Current Trace ID
|
|
177
|
-
|
|
178
|
-
```python
|
|
179
|
-
from aiqa import get_trace_id, get_span_id
|
|
180
|
-
|
|
181
|
-
# Get the current trace ID and span ID
|
|
182
|
-
trace_id = get_trace_id() # Returns hex string (32 chars) or None
|
|
183
|
-
span_id = get_span_id() # Returns hex string (16 chars) or None
|
|
184
|
-
|
|
185
|
-
# Pass these to another service (e.g., in HTTP headers, message queue, etc.)
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
#### Continuing a Trace in Another Service
|
|
189
|
-
|
|
190
|
-
```python
|
|
191
|
-
from aiqa import create_span_from_trace_id
|
|
192
|
-
|
|
193
|
-
# Continue a trace from another service/agent
|
|
194
|
-
# trace_id and parent_span_id come from the other service
|
|
195
|
-
with create_span_from_trace_id(
|
|
196
|
-
trace_id="abc123...",
|
|
197
|
-
parent_span_id="def456...",
|
|
198
|
-
span_name="service_b_operation"
|
|
199
|
-
):
|
|
200
|
-
# Your code here - this span will be linked to the original trace
|
|
201
|
-
pass
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
#### Using OpenTelemetry Context Propagation (Recommended)
|
|
205
|
-
|
|
206
|
-
For HTTP requests, use the built-in context propagation:
|
|
207
|
-
|
|
208
|
-
```python
|
|
209
|
-
from aiqa import inject_trace_context, extract_trace_context
|
|
210
|
-
import requests
|
|
211
|
-
from opentelemetry.trace import use_span
|
|
212
|
-
|
|
213
|
-
# In the sending service:
|
|
214
|
-
headers = {}
|
|
215
|
-
inject_trace_context(headers) # Adds trace context to headers
|
|
216
|
-
response = requests.get("http://other-service/api", headers=headers)
|
|
217
|
-
|
|
218
|
-
# In the receiving service:
|
|
219
|
-
# Extract context from incoming request headers
|
|
220
|
-
ctx = extract_trace_context(request.headers)
|
|
221
|
-
|
|
222
|
-
# Use the context to create a span
|
|
223
|
-
from opentelemetry.trace import use_span
|
|
224
|
-
with use_span(ctx):
|
|
225
|
-
# Your code here
|
|
226
|
-
pass
|
|
227
|
-
|
|
228
|
-
# Or create a span with the context
|
|
229
|
-
from opentelemetry import trace
|
|
230
|
-
tracer = trace.get_tracer("aiqa-tracer")
|
|
231
|
-
with tracer.start_as_current_span("operation", context=ctx):
|
|
232
|
-
# Your code here
|
|
233
|
-
pass
|
|
234
|
-
```
|
|
235
|
-
|
|
236
147
|
## Features
|
|
237
148
|
|
|
238
149
|
- Automatic tracing of function calls (sync and async)
|
|
@@ -240,7 +151,6 @@ with tracer.start_as_current_span("operation", context=ctx):
|
|
|
240
151
|
- Automatic error tracking and exception recording
|
|
241
152
|
- Thread-safe span buffering and auto-flushing
|
|
242
153
|
- OpenTelemetry context propagation for nested spans
|
|
243
|
-
- Trace ID propagation utilities for distributed tracing
|
|
244
154
|
|
|
245
155
|
## Example
|
|
246
156
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# A Python client for the AIQA server
|
|
2
|
+
|
|
3
|
+
OpenTelemetry-based client for tracing Python functions and sending traces to the AIQA server.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### From PyPI (recommended)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install aiqa-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### From source
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m venv .venv
|
|
17
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
18
|
+
pip install -r requirements.txt
|
|
19
|
+
pip install -e .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
See [TESTING.md](TESTING.md) for detailed testing instructions.
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
Set the following environment variables:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
export AIQA_SERVER_URL="http://localhost:3000"
|
|
30
|
+
export AIQA_API_KEY="your-api-key"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Basic Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from aiqa import WithTracing
|
|
39
|
+
|
|
40
|
+
@WithTracing
|
|
41
|
+
def my_function(x, y):
|
|
42
|
+
return x + y
|
|
43
|
+
|
|
44
|
+
@WithTracing
|
|
45
|
+
async def my_async_function(x, y):
|
|
46
|
+
await asyncio.sleep(0.1)
|
|
47
|
+
return x * y
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Custom Span Name
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
@WithTracing(name="custom_span_name")
|
|
54
|
+
def my_function():
|
|
55
|
+
pass
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Input/Output Filtering
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
@WithTracing(
|
|
62
|
+
filter_input=lambda x: {"filtered": str(x)},
|
|
63
|
+
filter_output=lambda x: {"result": x}
|
|
64
|
+
)
|
|
65
|
+
def my_function(data):
|
|
66
|
+
return {"processed": data}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Flushing Spans
|
|
70
|
+
|
|
71
|
+
Spans are automatically flushed every 5 seconds. To flush immediately:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from aiqa import flush_spans
|
|
75
|
+
import asyncio
|
|
76
|
+
|
|
77
|
+
async def main():
|
|
78
|
+
# Your code here
|
|
79
|
+
await flush_spans()
|
|
80
|
+
|
|
81
|
+
asyncio.run(main())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Shutting Down
|
|
85
|
+
|
|
86
|
+
To ensure all spans are sent before process exit:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from aiqa import shutdown_tracing
|
|
90
|
+
import asyncio
|
|
91
|
+
|
|
92
|
+
async def main():
|
|
93
|
+
# Your code here
|
|
94
|
+
await shutdown_tracing()
|
|
95
|
+
|
|
96
|
+
asyncio.run(main())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Setting Span Attributes and Names
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from aiqa import set_span_attribute, set_span_name
|
|
103
|
+
|
|
104
|
+
def my_function():
|
|
105
|
+
set_span_attribute("custom.attribute", "value")
|
|
106
|
+
set_span_name("custom_span_name")
|
|
107
|
+
# ... rest of function
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Features
|
|
111
|
+
|
|
112
|
+
- Automatic tracing of function calls (sync and async)
|
|
113
|
+
- Records function inputs and outputs as span attributes
|
|
114
|
+
- Automatic error tracking and exception recording
|
|
115
|
+
- Thread-safe span buffering and auto-flushing
|
|
116
|
+
- OpenTelemetry context propagation for nested spans
|
|
117
|
+
|
|
118
|
+
## Example
|
|
119
|
+
|
|
120
|
+
See `example.py` for a complete working example.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python client for AIQA server - OpenTelemetry tracing decorators.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .tracing import (
|
|
6
|
+
WithTracing,
|
|
7
|
+
flush_spans,
|
|
8
|
+
shutdown_tracing,
|
|
9
|
+
set_span_attribute,
|
|
10
|
+
set_span_name,
|
|
11
|
+
get_active_span,
|
|
12
|
+
provider,
|
|
13
|
+
exporter,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.4"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"WithTracing",
|
|
20
|
+
"flush_spans",
|
|
21
|
+
"shutdown_tracing",
|
|
22
|
+
"set_span_attribute",
|
|
23
|
+
"set_span_name",
|
|
24
|
+
"get_active_span",
|
|
25
|
+
"provider",
|
|
26
|
+
"exporter",
|
|
27
|
+
"__version__",
|
|
28
|
+
]
|
|
29
|
+
|
|
@@ -8,12 +8,11 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import threading
|
|
10
10
|
import time
|
|
11
|
-
import io
|
|
12
11
|
from typing import List, Dict, Any, Optional
|
|
13
12
|
from opentelemetry.sdk.trace import ReadableSpan
|
|
14
13
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
15
14
|
|
|
16
|
-
logger = logging.getLogger(
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class AIQASpanExporter(SpanExporter):
|
|
@@ -40,7 +39,6 @@ class AIQASpanExporter(SpanExporter):
|
|
|
40
39
|
self._api_key = api_key
|
|
41
40
|
self.flush_interval_ms = flush_interval_seconds * 1000
|
|
42
41
|
self.buffer: List[Dict[str, Any]] = []
|
|
43
|
-
self.buffer_span_keys: set = set() # Track (traceId, spanId) tuples to prevent duplicates (Python 3.8 compatible)
|
|
44
42
|
self.buffer_lock = threading.Lock()
|
|
45
43
|
self.flush_lock = threading.Lock()
|
|
46
44
|
self.shutdown_requested = False
|
|
@@ -63,39 +61,21 @@ class AIQASpanExporter(SpanExporter):
|
|
|
63
61
|
def export(self, spans: List[ReadableSpan]) -> SpanExportResult:
|
|
64
62
|
"""
|
|
65
63
|
Export spans to the AIQA server. Adds spans to buffer for async flushing.
|
|
66
|
-
Deduplicates spans based on (traceId, spanId) to prevent repeated exports.
|
|
67
64
|
"""
|
|
68
65
|
if not spans:
|
|
69
66
|
logger.debug("export() called with empty spans list")
|
|
70
67
|
return SpanExportResult.SUCCESS
|
|
71
|
-
|
|
72
|
-
# Serialize and add to buffer
|
|
68
|
+
|
|
69
|
+
# Serialize and add to buffer
|
|
73
70
|
with self.buffer_lock:
|
|
74
|
-
serialized_spans = []
|
|
75
|
-
duplicates_count = 0
|
|
76
|
-
for span in spans:
|
|
77
|
-
serialized = self._serialize_span(span)
|
|
78
|
-
span_key = (serialized["traceId"], serialized["spanId"])
|
|
79
|
-
if span_key not in self.buffer_span_keys:
|
|
80
|
-
serialized_spans.append(serialized)
|
|
81
|
-
self.buffer_span_keys.add(span_key)
|
|
82
|
-
else:
|
|
83
|
-
duplicates_count += 1
|
|
84
|
-
logger.debug(f"export() skipping duplicate span: traceId={serialized['traceId']}, spanId={serialized['spanId']}")
|
|
85
|
-
|
|
71
|
+
serialized_spans = [self._serialize_span(span) for span in spans]
|
|
86
72
|
self.buffer.extend(serialized_spans)
|
|
87
73
|
buffer_size = len(self.buffer)
|
|
88
74
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
else:
|
|
95
|
-
logger.debug(
|
|
96
|
-
f"export() added {len(spans)} span(s) to buffer. "
|
|
97
|
-
f"Total buffered: {buffer_size}"
|
|
98
|
-
)
|
|
75
|
+
logger.debug(
|
|
76
|
+
f"export() added {len(spans)} span(s) to buffer. "
|
|
77
|
+
f"Total buffered: {buffer_size}"
|
|
78
|
+
)
|
|
99
79
|
|
|
100
80
|
return SpanExportResult.SUCCESS
|
|
101
81
|
|
|
@@ -158,8 +138,8 @@ class AIQASpanExporter(SpanExporter):
|
|
|
158
138
|
"duration": self._time_to_tuple(span.end_time - span.start_time) if span.end_time else None,
|
|
159
139
|
"ended": span.end_time is not None,
|
|
160
140
|
"instrumentationLibrary": {
|
|
161
|
-
"name":
|
|
162
|
-
"version":
|
|
141
|
+
"name": span.instrumentation_info.name if hasattr(span, "instrumentation_info") else "",
|
|
142
|
+
"version": span.instrumentation_info.version if hasattr(span, "instrumentation_info") else None,
|
|
163
143
|
},
|
|
164
144
|
}
|
|
165
145
|
|
|
@@ -168,19 +148,6 @@ class AIQASpanExporter(SpanExporter):
|
|
|
168
148
|
seconds = int(nanoseconds // 1_000_000_000)
|
|
169
149
|
nanos = int(nanoseconds % 1_000_000_000)
|
|
170
150
|
return (seconds, nanos)
|
|
171
|
-
|
|
172
|
-
def _get_instrumentation_name(self) -> str:
|
|
173
|
-
"""Get instrumentation library name - always 'aiqa-tracer'."""
|
|
174
|
-
from .client import AIQA_TRACER_NAME
|
|
175
|
-
return AIQA_TRACER_NAME
|
|
176
|
-
|
|
177
|
-
def _get_instrumentation_version(self) -> Optional[str]:
|
|
178
|
-
"""Get instrumentation library version from __version__."""
|
|
179
|
-
try:
|
|
180
|
-
from . import __version__
|
|
181
|
-
return __version__
|
|
182
|
-
except (ImportError, AttributeError):
|
|
183
|
-
return None
|
|
184
151
|
|
|
185
152
|
def _build_request_headers(self) -> Dict[str, str]:
|
|
186
153
|
"""Build HTTP headers for span requests."""
|
|
@@ -210,38 +177,24 @@ class AIQASpanExporter(SpanExporter):
|
|
|
210
177
|
Atomically extract and remove all spans from buffer (thread-safe).
|
|
211
178
|
Returns the extracted spans. This prevents race conditions where spans
|
|
212
179
|
are added between extraction and clearing.
|
|
213
|
-
Note: Does NOT clear buffer_span_keys - that should be done after successful send
|
|
214
|
-
to avoid unnecessary clearing/rebuilding on failures.
|
|
215
180
|
"""
|
|
216
181
|
with self.buffer_lock:
|
|
217
182
|
spans = self.buffer[:]
|
|
218
183
|
self.buffer.clear()
|
|
219
184
|
return spans
|
|
220
|
-
|
|
221
|
-
def _remove_span_keys_from_tracking(self, spans: List[Dict[str, Any]]) -> None:
|
|
222
|
-
"""
|
|
223
|
-
Remove span keys from tracking set (thread-safe). Called after successful send.
|
|
224
|
-
"""
|
|
225
|
-
with self.buffer_lock:
|
|
226
|
-
for span in spans:
|
|
227
|
-
span_key = (span["traceId"], span["spanId"])
|
|
228
|
-
self.buffer_span_keys.discard(span_key)
|
|
229
185
|
|
|
230
186
|
def _prepend_spans_to_buffer(self, spans: List[Dict[str, Any]]) -> None:
|
|
231
187
|
"""
|
|
232
188
|
Prepend spans back to buffer (thread-safe). Used to restore spans
|
|
233
|
-
if sending fails.
|
|
189
|
+
if sending fails.
|
|
234
190
|
"""
|
|
235
191
|
with self.buffer_lock:
|
|
236
192
|
self.buffer[:0] = spans
|
|
237
|
-
# Rebuild span keys set from current buffer contents
|
|
238
|
-
self.buffer_span_keys = {(span["traceId"], span["spanId"]) for span in self.buffer}
|
|
239
193
|
|
|
240
194
|
def _clear_buffer(self) -> None:
|
|
241
195
|
"""Clear the buffer (thread-safe)."""
|
|
242
196
|
with self.buffer_lock:
|
|
243
197
|
self.buffer.clear()
|
|
244
|
-
self.buffer_span_keys.clear()
|
|
245
198
|
|
|
246
199
|
async def flush(self) -> None:
|
|
247
200
|
"""
|
|
@@ -265,8 +218,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
265
218
|
logger.warning(
|
|
266
219
|
f"Skipping flush: AIQA_SERVER_URL is not set. {len(spans_to_flush)} span(s) will not be sent."
|
|
267
220
|
)
|
|
268
|
-
# Spans already removed from buffer,
|
|
269
|
-
self._remove_span_keys_from_tracking(spans_to_flush)
|
|
221
|
+
# Spans already removed from buffer, nothing to clear
|
|
270
222
|
return
|
|
271
223
|
|
|
272
224
|
logger.info(f"flush() sending {len(spans_to_flush)} span(s) to server")
|
|
@@ -274,8 +226,6 @@ class AIQASpanExporter(SpanExporter):
|
|
|
274
226
|
await self._send_spans(spans_to_flush)
|
|
275
227
|
logger.info(f"flush() successfully sent {len(spans_to_flush)} span(s) to server")
|
|
276
228
|
# Spans already removed from buffer during extraction
|
|
277
|
-
# Now clear their keys from tracking set to free memory
|
|
278
|
-
self._remove_span_keys_from_tracking(spans_to_flush)
|
|
279
229
|
except RuntimeError as error:
|
|
280
230
|
if self._is_interpreter_shutdown_error(error):
|
|
281
231
|
if self.shutdown_requested:
|
|
@@ -287,12 +237,12 @@ class AIQASpanExporter(SpanExporter):
|
|
|
287
237
|
# Put spans back for retry
|
|
288
238
|
self._prepend_spans_to_buffer(spans_to_flush)
|
|
289
239
|
raise
|
|
290
|
-
logger.error(f"Error flushing spans to server: {error}")
|
|
240
|
+
logger.error(f"Error flushing spans to server: {error}", exc_info=True)
|
|
291
241
|
# Put spans back for retry
|
|
292
242
|
self._prepend_spans_to_buffer(spans_to_flush)
|
|
293
243
|
raise
|
|
294
244
|
except Exception as error:
|
|
295
|
-
logger.error(f"Error flushing spans to server: {error}")
|
|
245
|
+
logger.error(f"Error flushing spans to server: {error}", exc_info=True)
|
|
296
246
|
# Put spans back for retry
|
|
297
247
|
self._prepend_spans_to_buffer(spans_to_flush)
|
|
298
248
|
if self.shutdown_requested:
|
|
@@ -321,7 +271,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
321
271
|
logger.debug(f"Auto-flush cycle #{cycle_count} completed, sleeping {self.flush_interval_ms / 1000.0}s")
|
|
322
272
|
time.sleep(self.flush_interval_ms / 1000.0)
|
|
323
273
|
except Exception as e:
|
|
324
|
-
logger.error(f"Error in auto-flush cycle #{cycle_count}: {e}")
|
|
274
|
+
logger.error(f"Error in auto-flush cycle #{cycle_count}: {e}", exc_info=True)
|
|
325
275
|
logger.debug(f"Auto-flush cycle #{cycle_count} error handled, sleeping {self.flush_interval_ms / 1000.0}s")
|
|
326
276
|
time.sleep(self.flush_interval_ms / 1000.0)
|
|
327
277
|
|
|
@@ -357,13 +307,9 @@ class AIQASpanExporter(SpanExporter):
|
|
|
357
307
|
logger.debug("_send_spans() no API key provided")
|
|
358
308
|
|
|
359
309
|
try:
|
|
360
|
-
# Pre-serialize JSON to bytes and wrap in BytesIO to avoid blocking event loop
|
|
361
|
-
json_bytes = json.dumps(spans).encode('utf-8')
|
|
362
|
-
data = io.BytesIO(json_bytes)
|
|
363
|
-
|
|
364
310
|
async with aiohttp.ClientSession() as session:
|
|
365
311
|
logger.debug(f"_send_spans() POST request starting to {url}")
|
|
366
|
-
async with session.post(url,
|
|
312
|
+
async with session.post(url, json=spans, headers=headers) as response:
|
|
367
313
|
logger.debug(f"_send_spans() received response: status={response.status}")
|
|
368
314
|
if not response.ok:
|
|
369
315
|
error_text = await response.text()
|
|
@@ -382,10 +328,10 @@ class AIQASpanExporter(SpanExporter):
|
|
|
382
328
|
else:
|
|
383
329
|
logger.warning(f"_send_spans() interrupted by interpreter shutdown: {e}")
|
|
384
330
|
raise
|
|
385
|
-
logger.error(f"_send_spans() RuntimeError: {type(e).__name__}: {e}")
|
|
331
|
+
logger.error(f"_send_spans() RuntimeError: {type(e).__name__}: {e}", exc_info=True)
|
|
386
332
|
raise
|
|
387
333
|
except Exception as e:
|
|
388
|
-
logger.error(f"_send_spans() exception: {type(e).__name__}: {e}")
|
|
334
|
+
logger.error(f"_send_spans() exception: {type(e).__name__}: {e}", exc_info=True)
|
|
389
335
|
raise
|
|
390
336
|
|
|
391
337
|
def _send_spans_sync(self, spans: List[Dict[str, Any]]) -> None:
|
|
@@ -414,7 +360,7 @@ class AIQASpanExporter(SpanExporter):
|
|
|
414
360
|
)
|
|
415
361
|
logger.debug(f"_send_spans_sync() successfully sent {len(spans)} spans")
|
|
416
362
|
except Exception as e:
|
|
417
|
-
logger.error(f"_send_spans_sync() exception: {type(e).__name__}: {e}")
|
|
363
|
+
logger.error(f"_send_spans_sync() exception: {type(e).__name__}: {e}", exc_info=True)
|
|
418
364
|
raise
|
|
419
365
|
|
|
420
366
|
def shutdown(self) -> None:
|
|
@@ -451,21 +397,17 @@ class AIQASpanExporter(SpanExporter):
|
|
|
451
397
|
f"shutdown() skipping final flush: AIQA_SERVER_URL is not set. "
|
|
452
398
|
f"{len(spans_to_flush)} span(s) will not be sent."
|
|
453
399
|
)
|
|
454
|
-
# Spans already removed from buffer
|
|
455
|
-
self._remove_span_keys_from_tracking(spans_to_flush)
|
|
400
|
+
# Spans already removed from buffer
|
|
456
401
|
else:
|
|
457
402
|
logger.info(f"shutdown() sending {len(spans_to_flush)} span(s) to server (synchronous)")
|
|
458
403
|
try:
|
|
459
404
|
self._send_spans_sync(spans_to_flush)
|
|
460
405
|
logger.info(f"shutdown() successfully sent {len(spans_to_flush)} span(s) to server")
|
|
461
406
|
# Spans already removed from buffer during extraction
|
|
462
|
-
# Clear their keys from tracking set to free memory
|
|
463
|
-
self._remove_span_keys_from_tracking(spans_to_flush)
|
|
464
407
|
except Exception as e:
|
|
465
|
-
logger.error(f"shutdown() failed to send spans: {e}")
|
|
408
|
+
logger.error(f"shutdown() failed to send spans: {e}", exc_info=True)
|
|
466
409
|
# Spans already removed, but process is exiting anyway
|
|
467
410
|
logger.warning(f"shutdown() {len(spans_to_flush)} span(s) were not sent due to error")
|
|
468
|
-
# Keys will remain in tracking set, but process is exiting so memory will be freed
|
|
469
411
|
else:
|
|
470
412
|
logger.debug("shutdown() no spans to flush")
|
|
471
413
|
|