ioa-observe-sdk 1.0.27__py3-none-any.whl → 1.0.29__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.
@@ -2,6 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import sys
5
+ import threading
5
6
 
6
7
  from .http import HTTPClient
7
8
  from ioa_observe.sdk.version import __version__
@@ -51,21 +52,29 @@ class Client:
51
52
  class KVStore(object):
52
53
  """
53
54
  Key-Value Store for storing key-value pairs (Singleton).
55
+ Thread-safe implementation for concurrent access.
54
56
  """
55
57
 
56
58
  _instance = None
59
+ _instance_lock = threading.Lock()
57
60
 
58
61
  def __new__(cls):
59
62
  if cls._instance is None:
60
- cls._instance = super(KVStore, cls).__new__(cls)
61
- cls._instance.store = {}
63
+ with cls._instance_lock:
64
+ # Double-check locking pattern
65
+ if cls._instance is None:
66
+ cls._instance = super(KVStore, cls).__new__(cls)
67
+ cls._instance.store = {}
68
+ cls._instance._lock = threading.Lock()
62
69
  return cls._instance
63
70
 
64
71
  def set(self, key: str, value: str):
65
- self.store[key] = value
72
+ with self._lock:
73
+ self.store[key] = value
66
74
 
67
75
  def get(self, key: str):
68
- return self.store.get(key)
76
+ with self._lock:
77
+ return self.store.get(key)
69
78
 
70
79
 
71
80
  kv_store = KVStore()
@@ -19,6 +19,72 @@ _instruments = ("a2a-sdk >= 0.3.0",)
19
19
  _global_tracer = None
20
20
  _kv_lock = threading.RLock() # Add thread-safety for kv_store operations
21
21
 
22
+ # Track original methods for uninstrumentation
23
+ _original_methods = {}
24
+
25
+
26
+ def _inject_observe_metadata(request, span_name: str):
27
+ """
28
+ Helper function to inject observability metadata into request.
29
+ This is shared between legacy and new client instrumentation.
30
+ """
31
+ traceparent = get_current_traceparent()
32
+ session_id = None
33
+ if traceparent:
34
+ session_id = kv_store.get(f"execution.{traceparent}")
35
+ if not session_id:
36
+ session_id = get_value("session.id")
37
+ if session_id:
38
+ kv_store.set(f"execution.{traceparent}", session_id)
39
+
40
+ # Ensure metadata dict exists - handle both request.params.metadata and request.metadata
41
+ try:
42
+ if hasattr(request, "params") and hasattr(request.params, "metadata"):
43
+ md = getattr(request.params, "metadata", None)
44
+ else:
45
+ md = getattr(request, "metadata", None)
46
+ except AttributeError:
47
+ md = None
48
+ metadata = md if isinstance(md, dict) else {}
49
+
50
+ observe_meta = dict(metadata.get("observe", {}))
51
+
52
+ # Inject W3C trace context + baggage into observe_meta
53
+ TraceContextTextMapPropagator().inject(carrier=observe_meta)
54
+ W3CBaggagePropagator().inject(carrier=observe_meta)
55
+
56
+ if traceparent:
57
+ observe_meta["traceparent"] = traceparent
58
+ if session_id:
59
+ observe_meta["session_id"] = session_id
60
+ baggage.set_baggage(f"execution.{traceparent}", session_id)
61
+
62
+ metadata["observe"] = observe_meta
63
+
64
+ # Write back metadata (pydantic models are mutable by default in v2)
65
+ try:
66
+ if hasattr(request, "params") and hasattr(request.params, "metadata"):
67
+ request.params.metadata = metadata
68
+ else:
69
+ request.metadata = metadata
70
+ except Exception:
71
+ # Fallback - create a new request with updated metadata
72
+ try:
73
+ if hasattr(request, "params"):
74
+ request = request.model_copy(
75
+ update={
76
+ "params": request.params.model_copy(
77
+ update={"metadata": metadata}
78
+ )
79
+ }
80
+ )
81
+ else:
82
+ request = request.model_copy(update={"metadata": metadata})
83
+ except Exception:
84
+ pass # If all else fails, continue without metadata injection
85
+
86
+ return request
87
+
22
88
 
23
89
  class A2AInstrumentor(BaseInstrumentor):
24
90
  def __init__(self):
@@ -35,10 +101,105 @@ class A2AInstrumentor(BaseInstrumentor):
35
101
  if importlib.util.find_spec("a2a") is None:
36
102
  raise ImportError("No module named 'a2a-sdk'. Please install it first.")
37
103
 
38
- # Instrument client send_message
39
- from a2a.client import A2AClient
104
+ # Instrument new A2AClient (BaseClient) from a2a.client.base_client (v0.3.0+)
105
+ self._instrument_base_client()
106
+
107
+ # Instrument legacy A2AClient for backward compatibility
108
+ self._instrument_legacy_client()
109
+
110
+ # Instrument server handler
111
+ self._instrument_server_handler()
112
+
113
+ # Instrument slima2a if available
114
+ self._instrument_slima2a()
115
+
116
+ def _instrument_base_client(self):
117
+ """Instrument the new A2AClient (BaseClient) pattern introduced in v0.3.0."""
118
+ import importlib
119
+
120
+ # Check if the new base_client module exists
121
+ if importlib.util.find_spec("a2a.client.base_client") is None:
122
+ return
123
+
124
+ try:
125
+ from a2a.client.base_client import A2AClient as BaseA2AClient
126
+ except ImportError:
127
+ return
128
+
129
+ # Instrument send_message
130
+ if hasattr(BaseA2AClient, "send_message"):
131
+ original_send_message = BaseA2AClient.send_message
132
+ _original_methods["BaseA2AClient.send_message"] = original_send_message
133
+
134
+ @functools.wraps(original_send_message)
135
+ async def instrumented_send_message(self, request, *args, **kwargs):
136
+ nonlocal original_send_message
137
+ with _global_tracer.start_as_current_span("a2a.client.send_message"):
138
+ request = _inject_observe_metadata(
139
+ request, "a2a.client.send_message"
140
+ )
141
+ return await original_send_message(self, request, *args, **kwargs)
142
+
143
+ BaseA2AClient.send_message = instrumented_send_message
144
+
145
+ # Instrument send_message_streaming
146
+ if hasattr(BaseA2AClient, "send_message_streaming"):
147
+ original_send_message_streaming = BaseA2AClient.send_message_streaming
148
+ _original_methods["BaseA2AClient.send_message_streaming"] = (
149
+ original_send_message_streaming
150
+ )
151
+
152
+ @functools.wraps(original_send_message_streaming)
153
+ async def instrumented_send_message_streaming(
154
+ self, request, *args, **kwargs
155
+ ):
156
+ nonlocal original_send_message_streaming
157
+ with _global_tracer.start_as_current_span(
158
+ "a2a.client.send_message_streaming"
159
+ ):
160
+ request = _inject_observe_metadata(
161
+ request, "a2a.client.send_message_streaming"
162
+ )
163
+ # This is an async generator, so we need to yield from it
164
+ async for response in original_send_message_streaming(
165
+ self, request, *args, **kwargs
166
+ ):
167
+ yield response
168
+
169
+ BaseA2AClient.send_message_streaming = instrumented_send_message_streaming
170
+
171
+ def _instrument_legacy_client(self):
172
+ """Instrument the legacy A2AClient for backward compatibility."""
173
+
174
+ # Try to import from legacy location first (v0.3.0+)
175
+ legacy_client = None
176
+ legacy_module_path = None
40
177
 
