ioa-observe-sdk 1.0.26__tar.gz → 1.0.28__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/PKG-INFO +1 -1
  2. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/client/client.py +13 -4
  3. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/instrumentations/a2a.py +133 -0
  4. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/instrumentations/slim.py +9 -1
  5. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/tracing/tracing.py +101 -22
  6. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe_sdk.egg-info/PKG-INFO +1 -1
  7. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/pyproject.toml +1 -1
  8. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/LICENSE.md +0 -0
  9. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/README.md +0 -0
  10. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/__init__.py +0 -0
  11. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/__init__.py +0 -0
  12. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/client/__init__.py +0 -0
  13. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/client/http.py +0 -0
  14. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/config/__init__.py +0 -0
  15. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/connectors/__init__.py +0 -0
  16. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/connectors/slim.py +0 -0
  17. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/decorators/__init__.py +0 -0
  18. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/decorators/base.py +0 -0
  19. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/decorators/helpers.py +0 -0
  20. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/decorators/util.py +0 -0
  21. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/instrumentations/__init__.py +0 -0
  22. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/instrumentations/mcp.py +0 -0
  23. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/instrumentations/nats.py +0 -0
  24. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/instruments.py +0 -0
  25. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/logging/__init__.py +0 -0
  26. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/logging/logging.py +0 -0
  27. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/__init__.py +0 -0
  28. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/agent.py +0 -0
  29. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/agents/__init__.py +0 -0
  30. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/agents/agent_connections.py +0 -0
  31. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/agents/availability.py +0 -0
  32. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/agents/heuristics.py +0 -0
  33. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/agents/recovery_tracker.py +0 -0
  34. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/agents/tool_call_tracker.py +0 -0
  35. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/agents/tracker.py +0 -0
  36. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/metrics/metrics.py +0 -0
  37. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/telemetry.py +0 -0
  38. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/tracing/__init__.py +0 -0
  39. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/tracing/content_allow_list.py +0 -0
  40. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/tracing/context_manager.py +0 -0
  41. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/tracing/context_utils.py +0 -0
  42. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/tracing/manual.py +0 -0
  43. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/tracing/transform_span.py +0 -0
  44. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/utils/__init__.py +0 -0
  45. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/utils/const.py +0 -0
  46. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/utils/in_memory_span_exporter.py +0 -0
  47. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/utils/json_encoder.py +0 -0
  48. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/utils/package_check.py +0 -0
  49. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe/sdk/version.py +0 -0
  50. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe_sdk.egg-info/SOURCES.txt +0 -0
  51. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe_sdk.egg-info/dependency_links.txt +0 -0
  52. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe_sdk.egg-info/requires.txt +0 -0
  53. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/ioa_observe_sdk.egg-info/top_level.txt +0 -0
  54. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/setup.cfg +0 -0
  55. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/tests/test_client.py +0 -0
  56. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/tests/test_instrumentor.py +0 -0
  57. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/tests/test_manual_instrumentation.py +0 -0
  58. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/tests/test_transform_span.py +0 -0
  59. {ioa_observe_sdk-1.0.26 → ioa_observe_sdk-1.0.28}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ioa-observe-sdk
3
- Version: 1.0.26
3
+ Version: 1.0.28
4
4
  Summary: IOA Observability SDK
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -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()
@@ -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
- result = await original_get_message(self, **kwargs)
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,10 @@ 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
+ MAX_PROCESSED_SPANS_SIZE = 100000 # Maximum size before cleanup
71
+
67
72
 
68
73
  def determine_reliability_score(span):
69
74
  if "observe.entity.output" in span.attributes:
@@ -132,11 +137,16 @@ class TracerWrapper(object):
132
137
  if not TracerWrapper.endpoint:
133
138
  return obj
134
139
 
140
+ # session activity tracking
141
+ obj._session_last_activity: dict[str, float] = {}
142
+ obj._session_lock = threading.Lock()
135
143
  obj.__image_uploader = image_uploader
136
144
  # {(agent_name): [success_count, total_count]}
137
145
  obj._agent_execution_counts = {}
146
+ obj._agent_execution_counts_lock = threading.Lock()
138
147
  # Track spans that have been processed to avoid duplicates
139
148
  obj._processed_spans = set()
149
+ obj._processed_spans_lock = threading.Lock()
140
150
  TracerWrapper.app_name = TracerWrapper.resource_attributes.get(
141
151
  "service.name", "observe"
142
152
  )
@@ -270,12 +280,74 @@ class TracerWrapper(object):
270
280
 
271
281
  obj.__content_allow_list = ContentAllowList()
272
282
 
283
+ # start background watcher for session end
284
+ obj._start_session_watcher()
285
+
273
286
  # Force flushes for debug environments (e.g. local development)
274
287
  atexit.register(obj.exit_handler)
275
288
 
276
289
  return cls.instance
277
290
 
