ioa-observe-sdk 1.0.26__py3-none-any.whl → 1.0.27__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.
- ioa_observe/sdk/instrumentations/a2a.py +133 -0
- ioa_observe/sdk/instrumentations/slim.py +9 -1
- ioa_observe/sdk/tracing/tracing.py +71 -4
- {ioa_observe_sdk-1.0.26.dist-info → ioa_observe_sdk-1.0.27.dist-info}/METADATA +1 -1
- {ioa_observe_sdk-1.0.26.dist-info → ioa_observe_sdk-1.0.27.dist-info}/RECORD +8 -8
- {ioa_observe_sdk-1.0.26.dist-info → ioa_observe_sdk-1.0.27.dist-info}/WHEEL +1 -1
- {ioa_observe_sdk-1.0.26.dist-info → ioa_observe_sdk-1.0.27.dist-info}/licenses/LICENSE.md +0 -0
- {ioa_observe_sdk-1.0.26.dist-info → ioa_observe_sdk-1.0.27.dist-info}/top_level.txt +0 -0
|
@@ -204,6 +204,122 @@ class A2AInstrumentor(BaseInstrumentor):
|
|
|
204
204
|
|
|
205
205
|
# original_server_on_message_send = DefaultRequestHandler.on_message_send
|
|
206
206
|
|
|
207
|
+
# Instrumentation for slima2a
|
|
208
|
+
if importlib.util.find_spec("slima2a"):
|
|
209
|
+
from slima2a.client_transport import SRPCTransport
|
|
210
|
+
|
|
211
|
+
# send_message
|
|
212
|
+
original_srpc_transport_send_message = SRPCTransport.send_message
|
|
213
|
+
|
|
214
|
+
@functools.wraps(original_srpc_transport_send_message)
|
|
215
|
+
async def instrumented_srpc_transport_send_message(
|
|
216
|
+
self, request, *args, **kwargs
|
|
217
|
+
):
|
|
218
|
+
# Put context into A2A message metadata instead of HTTP headers
|
|
219
|
+
with _global_tracer.start_as_current_span("slima2a.send_message"):
|
|
220
|
+
traceparent = get_current_traceparent()
|
|
221
|
+
session_id = None
|
|
222
|
+
if traceparent:
|
|
223
|
+
session_id = kv_store.get(f"execution.{traceparent}")
|
|
224
|
+
if not session_id:
|
|
225
|
+
session_id = get_value("session.id")
|
|
226
|
+
if session_id:
|
|
227
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
228
|
+
|
|
229
|
+
# Ensure metadata dict exists
|
|
230
|
+
try:
|
|
231
|
+
md = getattr(request.params, "metadata", None)
|
|
232
|
+
except AttributeError:
|
|
233
|
+
md = None
|
|
234
|
+
metadata = md if isinstance(md, dict) else {}
|
|
235
|
+
|
|
236
|
+
observe_meta = dict(metadata.get("observe", {}))
|
|
237
|
+
|
|
238
|
+
# Inject W3C trace context + baggage into observe_meta
|
|
239
|
+
TraceContextTextMapPropagator().inject(carrier=observe_meta)
|
|
240
|
+
W3CBaggagePropagator().inject(carrier=observe_meta)
|
|
241
|
+
|
|
242
|
+
if traceparent:
|
|
243
|
+
observe_meta["traceparent"] = traceparent
|
|
244
|
+
if session_id:
|
|
245
|
+
observe_meta["session_id"] = session_id
|
|
246
|
+
baggage.set_baggage(f"execution.{traceparent}", session_id)
|
|
247
|
+
|
|
248
|
+
metadata["observe"] = observe_meta
|
|
249
|
+
|
|
250
|
+
# Write back metadata (pydantic models are mutable by default in v2)
|
|
251
|
+
try:
|
|
252
|
+
request.metadata = metadata
|
|
253
|
+
except Exception:
|
|
254
|
+
# Fallback
|
|
255
|
+
request = request.model_copy(update={"metadata": metadata})
|
|
256
|
+
|
|
257
|
+
# Call through without transport-specific kwargs
|
|
258
|
+
return await original_srpc_transport_send_message(
|
|
259
|
+
self, request, *args, **kwargs
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
SRPCTransport.send_message = instrumented_srpc_transport_send_message
|
|
263
|
+
|
|
264
|
+
# send_message_streaming
|
|
265
|
+
original_srpc_transport_send_message_streaming = (
|
|
266
|
+
SRPCTransport.send_message_streaming
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
@functools.wraps(original_srpc_transport_send_message_streaming)
|
|
270
|
+
async def instrumented_srpc_transport_send_message_streaming(
|
|
271
|
+
self, request, *args, **kwargs
|
|
272
|
+
):
|
|
273
|
+
# Put context into A2A message metadata instead of HTTP headers
|
|
274
|
+
with _global_tracer.start_as_current_span(
|
|
275
|
+
"slima2a.send_message_streaming"
|
|
276
|
+
):
|
|
277
|
+
traceparent = get_current_traceparent()
|
|
278
|
+
session_id = None
|
|
279
|
+
if traceparent:
|
|
280
|
+
session_id = kv_store.get(f"execution.{traceparent}")
|
|
281
|
+
if not session_id:
|
|
282
|
+
session_id = get_value("session.id")
|
|
283
|
+
if session_id:
|
|
284
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
285
|
+
|
|
286
|
+
# Ensure metadata dict exists
|
|
287
|
+
try:
|
|
288
|
+
md = getattr(request.params, "metadata", None)
|
|
289
|
+
except AttributeError:
|
|
290
|
+
md = None
|
|
291
|
+
metadata = md if isinstance(md, dict) else {}
|
|
292
|
+
|
|
293
|
+
observe_meta = dict(metadata.get("observe", {}))
|
|
294
|
+
|
|
295
|
+
# Inject W3C trace context + baggage into observe_meta
|
|
296
|
+
TraceContextTextMapPropagator().inject(carrier=observe_meta)
|
|
297
|
+
W3CBaggagePropagator().inject(carrier=observe_meta)
|
|
298
|
+
|
|
299
|
+
if traceparent:
|
|
300
|
+
observe_meta["traceparent"] = traceparent
|
|
301
|
+
if session_id:
|
|
302
|
+
observe_meta["session_id"] = session_id
|
|
303
|
+
baggage.set_baggage(f"execution.{traceparent}", session_id)
|
|
304
|
+
|
|
305
|
+
metadata["observe"] = observe_meta
|
|
306
|
+
|
|
307
|
+
# Write back metadata (pydantic models are mutable by default in v2)
|
|
308
|
+
try:
|
|
309
|
+
request.metadata = metadata
|
|
310
|
+
except Exception:
|
|
311
|
+
# Fallback
|
|
312
|
+
request = request.model_copy(update={"metadata": metadata})
|
|
313
|
+
|
|
314
|
+
# Call through without transport-specific kwargs
|
|
315
|
+
return await original_srpc_transport_send_message_streaming(
|
|
316
|
+
self, request, *args, **kwargs
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
SRPCTransport.send_message_streaming = (
|
|
320
|
+
instrumented_srpc_transport_send_message_streaming
|
|
321
|
+
)
|
|
322
|
+
|
|
207
323
|
def _uninstrument(self, **kwargs):
|
|
208
324
|
import importlib
|
|
209
325
|
|
|
@@ -227,3 +343,20 @@ class A2AInstrumentor(BaseInstrumentor):
|
|
|
227
343
|
DefaultRequestHandler.on_message_send = (
|
|
228
344
|
DefaultRequestHandler.on_message_send.__wrapped__
|
|
229
345
|
)
|
|
346
|
+
|
|
347
|
+
# handle slima2a
|
|
348
|
+
if importlib.util.find_spec("slima2a"):
|
|
349
|
+
from slima2a.client_transport import SRPCTransport
|
|
350
|
+
|
|
351
|
+
# Uninstrument `send_message`
|
|
352
|
+
if hasattr(SRPCTransport, "send_message") and hasattr(
|
|
353
|
+
SRPCTransport.send_message, "__wrapped__"
|
|
354
|
+
):
|
|
355
|
+
SRPCTransport.send_message = SRPCTransport.send_message.__wrapped__
|
|
356
|
+
|
|
357
|
+
if hasattr(SRPCTransport, "send_message_streaming") and hasattr(
|
|
358
|
+
SRPCTransport.send_message_streaming, "__wrapped__"
|
|
359
|
+
):
|
|
360
|
+
SRPCTransport.send_message_streaming = (
|
|
361
|
+
SRPCTransport.send_message.__wrapped__
|
|
362
|
+
)
|
|
@@ -531,7 +531,15 @@ class SLIMInstrumentor(BaseInstrumentor):
|
|
|
531
531
|
if timeout is not None:
|
|
532
532
|
kwargs["timeout"] = timeout
|
|
533
533
|
|
|
534
|
-
|
|
534
|
+
if _global_tracer:
|
|
535
|
+
with _global_tracer.start_as_current_span(
|
|
536
|
+
"session.get_message"
|
|
537
|
+
) as span:
|
|
538
|
+
if hasattr(self, "id"):
|
|
539
|
+
span.set_attribute("slim.session.id", str(self.id))
|
|
540
|
+
result = await original_get_message(self, **kwargs)
|
|
541
|
+
else:
|
|
542
|
+
result = await original_get_message(self, **kwargs)
|
|
535
543
|
|
|
536
544
|
# Handle different return types from get_message
|
|
537
545
|
if result is None:
|
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
+
import threading
|
|
10
11
|
import time
|
|
11
12
|
import uuid
|
|
12
13
|
|
|
@@ -64,6 +65,9 @@ from typing import Callable, Dict, Optional, Set
|
|
|
64
65
|
TRACER_NAME = "ioa.observe.tracer"
|
|
65
66
|
APP_NAME = ""
|
|
66
67
|
|
|
68
|
+
SESSION_IDLE_TIMEOUT_SECONDS = 300 # e.g. 5 minutes
|
|
69
|
+
SESSION_WATCHER_INTERVAL_SECONDS = 30
|
|
70
|
+
|
|
67
71
|
|
|
68
72
|
def determine_reliability_score(span):
|
|
69
73
|
if "observe.entity.output" in span.attributes:
|
|
@@ -132,6 +136,9 @@ class TracerWrapper(object):
|
|
|
132
136
|
if not TracerWrapper.endpoint:
|
|
133
137
|
return obj
|
|
134
138
|
|
|
139
|
+
# session activity tracking
|
|
140
|
+
obj._session_last_activity: dict[str, float] = {}
|
|
141
|
+
obj._session_lock = threading.Lock()
|
|
135
142
|
obj.__image_uploader = image_uploader
|
|
136
143
|
# {(agent_name): [success_count, total_count]}
|
|
137
144
|
obj._agent_execution_counts = {}
|
|
@@ -270,12 +277,70 @@ class TracerWrapper(object):
|
|
|
270
277
|
|
|
271
278
|
obj.__content_allow_list = ContentAllowList()
|
|
272
279
|
|
|
280
|
+
# start background watcher for session end
|
|
281
|
+
obj._start_session_watcher()
|
|
282
|
+
|
|
273
283
|
# Force flushes for debug environments (e.g. local development)
|
|
274
284
|
atexit.register(obj.exit_handler)
|
|
275
285
|
|
|
276
286
|
return cls.instance
|
|
277
287
|
|
|
288
|
+
def _start_session_watcher(self) -> None:
|
|
289
|
+
t = threading.Thread(
|
|
290
|
+
target=self._session_watcher_loop,
|
|
291
|
+
name="ioa-session-watcher",
|
|
292
|
+
daemon=True,
|
|
293
|
+
)
|
|
294
|
+
t.start()
|
|
295
|
+
|
|
296
|
+
def _session_watcher_loop(self) -> None:
|
|
297
|
+
while True:
|
|
298
|
+
time.sleep(SESSION_WATCHER_INTERVAL_SECONDS)
|
|
299
|
+
now = time.time()
|
|
300
|
+
expired: dict[str, float] = {}
|
|
301
|
+
|
|
302
|
+
# Find idle sessions and remove them from _session_last_activity
|
|
303
|
+
with self._session_lock:
|
|
304
|
+
for session_id, last_ts in list(self._session_last_activity.items()):
|
|
305
|
+
if now - last_ts > SESSION_IDLE_TIMEOUT_SECONDS:
|
|
306
|
+
expired[session_id] = last_ts
|
|
307
|
+
del self._session_last_activity[session_id]
|
|
308
|
+
|
|
309
|
+
if not expired:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
tracer = self.get_tracer()
|
|
313
|
+
|
|
314
|
+
# Iterate over a snapshot and do *not* modify `expired` in the loop
|
|
315
|
+
for session_id, _last_ts in list(expired.items()):
|
|
316
|
+
print("ending session", session_id)
|
|
317
|
+
with tracer.start_as_current_span("session.end") as span:
|
|
318
|
+
span.set_attribute("session.id", session_id)
|
|
319
|
+
workflow_name = get_value("workflow_name")
|
|
320
|
+
if workflow_name:
|
|
321
|
+
span.set_attribute(OBSERVE_WORKFLOW_NAME, workflow_name)
|
|
322
|
+
span.set_attribute("session.ended_at", _last_ts)
|
|
323
|
+
|
|
324
|
+
# ensure end spans are exported reasonably fast
|
|
325
|
+
self.flush()
|
|
326
|
+
|
|
278
327
|
def exit_handler(self):
|
|
328
|
+
# emit end spans for any sessions that never went idle
|
|
329
|
+
now = time.time()
|
|
330
|
+
tracer = self.get_tracer()
|
|
331
|
+
|
|
332
|
+
with self._session_lock:
|
|
333
|
+
remaining_ids = list(self._session_last_activity.keys())
|
|
334
|
+
self._session_last_activity.clear()
|
|
335
|
+
|
|
336
|
+
for session_id in remaining_ids:
|
|
337
|
+
with tracer.start_as_current_span("session.end") as span:
|
|
338
|
+
span.set_attribute("session.id", session_id)
|
|
339
|
+
workflow_name = get_value("workflow_name")
|
|
340
|
+
if workflow_name:
|
|
341
|
+
span.set_attribute(OBSERVE_WORKFLOW_NAME, workflow_name)
|
|
342
|
+
span.set_attribute("session.ended_at", now)
|
|
343
|
+
|
|
279
344
|
self.flush()
|
|
280
345
|
|
|
281
346
|
def _span_processor_on_start(self, span, parent_context):
|
|
@@ -362,6 +427,12 @@ class TracerWrapper(object):
|
|
|
362
427
|
# Mark this span as processed
|
|
363
428
|
self._processed_spans.add(span_id)
|
|
364
429
|
|
|
430
|
+
# update last activity per session
|
|
431
|
+
session_id = span.attributes.get("session.id")
|
|
432
|
+
if session_id:
|
|
433
|
+
with self._session_lock:
|
|
434
|
+
self._session_last_activity[session_id] = time.time()
|
|
435
|
+
|
|
365
436
|
determine_reliability_score(span)
|
|
366
437
|
# start_time = span.attributes.get("ioa_start_time")
|
|
367
438
|
|
|
@@ -382,10 +453,6 @@ class TracerWrapper(object):
|
|
|
382
453
|
except Exception as e:
|
|
383
454
|
logging.error(f"Error applying span transformation: {e}")
|
|
384
455
|
|
|
385
|
-
# if start_time is not None:
|
|
386
|
-
# latency = (time.time() - start_time) * 1000
|
|
387
|
-
# self.response_latency_histogram.record(latency, attributes=span.attributes)
|
|
388
|
-
|
|
389
456
|
# Call original on_end method if it exists
|
|
390
457
|
if (
|
|
391
458
|
hasattr(self, "_TracerWrapper__spans_processor_original_on_end")
|
|
@@ -14,10 +14,10 @@ ioa_observe/sdk/decorators/base.py,sha256=gezoLMLOo-CWsxzsXbQMsj5HxmchUEmlhTspbq
|
|
|
14
14
|
ioa_observe/sdk/decorators/helpers.py,sha256=I9HXMBivkZpGDtPe9Ad_UU35p_m_wEPate4r_fU0oOA,2705
|
|
15
15
|
ioa_observe/sdk/decorators/util.py,sha256=IebvH9gwZN1en3LblYJUh4bAV2STl6xmp8WpZzBDH2g,30068
|
|
16
16
|
ioa_observe/sdk/instrumentations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
ioa_observe/sdk/instrumentations/a2a.py,sha256=
|
|
17
|
+
ioa_observe/sdk/instrumentations/a2a.py,sha256=LHvvQUluPtmvbRNuoqf-EkWlZJa_NicerTpM3t1UZws,15299
|
|
18
18
|
ioa_observe/sdk/instrumentations/mcp.py,sha256=UG82fWitFT-K-ecnkTKZa-uFTDAs-j2HymvhUwCBRLY,18721
|
|
19
19
|
ioa_observe/sdk/instrumentations/nats.py,sha256=MEYCFqHyRK5HtkjrbGVV5qIWplZ1ZjJjFCB8vtQl6qE,14736
|
|
20
|
-
ioa_observe/sdk/instrumentations/slim.py,sha256=
|
|
20
|
+
ioa_observe/sdk/instrumentations/slim.py,sha256=c_Q1cb6pdjZLOw1MbclUf1eRFrc_sUEe9pQJvAyoD1Y,45712
|
|
21
21
|
ioa_observe/sdk/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
ioa_observe/sdk/logging/logging.py,sha256=HZxW9s8Due7jgiNkdI38cIjv5rC9D-Flta3RQMOnpow,2891
|
|
23
23
|
ioa_observe/sdk/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -35,15 +35,15 @@ ioa_observe/sdk/tracing/content_allow_list.py,sha256=1fAkpIwUQ7vDwCTkIVrqeltWQtr
|
|
|
35
35
|
ioa_observe/sdk/tracing/context_manager.py,sha256=O0JEXYa9h8anhW78R8KKBuqS0j4by1E1KXxNIMPnLr8,400
|
|
36
36
|
ioa_observe/sdk/tracing/context_utils.py,sha256=-sYS9vPLI87davV9ubneq5xqbV583CC_c0SmOQS1TAs,2933
|
|
37
37
|
ioa_observe/sdk/tracing/manual.py,sha256=KS6WN-zw9vAACzXYmnMoJm9d1fenYMfvzeK1GrGDPDE,1937
|
|
38
|
-
ioa_observe/sdk/tracing/tracing.py,sha256=
|
|
38
|
+
ioa_observe/sdk/tracing/tracing.py,sha256=7eJAEqaAEBZ2gjIXrdb7SyUidA8P10GnpBLzr8InLKI,50007
|
|
39
39
|
ioa_observe/sdk/tracing/transform_span.py,sha256=XTApi_gJxum7ynvhtcoCfDyK8VVOj91Q1DT6hAeLHA8,8419
|
|
40
40
|
ioa_observe/sdk/utils/__init__.py,sha256=UPn182U-UblF_XwXaFpx8F-TmQTbm1LYf9y89uSp5Hw,704
|
|
41
41
|
ioa_observe/sdk/utils/const.py,sha256=d67dUTAH9UpWvUV9GLBUqn1Sc2knJ55dy-e6YoLrvSo,1318
|
|
42
42
|
ioa_observe/sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
|
|
43
43
|
ioa_observe/sdk/utils/json_encoder.py,sha256=g4NQ0tTqgWssY6I1D7r4zo0G6PiUo61jhofTAw5-jno,639
|
|
44
44
|
ioa_observe/sdk/utils/package_check.py,sha256=1d1MjxhwoEZIx9dumirT2pRsEWgn-m-SI4npDeEalew,576
|
|
45
|
-
ioa_observe_sdk-1.0.
|
|
46
|
-
ioa_observe_sdk-1.0.
|
|
47
|
-
ioa_observe_sdk-1.0.
|
|
48
|
-
ioa_observe_sdk-1.0.
|
|
49
|
-
ioa_observe_sdk-1.0.
|
|
45
|
+
ioa_observe_sdk-1.0.27.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
|
|
46
|
+
ioa_observe_sdk-1.0.27.dist-info/METADATA,sha256=OrhfkqzR0Qj2qwSGXtSmL9rYjS1OMk-IYmTU5wHMHdE,7997
|
|
47
|
+
ioa_observe_sdk-1.0.27.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
48
|
+
ioa_observe_sdk-1.0.27.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
|
|
49
|
+
ioa_observe_sdk-1.0.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|