41
- original_send_message = A2AClient.send_message
178
+ try:
179
+ from a2a.client.legacy import A2AClient as LegacyA2AClient
180
+
181
+ legacy_client = LegacyA2AClient
182
+ legacy_module_path = "a2a.client.legacy"
183
+ except ImportError:
184
+ # Fall back to old import path (pre-v0.3.0)
185
+ try:
186
+ from a2a.client import A2AClient as LegacyA2AClient
187
+
188
+ # Check if this is the legacy client (has send_message method directly)
189
+ if hasattr(LegacyA2AClient, "send_message"):
190
+ legacy_client = LegacyA2AClient
191
+ legacy_module_path = "a2a.client"
192
+ except ImportError:
193
+ pass
194
+
195
+ if legacy_client is None:
196
+ return
197
+
198
+ # Store original methods for uninstrumentation
199
+ original_send_message = legacy_client.send_message
200
+ _original_methods[f"{legacy_module_path}.A2AClient.send_message"] = (
201
+ original_send_message
202
+ )
42
203
 
43
204
  @functools.wraps(original_send_message)
44
205
  async def instrumented_send_message(self, request, *args, **kwargs):
@@ -90,11 +251,14 @@ class A2AInstrumentor(BaseInstrumentor):
90
251
  # Call through without transport-specific kwargs
91
252
  return await original_send_message(self, request, *args, **kwargs)
92
253
 
93
- A2AClient.send_message = instrumented_send_message
254
+ legacy_client.send_message = instrumented_send_message
94
255
 
95
- # Instrument broadcast_message
96
- if hasattr(A2AClient, "broadcast_message"):
97
- original_broadcast_message = A2AClient.broadcast_message
256
+ # Instrument broadcast_message if it exists
257
+ if hasattr(legacy_client, "broadcast_message"):
258
+ original_broadcast_message = legacy_client.broadcast_message
259
+ _original_methods[f"{legacy_module_path}.A2AClient.broadcast_message"] = (
260
+ original_broadcast_message
261
+ )
98
262
 
99
263
  @functools.wraps(original_broadcast_message)
100
264
  async def instrumented_broadcast_message(self, request, *args, **kwargs):
@@ -146,12 +310,16 @@ class A2AInstrumentor(BaseInstrumentor):
146
310
  # Call through without transport-specific kwargs
147
311
  return await original_broadcast_message(self, request, *args, **kwargs)
148
312
 
149
- A2AClient.broadcast_message = instrumented_broadcast_message
313
+ legacy_client.broadcast_message = instrumented_broadcast_message
150
314
 
151
- # Instrument server handler
315
+ def _instrument_server_handler(self):
316
+ """Instrument the server-side request handlers."""
152
317
  from a2a.server.request_handlers import DefaultRequestHandler
153
318
 
154
319
  original_server_on_message_send = DefaultRequestHandler.on_message_send
320
+ _original_methods["DefaultRequestHandler.on_message_send"] = (
321
+ original_server_on_message_send
322
+ )
155
323
 
156
324
  @functools.wraps(original_server_on_message_send)
157
325
  async def instrumented_on_message_send(self, params, context):
@@ -190,173 +358,314 @@ class A2AInstrumentor(BaseInstrumentor):
190
358
  finally:
191
359
  if token is not None:
192
360
  try:
361
+ from opentelemetry import context as otel_ctx
362
+
193
363
  otel_ctx.detach(token)
194
364
  except Exception:
195
365
  pass
196
366
 
197
367
  DefaultRequestHandler.on_message_send = instrumented_on_message_send
198
368
 
199
- # from a2a.client import A2AClient
369
+ # Instrument on_message_send_stream for streaming message reception
370
+ if hasattr(DefaultRequestHandler, "on_message_send_stream"):
371
+ original_on_message_send_stream = (
372
+ DefaultRequestHandler.on_message_send_stream
373
+ )
374
+ _original_methods["DefaultRequestHandler.on_message_send_stream"] = (
375
+ original_on_message_send_stream
376
+ )
200
377
 
201
- # A2AClient.send_message = instrumented_send_message
378
+ @functools.wraps(original_on_message_send_stream)
379
+ async def instrumented_on_message_send_stream(self, params, context):
380
+ # Read context from A2A message metadata (transport-agnostic)
381
+ try:
382
+ metadata = getattr(params, "metadata", {}) or {}
383
+ except Exception:
384
+ metadata = {}
385
+
386
+ carrier = {}
387
+ observe_meta = metadata.get("observe", {}) or {}
388
+ for k in ("traceparent", "baggage", "session_id"):
389
+ if k in observe_meta:
390
+ carrier[k] = observe_meta[k]
391
+
392
+ token = None
393
+ if carrier.get("traceparent"):
394
+ ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
395
+ ctx = W3CBaggagePropagator().extract(carrier=carrier, context=ctx)
396
+ try:
397
+ from opentelemetry import context as otel_ctx
202
398
 