291
+ def _start_session_watcher(self) -> None:
292
+ t = threading.Thread(
293
+ target=self._session_watcher_loop,
294
+ name="ioa-session-watcher",
295
+ daemon=True,
296
+ )
297
+ t.start()
298
+
299
+ def _session_watcher_loop(self) -> None:
300
+ while True:
301
+ time.sleep(SESSION_WATCHER_INTERVAL_SECONDS)
302
+ now = time.time()
303
+ expired: dict[str, float] = {}
304
+
305
+ # Find idle sessions and remove them from _session_last_activity
306
+ with self._session_lock:
307
+ for session_id, last_ts in list(self._session_last_activity.items()):
308
+ if now - last_ts > SESSION_IDLE_TIMEOUT_SECONDS:
309
+ expired[session_id] = last_ts
310
+ del self._session_last_activity[session_id]
311
+
312
+ # Periodic cleanup of processed spans to prevent unbounded memory growth
313
+ with self._processed_spans_lock:
314
+ if len(self._processed_spans) > MAX_PROCESSED_SPANS_SIZE:
315
+ self._processed_spans.clear()
316
+
317
+ if not expired:
318
+ continue
319
+
320
+ tracer = self.get_tracer()
321
+
322
+ # Iterate over a snapshot and do *not* modify `expired` in the loop
323
+ for session_id, _last_ts in list(expired.items()):
324
+ with tracer.start_as_current_span("session.end") as span:
325
+ span.set_attribute("session.id", session_id)
326
+ workflow_name = get_value("workflow_name")
327
+ if workflow_name:
328
+ span.set_attribute(OBSERVE_WORKFLOW_NAME, workflow_name)
329
+ span.set_attribute("session.ended_at", _last_ts)
330
+
331
+ # ensure end spans are exported reasonably fast
332
+ self.flush()
333
+
278
334
  def exit_handler(self):
335
+ # emit end spans for any sessions that never went idle
336
+ now = time.time()
337
+ tracer = self.get_tracer()
338
+
339
+ with self._session_lock:
340
+ remaining_ids = list(self._session_last_activity.keys())
341
+ self._session_last_activity.clear()
342
+
343
+ for session_id in remaining_ids:
344
+ with tracer.start_as_current_span("session.end") as span:
345
+ span.set_attribute("session.id", session_id)
346
+ workflow_name = get_value("workflow_name")
347
+ if workflow_name:
348
+ span.set_attribute(OBSERVE_WORKFLOW_NAME, workflow_name)
349
+ span.set_attribute("session.ended_at", now)
350
+
279
351
  self.flush()
280
352
 
281
353
  def _span_processor_on_start(self, span, parent_context):
@@ -355,12 +427,21 @@ class TracerWrapper(object):
355
427
  # Added for avoid duplicate on_ending with manual triggers
356
428
  # from decorators (@tool, @workflow) in base.py
357
429
  span_id = span.context.span_id
358
- if span_id in self._processed_spans:
359
- # This span was already processed, skip to avoid duplicates
360
- return
361
-
362
- # Mark this span as processed
363
- self._processed_spans.add(span_id)
430
+ with self._processed_spans_lock:
431
+ if span_id in self._processed_spans:
432
+ # This span was already processed, skip to avoid duplicates
433
+ return
434
+
435
+ # Mark this span as processed
436
+ self._processed_spans.add(span_id)
437
+
438
+ # update last activity per session
439
+ # Skip session.end spans to avoid infinite loop - these are cleanup spans
440
+ # that should not re-register the session as active
441
+ session_id = span.attributes.get("session.id")
442
+ if session_id and span.name != "session.end":
443
+ with self._session_lock:
444
+ self._session_last_activity[session_id] = time.time()
364
445
 
365
446
  determine_reliability_score(span)
366
447
  # start_time = span.attributes.get("ioa_start_time")
@@ -382,10 +463,6 @@ class TracerWrapper(object):
382
463
  except Exception as e:
383
464
  logging.error(f"Error applying span transformation: {e}")
384
465
 
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
466
  # Call original on_end method if it exists
390
467
  if (
391
468
  hasattr(self, "_TracerWrapper__spans_processor_original_on_end")
@@ -500,21 +577,23 @@ class TracerWrapper(object):
500
577
  return self.__tracer_provider.get_tracer(TRACER_NAME)
501
578
 
502
579
  def record_agent_execution(self, agent_name: str, success: bool):
503
- counts = self._agent_execution_counts.setdefault(agent_name, [0, 0])
504
- if success:
505
- counts[0] += 1 # success count
506
- counts[1] += 1 # total count
580
+ with self._agent_execution_counts_lock:
581
+ counts = self._agent_execution_counts.setdefault(agent_name, [0, 0])
582
+ if success:
583
+ counts[0] += 1 # success count
584
+ counts[1] += 1 # total count
507
585
 
508
586
  def _observe_agent_execution_success_rate(self, observer):
509
587
  measurements = []
510
- for agent_name, (
511
- success_count,
512
- total_count,
513
- ) in self._agent_execution_counts.items():
514
- rate = (success_count / total_count) if total_count > 0 else 0.0
515
- measurements.append(
516
- Observation(value=rate, attributes={"agent_name": agent_name})
517
- )
588
+ with self._agent_execution_counts_lock:
589
+ for agent_name, (
590
+ success_count,
591
+ total_count,
592
+ ) in self._agent_execution_counts.items():
593
+ rate = (success_count / total_count) if total_count > 0 else 0.0
594
+ measurements.append(
595
+ Observation(value=rate, attributes={"agent_name": agent_name})
596
+ )
518
597
  return measurements
519
598
 
520
599
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ioa-observe-sdk
3
- Version: 1.0.26
3
+ Version: 1.0.28
4
4
  Summary: IOA Observability SDK
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "ioa-observe-sdk"
8
- version = "1.0.26"
8
+ version = "1.0.28"
9
9
  license = "Apache-2.0"
10
10
  description = "IOA Observability SDK"
11
11
  readme = "README.md"