203
- # from a2a.server.request_handlers import DefaultRequestHandler
399
+ token = otel_ctx.attach(ctx)
400
+ except Exception:
401
+ token = None
204
402
 
205
- # original_server_on_message_send = DefaultRequestHandler.on_message_send
403
+ session_id = observe_meta.get("session_id")
404
+ if session_id and session_id != "None":
405
+ set_session_id(
406
+ session_id, traceparent=carrier.get("traceparent")
407
+ )
408
+ kv_store.set(
409
+ f"execution.{carrier.get('traceparent')}", session_id
410
+ )
206
411
 
207
- # Instrumentation for slima2a
208
- if importlib.util.find_spec("slima2a"):
209
- from slima2a.client_transport import SRPCTransport
412
+ try:
413
+ # This is an async generator
414
+ async for event in original_on_message_send_stream(
415
+ self, params, context
416
+ ):
417
+ yield event
418
+ finally:
419
+ if token is not None:
420
+ try:
421
+ from opentelemetry import context as otel_ctx
422
+
423
+ otel_ctx.detach(token)
424
+ except Exception:
425
+ pass
426
+
427
+ DefaultRequestHandler.on_message_send_stream = (
428
+ instrumented_on_message_send_stream
429
+ )
210
430
 
211
- # send_message
212
- original_srpc_transport_send_message = SRPCTransport.send_message
431
+ def _instrument_slima2a(self):
432
+ """Instrument slima2a transport if available."""
433
+ import importlib
213
434
 
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)
435
+ if importlib.util.find_spec("slima2a") is None:
436
+ return
228
437
 
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 {}
438
+ from slima2a.client_transport import SRPCTransport
235
439
 
236
- observe_meta = dict(metadata.get("observe", {}))
440
+ # send_message
441
+ original_srpc_transport_send_message = SRPCTransport.send_message
442
+ _original_methods["SRPCTransport.send_message"] = (
443
+ original_srpc_transport_send_message
444
+ )
237
445
 
238
- # Inject W3C trace context + baggage into observe_meta
239
- TraceContextTextMapPropagator().inject(carrier=observe_meta)
240
- W3CBaggagePropagator().inject(carrier=observe_meta)
446
+ @functools.wraps(original_srpc_transport_send_message)
447
+ async def instrumented_srpc_transport_send_message(
448
+ self, request, *args, **kwargs
449
+ ):
450
+ # Put context into A2A message metadata instead of HTTP headers
451
+ with _global_tracer.start_as_current_span("slima2a.send_message"):
452
+ traceparent = get_current_traceparent()
453
+ session_id = None
454
+ if traceparent:
455
+ session_id = kv_store.get(f"execution.{traceparent}")
456
+ if not session_id:
457
+ session_id = get_value("session.id")
458
+ if session_id:
459
+ kv_store.set(f"execution.{traceparent}", session_id)
241
460
 
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)
461
+ # Ensure metadata dict exists
462
+ try:
463
+ md = getattr(request.params, "metadata", None)
464
+ except AttributeError:
465
+ md = None
466
+ metadata = md if isinstance(md, dict) else {}
247
467
 
248
- metadata["observe"] = observe_meta
468
+ observe_meta = dict(metadata.get("observe", {}))
249
469
 
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})
470
+ # Inject W3C trace context + baggage into observe_meta
471
+ TraceContextTextMapPropagator().inject(carrier=observe_meta)
472
+ W3CBaggagePropagator().inject(carrier=observe_meta)
256
473
 
257
- # Call through without transport-specific kwargs
258
- return await original_srpc_transport_send_message(
259
- self, request, *args, **kwargs
260
- )
474
+ if traceparent:
475
+ observe_meta["traceparent"] = traceparent
476
+ if session_id:
477
+ observe_meta["session_id"] = session_id
478
+ baggage.set_baggage(f"execution.{traceparent}", session_id)
261
479
 
262
- SRPCTransport.send_message = instrumented_srpc_transport_send_message
480
+ metadata["observe"] = observe_meta
263
481
 
264
- # send_message_streaming
265
- original_srpc_transport_send_message_streaming = (
266
- SRPCTransport.send_message_streaming
267
- )
482
+ # Write back metadata (pydantic models are mutable by default in v2)
483
+ try:
484
+ request.metadata = metadata
485
+ except Exception:
486
+ # Fallback
487
+ request = request.model_copy(update={"metadata": metadata})
268
488
 
269
- @functools.wraps(original_srpc_transport_send_message_streaming)
270
- async def instrumented_srpc_transport_send_message_streaming(
489
+ # Call through without transport-specific kwargs
490
+ return await original_srpc_transport_send_message(
271
491
  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)
492
+ )
285
493
 
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 {}
494
+ SRPCTransport.send_message = instrumented_srpc_transport_send_message
292
495
 
293
- observe_meta = dict(metadata.get("observe", {}))
496
+ # send_message_streaming
497
+ original_srpc_transport_send_message_streaming = (
498
+ SRPCTransport.send_message_streaming
499
+ )
500
+ _original_methods["SRPCTransport.send_message_streaming"] = (
501
+ original_srpc_transport_send_message_streaming
502
+ )
294
503
 
295
- # Inject W3C trace context + baggage into observe_meta
296
- TraceContextTextMapPropagator().inject(carrier=observe_meta)
297
- W3CBaggagePropagator().inject(carrier=observe_meta)
504
+ @functools.wraps(original_srpc_transport_send_message_streaming)
505
+ async def instrumented_srpc_transport_send_message_streaming(
506
+ self, request, *args, **kwargs
507
+ ):
508
+ # Put context into A2A message metadata instead of HTTP headers
509
+ with _global_tracer.start_as_current_span("slima2a.send_message_streaming"):
510
+ traceparent = get_current_traceparent()
511
+ session_id = None
512
+ if traceparent:
513
+ session_id = kv_store.get(f"execution.{traceparent}")
514
+ if not session_id:
515
+ session_id = get_value("session.id")
516
+ if session_id:
517
+ kv_store.set(f"execution.{traceparent}", session_id)
298
518
 
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)
519
+ # Ensure metadata dict exists
520
+ try:
521
+ md = getattr(request.params, "metadata", None)
522
+ except AttributeError:
523
+ md = None
524
+ metadata = md if isinstance(md, dict) else {}
304
525
 
305
- metadata["observe"] = observe_meta
526
+ observe_meta = dict(metadata.get("observe", {}))
306
527
 
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})
528
+ # Inject W3C trace context + baggage into observe_meta
529
+ TraceContextTextMapPropagator().inject(carrier=observe_meta)
530
+ W3CBaggagePropagator().inject(carrier=observe_meta)
313
531
 
314
- # Call through without transport-specific kwargs
315
- return await original_srpc_transport_send_message_streaming(
316
- self, request, *args, **kwargs
317
- )
532
+ if traceparent:
533
+ observe_meta["traceparent"] = traceparent
534
+ if session_id:
535
+ observe_meta["session_id"] = session_id
536
+ baggage.set_baggage(f"execution.{traceparent}", session_id)
318
537
 
319
- SRPCTransport.send_message_streaming = (
320
- instrumented_srpc_transport_send_message_streaming
538
+ metadata["observe"] = observe_meta
539
+
540
+ # Write back metadata (pydantic models are mutable by default in v2)
541
+ try:
542
+ request.metadata = metadata
543
+ except Exception:
544
+ # Fallback
545
+ request = request.model_copy(update={"metadata": metadata})
546
+
547
+ # Call through without transport-specific kwargs
548
+ return await original_srpc_transport_send_message_streaming(
549
+ self, request, *args, **kwargs
321
550
  )
322
551
 
552
+ SRPCTransport.send_message_streaming = (
553
+ instrumented_srpc_transport_send_message_streaming
554
+ )
555
+
323
556
  def _uninstrument(self, **kwargs):
324
557
  import importlib
325
558
 
326
559
  if importlib.util.find_spec("a2a") is None:
327
560
  raise ImportError("No module named 'a2a-sdk'. Please install it first.")
328
561
 
329
- # Uninstrument `send_message`
330
- from a2a.client import A2AClient
562
+ # Uninstrument BaseA2AClient (new pattern v0.3.0+)
563
+ if importlib.util.find_spec("a2a.client.base_client") is not None:
564
+ try:
565
+ from a2a.client.base_client import A2AClient as BaseA2AClient
566
+
567
+ if "BaseA2AClient.send_message" in _original_methods:
568
+ BaseA2AClient.send_message = _original_methods[
569
+ "BaseA2AClient.send_message"
570
+ ]
571
+ elif hasattr(BaseA2AClient.send_message, "__wrapped__"):
572
+ BaseA2AClient.send_message = BaseA2AClient.send_message.__wrapped__
573
+
574
+ if "BaseA2AClient.send_message_streaming" in _original_methods:
575
+ BaseA2AClient.send_message_streaming = _original_methods[
576
+ "BaseA2AClient.send_message_streaming"
577
+ ]
578
+ elif hasattr(BaseA2AClient, "send_message_streaming") and hasattr(
579
+ BaseA2AClient.send_message_streaming, "__wrapped__"
580
+ ):
581
+ BaseA2AClient.send_message_streaming = (
582
+ BaseA2AClient.send_message_streaming.__wrapped__
583
+ )
584
+ except ImportError:
585
+ pass
331
586
 
332
- A2AClient.send_message = A2AClient.send_message.__wrapped__
587
+ # Uninstrument legacy A2AClient
588
+ legacy_client = None
589
+ legacy_module_path = None
333
590
 
334
- # Uninstrument `broadcast_message`
335
- if hasattr(A2AClient, "broadcast_message") and hasattr(
336
- A2AClient.broadcast_message, "__wrapped__"
337
- ):
338
- A2AClient.broadcast_message = A2AClient.broadcast_message.__wrapped__
591
+ try:
592
+ from a2a.client.legacy import A2AClient as LegacyA2AClient
593
+
594
+ legacy_client = LegacyA2AClient
595
+ legacy_module_path = "a2a.client.legacy"
596
+ except ImportError:
597
+ try:
598
+ from a2a.client import A2AClient as LegacyA2AClient
599
+
600
+ if hasattr(LegacyA2AClient, "send_message"):
601
+ legacy_client = LegacyA2AClient
602
+ legacy_module_path = "a2a.client"
603
+ except ImportError:
604
+ pass
605
+
606
+ if legacy_client is not None:
607
+ key = f"{legacy_module_path}.A2AClient.send_message"
608
+ if key in _original_methods:
609
+ legacy_client.send_message = _original_methods[key]
610
+ elif hasattr(legacy_client.send_message, "__wrapped__"):
611
+ legacy_client.send_message = legacy_client.send_message.__wrapped__
612
+
613
+ broadcast_key = f"{legacy_module_path}.A2AClient.broadcast_message"
614
+ if broadcast_key in _original_methods:
615
+ legacy_client.broadcast_message = _original_methods[broadcast_key]
616
+ elif hasattr(legacy_client, "broadcast_message") and hasattr(
617
+ legacy_client.broadcast_message, "__wrapped__"
618
+ ):
619
+ legacy_client.broadcast_message = (
620
+ legacy_client.broadcast_message.__wrapped__
621
+ )
339
622
 
340
623
  # Uninstrument server handler
341
624
  from a2a.server.request_handlers import DefaultRequestHandler
342
625
 
343
- DefaultRequestHandler.on_message_send = (
344
- DefaultRequestHandler.on_message_send.__wrapped__
345
- )
626
+ if "DefaultRequestHandler.on_message_send" in _original_methods:
627
+ DefaultRequestHandler.on_message_send = _original_methods[
628
+ "DefaultRequestHandler.on_message_send"
629
+ ]
630
+ elif hasattr(DefaultRequestHandler.on_message_send, "__wrapped__"):
631
+ DefaultRequestHandler.on_message_send = (
632
+ DefaultRequestHandler.on_message_send.__wrapped__
633
+ )
634
+
635
+ if "DefaultRequestHandler.on_message_send_stream" in _original_methods:
636
+ DefaultRequestHandler.on_message_send_stream = _original_methods[
637
+ "DefaultRequestHandler.on_message_send_stream"
638
+ ]
639
+ elif hasattr(DefaultRequestHandler, "on_message_send_stream") and hasattr(
640
+ DefaultRequestHandler.on_message_send_stream, "__wrapped__"
641
+ ):
642
+ DefaultRequestHandler.on_message_send_stream = (
643
+ DefaultRequestHandler.on_message_send_stream.__wrapped__
644
+ )
346
645
 
347
- # handle slima2a
646
+ # Uninstrument slima2a
348
647
  if importlib.util.find_spec("slima2a"):
349
648
  from slima2a.client_transport import SRPCTransport
350
649
 
351
- # Uninstrument `send_message`
352
- if hasattr(SRPCTransport, "send_message") and hasattr(
650
+ if "SRPCTransport.send_message" in _original_methods:
651
+ SRPCTransport.send_message = _original_methods[
652
+ "SRPCTransport.send_message"
653
+ ]
654
+ elif hasattr(SRPCTransport, "send_message") and hasattr(
353
655
  SRPCTransport.send_message, "__wrapped__"
354
656
  ):
355
657
  SRPCTransport.send_message = SRPCTransport.send_message.__wrapped__
356
658
 
357
- if hasattr(SRPCTransport, "send_message_streaming") and hasattr(
659
+ if "SRPCTransport.send_message_streaming" in _original_methods:
660
+ SRPCTransport.send_message_streaming = _original_methods[
661
+ "SRPCTransport.send_message_streaming"
662
+ ]
663
+ elif hasattr(SRPCTransport, "send_message_streaming") and hasattr(
358
664
  SRPCTransport.send_message_streaming, "__wrapped__"
359
665
  ):
360
666
  SRPCTransport.send_message_streaming = (
361
- SRPCTransport.send_message.__wrapped__
667
+ SRPCTransport.send_message_streaming.__wrapped__
362
668
  )
669
+
670
+ # Clear stored original methods
671
+ _original_methods.clear()
@@ -7,16 +7,15 @@ import json
7
7
  import base64
8
8
  import threading
9
9
 
10
- from opentelemetry.context import get_value
11
10
  from opentelemetry import baggage, context
12
11
  from opentelemetry.baggage.propagation import W3CBaggagePropagator
12
+ from opentelemetry.context import get_value
13
13
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
14
14
  from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
15
15
 
16
16
  from ioa_observe.sdk import TracerWrapper
17
17
  from ioa_observe.sdk.client import kv_store
18
18
  from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
19
- from ioa_observe.sdk.tracing.context_manager import get_tracer
20
19
 
21
20
  _instruments = ("slim-bindings >= 0.4.0",)
22
21
  _global_tracer = None
@@ -81,10 +80,6 @@ class SLIMInstrumentor(BaseInstrumentor):
81
80
  if traceparent:
82
81
  with _kv_lock:
83
82
  session_id = kv_store.get(f"execution.{traceparent}")
84
- if not session_id:
85
- session_id = get_value("session.id")
86
- if session_id:
87
- kv_store.set(f"execution.{traceparent}", session_id)
88
83
 
89
84
  headers = {
90
85
  "session_id": session_id if session_id else None,
@@ -142,10 +137,8 @@ class SLIMInstrumentor(BaseInstrumentor):
142
137
  if traceparent:
143
138
  with _kv_lock:
144
139
  session_id = kv_store.get(f"execution.{traceparent}")
145
- if not session_id:
146
- session_id = get_value("session.id")
147
- if session_id:
148
- kv_store.set(f"execution.{traceparent}", session_id)
140
+ if session_id:
141
+ kv_store.set(f"execution.{traceparent}", session_id)
149
142
 
150
143
  headers = {
151
144
  "session_id": session_id if session_id else None,
@@ -207,10 +200,6 @@ class SLIMInstrumentor(BaseInstrumentor):
207
200
  if traceparent:
208
201
  with _kv_lock:
209
202
  session_id = kv_store.get(f"execution.{traceparent}")
210
- if not session_id:
211
- session_id = get_value("session.id")
212
- if session_id:
213
- kv_store.set(f"execution.{traceparent}", session_id)
214
203
 
215
204
  headers = {
216
205
  "session_id": session_id if session_id else None,
@@ -531,15 +520,7 @@ class SLIMInstrumentor(BaseInstrumentor):
531
520
  if timeout is not None:
532
521
  kwargs["timeout"] = timeout
533
522
 
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)
523
+ result = await original_get_message(self, **kwargs)
543
524
 
544
525
  # Handle different return types from get_message
545
526
  if result is None:
@@ -688,8 +669,10 @@ class SLIMInstrumentor(BaseInstrumentor):
688
669
  session_id = kv_store.get(f"execution.{traceparent}")
689
670
  if not session_id:
690
671
  session_id = get_value("session.id")
691
- if session_id:
692
- kv_store.set(f"execution.{traceparent}", session_id)
672
+ if session_id:
673
+ kv_store.set(
674
+ f"execution.{traceparent}", session_id
675
+ )
693
676
 
694
677
  headers = {
695
678
  "session_id": session_id if session_id else None,
@@ -800,8 +783,7 @@ class SLIMInstrumentor(BaseInstrumentor):
800
783
  @functools.wraps(original_method)
801
784
  async def instrumented_session_method(self, *args, **kwargs):
802
785
  if _global_tracer:
803
- tracer = get_tracer()
804
- with tracer.start_as_current_span(f"session.{method_name}"):
786
+ with _global_tracer.start_as_current_span(f"session.{method_name}"):
805
787
  traceparent = get_current_traceparent()
806
788
 
807
789
  # Handle message wrapping for publish methods
@@ -857,10 +839,6 @@ class SLIMInstrumentor(BaseInstrumentor):
857
839
  if traceparent:
858
840
  with _kv_lock:
859
841
  session_id = kv_store.get(f"execution.{traceparent}")
860
- if not session_id:
861
- session_id = get_value("session.id")
862
- if session_id:
863
- kv_store.set(f"execution.{traceparent}", session_id)
864
842
 
865
843
  if traceparent or session_id:
866
844
  headers = {
@@ -30,8 +30,15 @@ from opentelemetry.sdk.trace.export import (
30
30
  SimpleSpanProcessor,
31
31
  BatchSpanProcessor,
32
32
  )
33
- from opentelemetry.trace import get_tracer_provider, ProxyTracerProvider
33
+ from opentelemetry.trace import (
34
+ get_tracer_provider,
35
+ ProxyTracerProvider,
36
+ SpanContext,
37
+ TraceFlags,
38
+ NonRecordingSpan,
39
+ )
34
40
  from opentelemetry.context import get_value, attach, set_value
41
+ from opentelemetry.trace import set_span_in_context
35
42
  from opentelemetry.instrumentation.threading import ThreadingInstrumentor
36
43
  from opentelemetry.metrics import get_meter
37
44
  from ioa_observe.sdk.metrics.agents.agent_connections import connection_reliability
@@ -67,6 +74,7 @@ APP_NAME = ""
67
74
 
68
75
  SESSION_IDLE_TIMEOUT_SECONDS = 300 # e.g. 5 minutes
69
76
  SESSION_WATCHER_INTERVAL_SECONDS = 30
77
+ MAX_PROCESSED_SPANS_SIZE = 100000 # Maximum size before cleanup
70
78
 
71
79
 
72
80
  def determine_reliability_score(span):
@@ -136,14 +144,19 @@ class TracerWrapper(object):
136
144
  if not TracerWrapper.endpoint:
137
145
  return obj
138
146
 
139
- # session activity tracking
140
- obj._session_last_activity: dict[str, float] = {}
147
+ # session activity tracking: {session_id: (last_activity_time, trace_id)}
148
+ obj._session_last_activity: dict[str, tuple[float, int]] = {}
141
149
  obj._session_lock = threading.Lock()
150
+ # Track sessions that have already been ended to prevent duplicates
151
+ obj._ended_sessions: set[str] = set()
152
+ obj._ended_sessions_lock = threading.Lock()
142
153
  obj.__image_uploader = image_uploader
143
154
  # {(agent_name): [success_count, total_count]}
144
155
  obj._agent_execution_counts = {}
156
+ obj._agent_execution_counts_lock = threading.Lock()
145
157
  # Track spans that have been processed to avoid duplicates
146
158
  obj._processed_spans = set()
159
+ obj._processed_spans_lock = threading.Lock()
147
160
  TracerWrapper.app_name = TracerWrapper.resource_attributes.get(
148
161
  "service.name", "observe"
149
162
  )
@@ -297,29 +310,58 @@ class TracerWrapper(object):
297
310
  while True:
298
311
  time.sleep(SESSION_WATCHER_INTERVAL_SECONDS)
299
312
  now = time.time()
300
- expired: dict[str, float] = {}
313
+ expired: dict[str, tuple[float, int]] = {}
301
314
 
302
315
  # Find idle sessions and remove them from _session_last_activity
303
316
  with self._session_lock:
304
- for session_id, last_ts in list(self._session_last_activity.items()):
317
+ for session_id, (last_ts, trace_id) in list(
318
+ self._session_last_activity.items()
319
+ ):
305
320
  if now - last_ts > SESSION_IDLE_TIMEOUT_SECONDS:
306
- expired[session_id] = last_ts
321
+ expired[session_id] = (last_ts, trace_id)
307
322
  del self._session_last_activity[session_id]
308
323
 
324
+ # Periodic cleanup of processed spans to prevent unbounded memory growth
325
+ with self._processed_spans_lock:
326
+ if len(self._processed_spans) > MAX_PROCESSED_SPANS_SIZE:
327
+ self._processed_spans.clear()
328
+
329
+ # Periodic cleanup of ended sessions to prevent unbounded memory growth
330
+ with self._ended_sessions_lock:
331
+ if len(self._ended_sessions) > MAX_PROCESSED_SPANS_SIZE:
332
+ self._ended_sessions.clear()
333
+
309
334
  if not expired:
310
335
  continue
311
336
 
312
337
  tracer = self.get_tracer()
313
338
 
314
339
  # 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:
340
+ for session_id, (last_ts, trace_id) in list(expired.items()):
341
+ # Check if this session has already been ended to prevent duplicates
342
+ with self._ended_sessions_lock:
343
+ if session_id in self._ended_sessions:
344
+ continue
345
+ self._ended_sessions.add(session_id)
346
+
347
+ # Create a parent context from the stored trace_id to keep session.end under the same trace
348
+ parent_span_context = SpanContext(
349
+ trace_id=trace_id,
350
+ span_id=0, # Use 0 as we don't have a specific parent span
351
+ is_remote=False,
352
+ trace_flags=TraceFlags(TraceFlags.SAMPLED),
353
+ )
354
+ parent_span = NonRecordingSpan(parent_span_context)
355
+ parent_context = set_span_in_context(parent_span)
356
+
357
+ with tracer.start_as_current_span(
358
+ "session.end", context=parent_context
359
+ ) as span:
318
360
  span.set_attribute("session.id", session_id)
319
361
  workflow_name = get_value("workflow_name")
320
362
  if workflow_name:
321
363
  span.set_attribute(OBSERVE_WORKFLOW_NAME, workflow_name)
322
- span.set_attribute("session.ended_at", _last_ts)
364
+ span.set_attribute("session.ended_at", last_ts)
323
365
 
324
366
  # ensure end spans are exported reasonably fast
325
367
  self.flush()
@@ -330,11 +372,29 @@ class TracerWrapper(object):
330
372
  tracer = self.get_tracer()
331
373
 
332
374
  with self._session_lock:
333
- remaining_ids = list(self._session_last_activity.keys())
375
+ remaining_sessions = dict(self._session_last_activity)
334
376
  self._session_last_activity.clear()
335
377
 
336
- for session_id in remaining_ids:
337
- with tracer.start_as_current_span("session.end") as span:
378
+ for session_id, (_, trace_id) in remaining_sessions.items():
379
+ # Check if this session has already been ended to prevent duplicates
380
+ with self._ended_sessions_lock:
381
+ if session_id in self._ended_sessions:
382
+ continue
383
+ self._ended_sessions.add(session_id)
384
+
385
+ # Create a parent context from the stored trace_id to keep session.end under the same trace
386
+ parent_span_context = SpanContext(
387
+ trace_id=trace_id,
388
+ span_id=0, # Use 0 as we don't have a specific parent span
389
+ is_remote=False,
390
+ trace_flags=TraceFlags(TraceFlags.SAMPLED),
391
+ )
392
+ parent_span = NonRecordingSpan(parent_span_context)
393
+ parent_context = set_span_in_context(parent_span)
394
+
395
+ with tracer.start_as_current_span(
396
+ "session.end", context=parent_context
397
+ ) as span:
338
398
  span.set_attribute("session.id", session_id)
339
399
  workflow_name = get_value("workflow_name")
340
400
  if workflow_name:
@@ -420,18 +480,25 @@ class TracerWrapper(object):
420
480
  # Added for avoid duplicate on_ending with manual triggers
421
481
  # from decorators (@tool, @workflow) in base.py
422
482
  span_id = span.context.span_id
423
- if span_id in self._processed_spans:
424
- # This span was already processed, skip to avoid duplicates
425
- return
483
+ with self._processed_spans_lock:
484
+ if span_id in self._processed_spans:
485
+ # This span was already processed, skip to avoid duplicates
486
+ return
426
487
 
427
- # Mark this span as processed
428
- self._processed_spans.add(span_id)
488
+ # Mark this span as processed
489
+ self._processed_spans.add(span_id)
429
490
 
430
491
  # update last activity per session
492
+ # Skip session.end spans to avoid infinite loop - these are cleanup spans
493
+ # that should not re-register the session as active
431
494
  session_id = span.attributes.get("session.id")
432
- if session_id:
495
+ if session_id and span.name != "session.end":
433
496
  with self._session_lock:
434
- self._session_last_activity[session_id] = time.time()
497
+ # Store both the last activity time and the trace_id for this session
498
+ self._session_last_activity[session_id] = (
499
+ time.time(),
500
+ span.context.trace_id,
501
+ )
435
502
 
436
503
  determine_reliability_score(span)
437
504
  # start_time = span.attributes.get("ioa_start_time")
@@ -567,21 +634,23 @@ class TracerWrapper(object):
567
634
  return self.__tracer_provider.get_tracer(TRACER_NAME)
568
635
 
569
636
  def record_agent_execution(self, agent_name: str, success: bool):
570
- counts = self._agent_execution_counts.setdefault(agent_name, [0, 0])
571
- if success:
572
- counts[0] += 1 # success count
573
- counts[1] += 1 # total count
637
+ with self._agent_execution_counts_lock:
638
+ counts = self._agent_execution_counts.setdefault(agent_name, [0, 0])
639
+ if success:
640
+ counts[0] += 1 # success count
641
+ counts[1] += 1 # total count
574
642
 
575
643
  def _observe_agent_execution_success_rate(self, observer):
576
644
  measurements = []
577
- for agent_name, (
578
- success_count,
579
- total_count,
580
- ) in self._agent_execution_counts.items():
581
- rate = (success_count / total_count) if total_count > 0 else 0.0
582
- measurements.append(
583
- Observation(value=rate, attributes={"agent_name": agent_name})
584
- )
645
+ with self._agent_execution_counts_lock:
646
+ for agent_name, (
647
+ success_count,
648
+ total_count,
649
+ ) in self._agent_execution_counts.items():
650
+ rate = (success_count / total_count) if total_count > 0 else 0.0
651
+ measurements.append(
652
+ Observation(value=rate, attributes={"agent_name": agent_name})
653
+ )
585
654
  return measurements
586
655
 
587
656
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ioa-observe-sdk
3
- Version: 1.0.27
3
+ Version: 1.0.29
4
4
  Summary: IOA Observability SDK
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -8,37 +8,37 @@ Description-Content-Type: text/markdown
8
8
  License-File: LICENSE.md
9
9
  Requires-Dist: colorama==0.4.6
10
10
  Requires-Dist: requests>=2.32.3
11
- Requires-Dist: opentelemetry-api==1.37.0
11
+ Requires-Dist: opentelemetry-api==1.39.1
12
12
  Requires-Dist: opentelemetry-distro
13
- Requires-Dist: opentelemetry-exporter-otlp==1.37.0
14
- Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.37.0
15
- Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.37.0
16
- Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.37.0
13
+ Requires-Dist: opentelemetry-exporter-otlp==1.39.1
14
+ Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.39.1
15
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.39.1
16
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.39.1
17
17
  Requires-Dist: opentelemetry-instrumentation
18
- Requires-Dist: opentelemetry-instrumentation-logging==0.58b0
19
- Requires-Dist: opentelemetry-instrumentation-openai==0.43.1
20
- Requires-Dist: opentelemetry-instrumentation-llamaindex==0.43.1
21
- Requires-Dist: opentelemetry-instrumentation-ollama==0.43.1
22
- Requires-Dist: opentelemetry-instrumentation-anthropic==0.43.1
23
- Requires-Dist: opentelemetry-instrumentation-langchain==0.43.1
24
- Requires-Dist: opentelemetry-instrumentation-bedrock==0.43.1
25
- Requires-Dist: opentelemetry-instrumentation-cohere==0.43.1
26
- Requires-Dist: opentelemetry-instrumentation-crewai==0.43.1
27
- Requires-Dist: opentelemetry-instrumentation-google-generativeai==0.43.1
28
- Requires-Dist: opentelemetry-instrumentation-groq==0.43.1
29
- Requires-Dist: opentelemetry-instrumentation-mistralai==0.43.1
30
- Requires-Dist: opentelemetry-instrumentation-requests==0.58b0
31
- Requires-Dist: opentelemetry-instrumentation-sagemaker==0.43.1
32
- Requires-Dist: opentelemetry-instrumentation-threading==0.58b0
33
- Requires-Dist: opentelemetry-instrumentation-together==0.43.1
34
- Requires-Dist: opentelemetry-instrumentation-transformers==0.43.1
35
- Requires-Dist: opentelemetry-instrumentation-urllib3==0.58b0
36
- Requires-Dist: opentelemetry-instrumentation-vertexai==0.43.1
37
- Requires-Dist: opentelemetry-proto==1.37.0
38
- Requires-Dist: opentelemetry-sdk==1.37.0
39
- Requires-Dist: opentelemetry-semantic-conventions==0.58b0
18
+ Requires-Dist: opentelemetry-instrumentation-logging==0.60b1
19
+ Requires-Dist: opentelemetry-instrumentation-openai==0.52.1
20
+ Requires-Dist: opentelemetry-instrumentation-llamaindex==0.52.1
21
+ Requires-Dist: opentelemetry-instrumentation-ollama==0.52.1
22
+ Requires-Dist: opentelemetry-instrumentation-anthropic==0.52.1
23
+ Requires-Dist: opentelemetry-instrumentation-langchain==0.52.1
24
+ Requires-Dist: opentelemetry-instrumentation-bedrock==0.52.1
25
+ Requires-Dist: opentelemetry-instrumentation-cohere==0.52.1
26
+ Requires-Dist: opentelemetry-instrumentation-crewai==0.52.1
27
+ Requires-Dist: opentelemetry-instrumentation-google-generativeai==0.52.1
28
+ Requires-Dist: opentelemetry-instrumentation-groq==0.52.1
29
+ Requires-Dist: opentelemetry-instrumentation-mistralai==0.52.1
30
+ Requires-Dist: opentelemetry-instrumentation-requests==0.60b1
31
+ Requires-Dist: opentelemetry-instrumentation-sagemaker==0.52.1
32
+ Requires-Dist: opentelemetry-instrumentation-threading==0.60b1
33
+ Requires-Dist: opentelemetry-instrumentation-together==0.52.1
34
+ Requires-Dist: opentelemetry-instrumentation-transformers==0.52.1
35
+ Requires-Dist: opentelemetry-instrumentation-urllib3==0.60b1
36
+ Requires-Dist: opentelemetry-instrumentation-vertexai==0.52.1
37
+ Requires-Dist: opentelemetry-proto==1.39.1
38
+ Requires-Dist: opentelemetry-sdk==1.39.1
39
+ Requires-Dist: opentelemetry-semantic-conventions==0.60b1
40
40
  Requires-Dist: opentelemetry-semantic-conventions-ai>=0.4.11
41
- Requires-Dist: opentelemetry-util-http==0.58b0
41
+ Requires-Dist: opentelemetry-util-http==0.60b1
42
42
  Requires-Dist: langgraph>=0.3.2
43
43
  Requires-Dist: langchain>=0.3.19
44
44
  Requires-Dist: langchain-openai>=0.3.8
@@ -4,7 +4,7 @@ ioa_observe/sdk/instruments.py,sha256=cA5Yq1BYFovMrYUNYQXua-JXsMtMOa_YOn6yiJZNwL
4
4
  ioa_observe/sdk/telemetry.py,sha256=6wwaOYhZMjAZ6dXDdBA2LUWo3LLptTcy93BJqDdbqBM,3103
5
5
  ioa_observe/sdk/version.py,sha256=oriNAY8huVDPw5N_rv5F_PehFrcGo37FSGBCfZCM81M,121
6
6
  ioa_observe/sdk/client/__init__.py,sha256=V4Rt-Z1EHlM12Lx3hGd0Ew70V1JKAQZXNb9ABtdWHEI,224
7
- ioa_observe/sdk/client/client.py,sha256=6TVOo_E1ulE3WO_CYG7oPgeucs-qegOA09uTO3yQiyk,2112
7
+ ioa_observe/sdk/client/client.py,sha256=i4ZlYrhFcXfg5BTROLAKxhfAAsb-7RS0ch9gucsWXd0,2480
8
8
  ioa_observe/sdk/client/http.py,sha256=LdLYSQPFIhKN5BTB-N78jLO7ITl7jGjA0-qpewEIvO4,1724
9
9
  ioa_observe/sdk/config/__init__.py,sha256=8aVNaw0yRNLFPxlf97iOZLlJVcV81ivSDnudH2m1OIo,572
10
10
  ioa_observe/sdk/connectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -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=LHvvQUluPtmvbRNuoqf-EkWlZJa_NicerTpM3t1UZws,15299
17
+ ioa_observe/sdk/instrumentations/a2a.py,sha256=smPMHhFONi9B7r2qVHe-YWoFctjHWehO_MR0kY_DgVI,27897
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=c_Q1cb6pdjZLOw1MbclUf1eRFrc_sUEe9pQJvAyoD1Y,45712
20
+ ioa_observe/sdk/instrumentations/slim.py,sha256=pwKUnCvDmydysQ9VpqnREf4DyHjN2bZ-e54YyX-de1o,44500
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=7eJAEqaAEBZ2gjIXrdb7SyUidA8P10GnpBLzr8InLKI,50007
38
+ ioa_observe/sdk/tracing/tracing.py,sha256=pGiIa1S3562QRlTG5mANtU94wkFjKspnX21N_bJ7SfY,53359
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.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,,
45
+ ioa_observe_sdk-1.0.29.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
46
+ ioa_observe_sdk-1.0.29.dist-info/METADATA,sha256=qksjp3ihfWLY1ihgNoZv13Q4mluzMOfTP2kP6Tud1GA,7997
47
+ ioa_observe_sdk-1.0.29.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
48
+ ioa_observe_sdk-1.0.29.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
49
+ ioa_observe_sdk-1.0.29.dist-info/RECORD,,