ioa-observe-sdk 1.0.6__py3-none-any.whl → 1.0.8__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/decorators/__init__.py +22 -10
- ioa_observe/sdk/decorators/base.py +82 -23
- ioa_observe/sdk/decorators/util.py +17 -10
- ioa_observe/sdk/instrumentations/a2a.py +111 -0
- ioa_observe/sdk/instrumentations/slim.py +18 -18
- ioa_observe/sdk/tracing/__init__.py +2 -2
- ioa_observe/sdk/tracing/context_utils.py +19 -19
- ioa_observe/sdk/tracing/tracing.py +48 -28
- {ioa_observe_sdk-1.0.6.dist-info → ioa_observe_sdk-1.0.8.dist-info}/METADATA +99 -47
- {ioa_observe_sdk-1.0.6.dist-info → ioa_observe_sdk-1.0.8.dist-info}/RECORD +13 -12
- {ioa_observe_sdk-1.0.6.dist-info → ioa_observe_sdk-1.0.8.dist-info}/WHEEL +0 -0
- {ioa_observe_sdk-1.0.6.dist-info → ioa_observe_sdk-1.0.8.dist-info}/licenses/LICENSE.md +0 -0
- {ioa_observe_sdk-1.0.6.dist-info → ioa_observe_sdk-1.0.8.dist-info}/top_level.txt +0 -0
|
@@ -19,15 +19,22 @@ F = TypeVar("F", bound=Callable[P, Union[R, Awaitable[R]]])
|
|
|
19
19
|
|
|
20
20
|
def task(
|
|
21
21
|
name: Optional[str] = None,
|
|
22
|
+
description: Optional[str] = None,
|
|
22
23
|
version: Optional[int] = None,
|
|
23
24
|
method_name: Optional[str] = None,
|
|
24
25
|
tlp_span_kind: Optional[ObserveSpanKindValues] = ObserveSpanKindValues.TASK,
|
|
25
26
|
) -> Callable[[F], F]:
|
|
26
27
|
if method_name is None:
|
|
27
|
-
return entity_method(
|
|
28
|
+
return entity_method(
|
|
29
|
+
name=name,
|
|
30
|
+
description=description,
|
|
31
|
+
version=version,
|
|
32
|
+
tlp_span_kind=tlp_span_kind,
|
|
33
|
+
)
|
|
28
34
|
else:
|
|
29
35
|
return entity_class(
|
|
30
36
|
name=name,
|
|
37
|
+
description=description,
|
|
31
38
|
version=version,
|
|
32
39
|
method_name=method_name,
|
|
33
40
|
tlp_span_kind=tlp_span_kind,
|
|
@@ -36,21 +43,21 @@ def task(
|
|
|
36
43
|
|
|
37
44
|
def workflow(
|
|
38
45
|
name: Optional[str] = None,
|
|
46
|
+
description: Optional[str] = None,
|
|
39
47
|
version: Optional[int] = None,
|
|
40
48
|
method_name: Optional[str] = None,
|
|
41
49
|
tlp_span_kind: Optional[
|
|
42
50
|
Union[ObserveSpanKindValues, str]
|
|
43
51
|
] = ObserveSpanKindValues.WORKFLOW,
|
|
44
52
|
) -> Callable[[F], F]:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
53
|
+
# Always use entity_class for class decorators
|
|
54
|
+
return entity_class(
|
|
55
|
+
name=name,
|
|
56
|
+
description=description,
|
|
57
|
+
version=version,
|
|
58
|
+
method_name=method_name,
|
|
59
|
+
tlp_span_kind=tlp_span_kind,
|
|
60
|
+
)
|
|
54
61
|
|
|
55
62
|
|
|
56
63
|
def graph(
|
|
@@ -60,6 +67,7 @@ def graph(
|
|
|
60
67
|
) -> Callable[[F], F]:
|
|
61
68
|
return workflow(
|
|
62
69
|
name=name,
|
|
70
|
+
description=None,
|
|
63
71
|
version=version,
|
|
64
72
|
method_name=method_name,
|
|
65
73
|
tlp_span_kind="graph",
|
|
@@ -68,11 +76,13 @@ def graph(
|
|
|
68
76
|
|
|
69
77
|
def agent(
|
|
70
78
|
name: Optional[str] = None,
|
|
79
|
+
description: Optional[str] = None,
|
|
71
80
|
version: Optional[int] = None,
|
|
72
81
|
method_name: Optional[str] = None,
|
|
73
82
|
) -> Callable[[F], F]:
|
|
74
83
|
return workflow(
|
|
75
84
|
name=name,
|
|
85
|
+
description=description,
|
|
76
86
|
version=version,
|
|
77
87
|
method_name=method_name,
|
|
78
88
|
tlp_span_kind=ObserveSpanKindValues.AGENT,
|
|
@@ -81,11 +91,13 @@ def agent(
|
|
|
81
91
|
|
|
82
92
|
def tool(
|
|
83
93
|
name: Optional[str] = None,
|
|
94
|
+
description: Optional[str] = None,
|
|
84
95
|
version: Optional[int] = None,
|
|
85
96
|
method_name: Optional[str] = None,
|
|
86
97
|
) -> Callable[[F], F]:
|
|
87
98
|
return task(
|
|
88
99
|
name=name,
|
|
100
|
+
description=description,
|
|
89
101
|
version=version,
|
|
90
102
|
method_name=method_name,
|
|
91
103
|
tlp_span_kind=ObserveSpanKindValues.TOOL,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import time
|
|
5
6
|
import traceback
|
|
6
7
|
from functools import wraps
|
|
7
8
|
import os
|
|
@@ -91,6 +92,7 @@ def _setup_span(
|
|
|
91
92
|
entity_name,
|
|
92
93
|
tlp_span_kind: Optional[ObserveSpanKindValues] = None,
|
|
93
94
|
version: Optional[int] = None,
|
|
95
|
+
description: Optional[str] = None,
|
|
94
96
|
):
|
|
95
97
|
"""Sets up the OpenTelemetry span and context"""
|
|
96
98
|
if tlp_span_kind in [
|
|
@@ -100,8 +102,8 @@ def _setup_span(
|
|
|
100
102
|
]:
|
|
101
103
|
set_workflow_name(entity_name)
|
|
102
104
|
# if tlp_span_kind == "graph":
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
+
# session_id = entity_name + "_" + str(uuid.uuid4())
|
|
106
|
+
# set_session_id(session_id)
|
|
105
107
|
if tlp_span_kind == "graph":
|
|
106
108
|
span_name = f"{entity_name}.{tlp_span_kind}"
|
|
107
109
|
else:
|
|
@@ -122,13 +124,14 @@ def _setup_span(
|
|
|
122
124
|
"agent_start_event",
|
|
123
125
|
{
|
|
124
126
|
"agent_name": entity_name,
|
|
127
|
+
"description": description if description else "",
|
|
125
128
|
"type": tlp_span_kind.value,
|
|
126
129
|
},
|
|
127
130
|
)
|
|
128
131
|
# start_span.end() # end the span immediately
|
|
129
|
-
#
|
|
130
|
-
# if
|
|
131
|
-
# span.set_attribute("
|
|
132
|
+
# session_id = get_value("session.id")
|
|
133
|
+
# if session_id is not None:
|
|
134
|
+
# span.set_attribute("session.id", session_id)
|
|
132
135
|
if tlp_span_kind in [
|
|
133
136
|
ObserveSpanKindValues.TASK,
|
|
134
137
|
ObserveSpanKindValues.TOOL,
|
|
@@ -145,9 +148,8 @@ def _setup_span(
|
|
|
145
148
|
if version:
|
|
146
149
|
span.set_attribute(OBSERVE_ENTITY_VERSION, version)
|
|
147
150
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# span.set_attribute("execution.id", execution_id)
|
|
151
|
+
if tlp_span_kind == ObserveSpanKindValues.AGENT:
|
|
152
|
+
span.set_attribute("agent_chain_start_time", time.time())
|
|
151
153
|
|
|
152
154
|
return span, ctx, ctx_token
|
|
153
155
|
|
|
@@ -243,12 +245,27 @@ def _handle_span_output(span, tlp_span_kind, res, cls=None):
|
|
|
243
245
|
|
|
244
246
|
def _cleanup_span(span, ctx_token):
|
|
245
247
|
"""End the span process and detach the context token"""
|
|
248
|
+
|
|
249
|
+
# Calculate agent chain completion time before ending span
|
|
250
|
+
span_kind = span.attributes.get(OBSERVE_SPAN_KIND)
|
|
251
|
+
if span_kind == ObserveSpanKindValues.AGENT.value:
|
|
252
|
+
start_time = span.attributes.get("agent_chain_start_time")
|
|
253
|
+
if start_time is not None:
|
|
254
|
+
import time
|
|
255
|
+
|
|
256
|
+
completion_time = time.time() - start_time
|
|
257
|
+
|
|
258
|
+
# Emit the metric
|
|
259
|
+
TracerWrapper().agent_chain_completion_time_histogram.record(
|
|
260
|
+
completion_time, attributes=span.attributes
|
|
261
|
+
)
|
|
246
262
|
span.end()
|
|
247
263
|
context_api.detach(ctx_token)
|
|
248
264
|
|
|
249
265
|
|
|
250
266
|
def entity_method(
|
|
251
267
|
name: Optional[str] = None,
|
|
268
|
+
description: Optional[str] = None,
|
|
252
269
|
version: Optional[int] = None,
|
|
253
270
|
tlp_span_kind: Optional[ObserveSpanKindValues] = ObserveSpanKindValues.TASK,
|
|
254
271
|
) -> Callable[[F], F]:
|
|
@@ -266,7 +283,10 @@ def entity_method(
|
|
|
266
283
|
return
|
|
267
284
|
|
|
268
285
|
span, ctx, ctx_token = _setup_span(
|
|
269
|
-
entity_name,
|
|
286
|
+
entity_name,
|
|
287
|
+
tlp_span_kind,
|
|
288
|
+
version,
|
|
289
|
+
description if description else None,
|
|
270
290
|
)
|
|
271
291
|
_handle_span_input(span, args, kwargs, cls=JSONEncoder)
|
|
272
292
|
|
|
@@ -284,7 +304,10 @@ def entity_method(
|
|
|
284
304
|
return await fn(*args, **kwargs)
|
|
285
305
|
|
|
286
306
|
span, ctx, ctx_token = _setup_span(
|
|
287
|
-
entity_name,
|
|
307
|
+
entity_name,
|
|
308
|
+
tlp_span_kind,
|
|
309
|
+
version,
|
|
310
|
+
description if description else None,
|
|
288
311
|
)
|
|
289
312
|
_handle_span_input(span, args, kwargs, cls=JSONEncoder)
|
|
290
313
|
success = False
|
|
@@ -359,6 +382,8 @@ def entity_method(
|
|
|
359
382
|
_handle_agent_failure_event(str(e), span, tlp_span_kind)
|
|
360
383
|
raise e
|
|
361
384
|
finally:
|
|
385
|
+
if tlp_span_kind == ObserveSpanKindValues.AGENT:
|
|
386
|
+
TracerWrapper().record_agent_execution(entity_name, success)
|
|
362
387
|
_cleanup_span(span, ctx_token)
|
|
363
388
|
return res
|
|
364
389
|
|
|
@@ -370,10 +395,15 @@ def entity_method(
|
|
|
370
395
|
if not TracerWrapper.verify_initialized():
|
|
371
396
|
return fn(*args, **kwargs)
|
|
372
397
|
|
|
373
|
-
span, ctx, ctx_token = _setup_span(
|
|
398
|
+
span, ctx, ctx_token = _setup_span(
|
|
399
|
+
entity_name,
|
|
400
|
+
tlp_span_kind,
|
|
401
|
+
version,
|
|
402
|
+
description if description else None,
|
|
403
|
+
)
|
|
374
404
|
|
|
375
405
|
_handle_span_input(span, args, kwargs, cls=JSONEncoder)
|
|
376
|
-
_handle_agent_span(span, entity_name, tlp_span_kind)
|
|
406
|
+
_handle_agent_span(span, entity_name, description, tlp_span_kind)
|
|
377
407
|
success = False
|
|
378
408
|
|
|
379
409
|
# Record heartbeat for agent
|
|
@@ -459,6 +489,8 @@ def entity_method(
|
|
|
459
489
|
_handle_agent_failure_event(str(e), span, tlp_span_kind)
|
|
460
490
|
raise e
|
|
461
491
|
finally:
|
|
492
|
+
if tlp_span_kind == ObserveSpanKindValues.AGENT:
|
|
493
|
+
TracerWrapper().record_agent_execution(entity_name, success)
|
|
462
494
|
_cleanup_span(span, ctx_token)
|
|
463
495
|
return res
|
|
464
496
|
|
|
@@ -469,26 +501,51 @@ def entity_method(
|
|
|
469
501
|
|
|
470
502
|
def entity_class(
|
|
471
503
|
name: Optional[str],
|
|
504
|
+
description: Optional[str],
|
|
472
505
|
version: Optional[int],
|
|
473
|
-
method_name: str,
|
|
506
|
+
method_name: Optional[str],
|
|
474
507
|
tlp_span_kind: Optional[ObserveSpanKindValues] = ObserveSpanKindValues.TASK,
|
|
475
508
|
):
|
|
476
509
|
def decorator(cls):
|
|
477
510
|
task_name = name if name else camel_to_snake(cls.__qualname__)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
511
|
+
|
|
512
|
+
methods_to_wrap = []
|
|
513
|
+
|
|
514
|
+
if method_name:
|
|
515
|
+
# Specific method specified - existing behavior
|
|
516
|
+
methods_to_wrap = [method_name]
|
|
517
|
+
else:
|
|
518
|
+
# No method specified - wrap all public methods
|
|
519
|
+
for attr_name in dir(cls):
|
|
520
|
+
if (
|
|
521
|
+
not attr_name.startswith("_") # Skip private/built-in methods
|
|
522
|
+
and attr_name != "mro" # Skip class method
|
|
523
|
+
and hasattr(cls, attr_name)
|
|
524
|
+
and callable(getattr(cls, attr_name))
|
|
525
|
+
and not isinstance(
|
|
526
|
+
getattr(cls, attr_name), (classmethod, staticmethod, property)
|
|
527
|
+
)
|
|
528
|
+
):
|
|
529
|
+
methods_to_wrap.append(attr_name)
|
|
530
|
+
|
|
531
|
+
# Wrap all detected methods
|
|
532
|
+
for method_to_wrap in methods_to_wrap:
|
|
533
|
+
if hasattr(cls, method_to_wrap):
|
|
534
|
+
method = getattr(cls, method_to_wrap)
|
|
535
|
+
wrapped_method = entity_method(
|
|
536
|
+
name=f"{task_name}.{method_to_wrap}",
|
|
537
|
+
description=description,
|
|
538
|
+
version=version,
|
|
539
|
+
tlp_span_kind=tlp_span_kind,
|
|
540
|
+
)(method)
|
|
541
|
+
setattr(cls, method_to_wrap, wrapped_method)
|
|
542
|
+
|
|
486
543
|
return cls
|
|
487
544
|
|
|
488
545
|
return decorator
|
|
489
546
|
|
|
490
547
|
|
|
491
|
-
def _handle_agent_span(span, entity_name, tlp_span_kind):
|
|
548
|
+
def _handle_agent_span(span, entity_name, description, tlp_span_kind):
|
|
492
549
|
if tlp_span_kind == ObserveSpanKindValues.AGENT:
|
|
493
550
|
try:
|
|
494
551
|
set_agent_id_event(entity_name)
|
|
@@ -496,6 +553,7 @@ def _handle_agent_span(span, entity_name, tlp_span_kind):
|
|
|
496
553
|
"agent_start_event",
|
|
497
554
|
{
|
|
498
555
|
"agent_name": entity_name,
|
|
556
|
+
"description": description if description else "",
|
|
499
557
|
"type": tlp_span_kind.value
|
|
500
558
|
if tlp_span_kind != "graph"
|
|
501
559
|
else "graph",
|
|
@@ -581,7 +639,8 @@ def _handle_graph_response(span, res, tlp_span_kind):
|
|
|
581
639
|
}
|
|
582
640
|
|
|
583
641
|
# Convert to JSON string
|
|
584
|
-
s_graph_json = json.dumps(graph_dict
|
|
642
|
+
s_graph_json = json.dumps(graph_dict)
|
|
643
|
+
# convert to JSON and set as attribute
|
|
585
644
|
span.set_attribute("gen_ai.ioa.graph", s_graph_json)
|
|
586
645
|
|
|
587
646
|
# get graph dynamism
|
|
@@ -21,6 +21,12 @@ def determine_workflow_type(workflow_obj: Any) -> Union[None, dict]:
|
|
|
21
21
|
for key, value in workflow_obj.items()
|
|
22
22
|
):
|
|
23
23
|
return build_agent_dict_topology(workflow_obj)
|
|
24
|
+
|
|
25
|
+
# Check if it's a list of agent names
|
|
26
|
+
elif isinstance(workflow_obj, list) and all(
|
|
27
|
+
isinstance(item, str) for item in workflow_obj
|
|
28
|
+
):
|
|
29
|
+
return build_agent_dict_topology(workflow_obj)
|
|
24
30
|
# Try LlamaIndex built-in workflow types first
|
|
25
31
|
result = determine_llama_index_workflow_type(workflow_obj)
|
|
26
32
|
if result:
|
|
@@ -42,17 +48,14 @@ def determine_workflow_type(workflow_obj: Any) -> Union[None, dict]:
|
|
|
42
48
|
return None
|
|
43
49
|
|
|
44
50
|
|
|
45
|
-
# This function generates a graph topology dict for the workflow.
|
|
46
51
|
def determine_llama_index_workflow_type(workflow_obj: Any) -> Union[None, dict]:
|
|
47
52
|
"""Generates a graph topology dict for the llama-index compatible workflow."""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
):
|
|
51
|
-
return generate_topology_dict(workflow_obj)
|
|
52
|
-
elif isinstance(
|
|
53
|
-
workflow_obj, AgentWorkflow
|
|
54
|
-
) and "multi_agent_workflow.AgentWorkflow" in str(workflow_obj.__class__.__bases__):
|
|
53
|
+
# Check for AgentWorkflow first (more specific)
|
|
54
|
+
if isinstance(workflow_obj, AgentWorkflow):
|
|
55
55
|
return get_multi_agent_workflow_graph_as_json(workflow_obj)
|
|
56
|
+
# Check for general Workflow (less specific)
|
|
57
|
+
elif isinstance(workflow_obj, Workflow):
|
|
58
|
+
return generate_topology_dict(workflow_obj)
|
|
56
59
|
else:
|
|
57
60
|
return None
|
|
58
61
|
|
|
@@ -171,7 +174,6 @@ def generate_topology_dict(workflow: Workflow) -> Union[None, dict]:
|
|
|
171
174
|
else:
|
|
172
175
|
# No next step? Ignore or log
|
|
173
176
|
pass
|
|
174
|
-
|
|
175
177
|
return {"nodes": nodes, "edges": edges}
|
|
176
178
|
|
|
177
179
|
|
|
@@ -282,8 +284,13 @@ def get_multi_agent_workflow_graph_as_json(agent_workflow_instance):
|
|
|
282
284
|
|
|
283
285
|
|
|
284
286
|
def detect_custom_agent_workflow(obj: Any) -> Union[None, dict]:
|
|
285
|
-
|
|
287
|
+
# Check if object has a workflow attribute that's a LlamaIndex workflow
|
|
288
|
+
if hasattr(obj, "workflow") and isinstance(obj.workflow, (Workflow, AgentWorkflow)):
|
|
289
|
+
return determine_llama_index_workflow_type(obj.workflow)
|
|
290
|
+
|
|
291
|
+
# Detects and generates topology for custom agent workflows not built with LlamaIndex."""
|
|
286
292
|
# Check if this is a dictionary of agents returned from a get_agents method
|
|
293
|
+
|
|
287
294
|
if isinstance(obj, dict) and all(
|
|
288
295
|
hasattr(agent, "invoke") for agent in obj.values()
|
|
289
296
|
):
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Copyright AGNTCY Contributors (https://github.com/agntcy)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from typing import Collection
|
|
5
|
+
import functools
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
from opentelemetry import baggage
|
|
9
|
+
from opentelemetry.baggage.propagation import W3CBaggagePropagator
|
|
10
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
11
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
12
|
+
|
|
13
|
+
from ioa_observe.sdk import TracerWrapper
|
|
14
|
+
from ioa_observe.sdk.client import kv_store
|
|
15
|
+
from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
|
|
16
|
+
|
|
17
|
+
_instruments = ("a2a-sdk >= 0.2.5",)
|
|
18
|
+
_global_tracer = None
|
|
19
|
+
_kv_lock = threading.RLock() # Add thread-safety for kv_store operations
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class A2AInstrumentor(BaseInstrumentor):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__()
|
|
25
|
+
global _global_tracer
|
|
26
|
+
_global_tracer = TracerWrapper().get_tracer()
|
|
27
|
+
|
|
28
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
29
|
+
return _instruments
|
|
30
|
+
|
|
31
|
+
def _instrument(self, **kwargs):
|
|
32
|
+
import importlib
|
|
33
|
+
|
|
34
|
+
if importlib.util.find_spec("a2a") is None:
|
|
35
|
+
raise ImportError("No module named 'a2a-sdk'. Please install it first.")
|
|
36
|
+
|
|
37
|
+
# Instrument `publish`
|
|
38
|
+
from a2a.client import A2AClient
|
|
39
|
+
|
|
40
|
+
original_send_message = A2AClient.send_message
|
|
41
|
+
|
|
42
|
+
@functools.wraps(original_send_message)
|
|
43
|
+
async def instrumented_send_message(
|
|
44
|
+
self, request, *, http_kwargs=None, context=None
|
|
45
|
+
):
|
|
46
|
+
with _global_tracer.start_as_current_span("a2a.send_message"):
|
|
47
|
+
traceparent = get_current_traceparent()
|
|
48
|
+
session_id = None
|
|
49
|
+
if traceparent:
|
|
50
|
+
session_id = kv_store.get(f"execution.{traceparent}")
|
|
51
|
+
if session_id:
|
|
52
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
53
|
+
# Inject headers into http_kwargs
|
|
54
|
+
if http_kwargs is None:
|
|
55
|
+
http_kwargs = {}
|
|
56
|
+
headers = http_kwargs.get("headers", {})
|
|
57
|
+
headers["traceparent"] = traceparent
|
|
58
|
+
if session_id:
|
|
59
|
+
headers["session_id"] = session_id
|
|
60
|
+
baggage.set_baggage(f"execution.{traceparent}", session_id)
|
|
61
|
+
http_kwargs["headers"] = headers
|
|
62
|
+
return await original_send_message(self, request, http_kwargs=http_kwargs)
|
|
63
|
+
|
|
64
|
+
from a2a.client import A2AClient
|
|
65
|
+
|
|
66
|
+
A2AClient.send_message = instrumented_send_message
|
|
67
|
+
|
|
68
|
+
from a2a.server.request_handlers import DefaultRequestHandler
|
|
69
|
+
|
|
70
|
+
original_server_on_message_send = DefaultRequestHandler.on_message_send
|
|
71
|
+
|
|
72
|
+
@functools.wraps(original_server_on_message_send)
|
|
73
|
+
async def instrumented_execute(self, params, context):
|
|
74
|
+
# Extract headers from context (assume context.request.headers)
|
|
75
|
+
|
|
76
|
+
traceparent = context.state.get("headers", {}).get("traceparent")
|
|
77
|
+
session_id = context.state.get("headers", {}).get("session_id")
|
|
78
|
+
carrier = {
|
|
79
|
+
k.lower(): v
|
|
80
|
+
for k, v in context.state.get("headers", {}).items()
|
|
81
|
+
if k.lower() in ["traceparent", "baggage"]
|
|
82
|
+
}
|
|
83
|
+
if carrier and traceparent:
|
|
84
|
+
ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
|
|
85
|
+
ctx = W3CBaggagePropagator().extract(carrier=carrier, context=ctx)
|
|
86
|
+
if session_id and session_id != "None":
|
|
87
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
88
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
89
|
+
return await original_server_on_message_send(self, params, context)
|
|
90
|
+
|
|
91
|
+
from a2a.server.request_handlers import DefaultRequestHandler
|
|
92
|
+
|
|
93
|
+
DefaultRequestHandler.on_message_send = instrumented_execute
|
|
94
|
+
|
|
95
|
+
def _uninstrument(self, **kwargs):
|
|
96
|
+
import importlib
|
|
97
|
+
|
|
98
|
+
if importlib.util.find_spec("a2a") is None:
|
|
99
|
+
raise ImportError("No module named 'a2a-sdk'. Please install it first.")
|
|
100
|
+
|
|
101
|
+
# Uninstrument `send_message`
|
|
102
|
+
from a2a.client import A2AClient
|
|
103
|
+
|
|
104
|
+
A2AClient.send_message = A2AClient.send_message.__wrapped__
|
|
105
|
+
|
|
106
|
+
# Uninstrument `execute`
|
|
107
|
+
from a2a.server.request_handlers import DefaultRequestHandler
|
|
108
|
+
|
|
109
|
+
DefaultRequestHandler.on_message_send = (
|
|
110
|
+
DefaultRequestHandler.on_message_send.__wrapped__
|
|
111
|
+
)
|
|
@@ -14,7 +14,7 @@ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapProp
|
|
|
14
14
|
|
|
15
15
|
from ioa_observe.sdk import TracerWrapper
|
|
16
16
|
from ioa_observe.sdk.client import kv_store
|
|
17
|
-
from ioa_observe.sdk.tracing import
|
|
17
|
+
from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
|
|
18
18
|
|
|
19
19
|
_instruments = ("slim-bindings >= 0.2",)
|
|
20
20
|
_global_tracer = None
|
|
@@ -50,21 +50,21 @@ class SLIMInstrumentor(BaseInstrumentor):
|
|
|
50
50
|
traceparent = get_current_traceparent()
|
|
51
51
|
|
|
52
52
|
# Thread-safe access to kv_store
|
|
53
|
-
|
|
53
|
+
session_id = None
|
|
54
54
|
if traceparent:
|
|
55
55
|
with _kv_lock:
|
|
56
|
-
|
|
57
|
-
if
|
|
58
|
-
kv_store.set(f"execution.{traceparent}",
|
|
56
|
+
session_id = kv_store.get(f"execution.{traceparent}")
|
|
57
|
+
if session_id:
|
|
58
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
59
59
|
# Add tracing context to the message headers
|
|
60
60
|
headers = {
|
|
61
|
-
"
|
|
61
|
+
"session_id": session_id if session_id else None,
|
|
62
62
|
"traceparent": traceparent,
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
# Set baggage context
|
|
66
|
-
if traceparent and
|
|
67
|
-
baggage.set_baggage(f"execution.{traceparent}",
|
|
66
|
+
if traceparent and session_id:
|
|
67
|
+
baggage.set_baggage(f"execution.{traceparent}", session_id)
|
|
68
68
|
|
|
69
69
|
# Process message payload
|
|
70
70
|
if isinstance(message, bytes):
|
|
@@ -120,7 +120,7 @@ class SLIMInstrumentor(BaseInstrumentor):
|
|
|
120
120
|
|
|
121
121
|
# Extract traceparent from headers
|
|
122
122
|
traceparent = headers.get("traceparent")
|
|
123
|
-
|
|
123
|
+
session_id = headers.get("session_id")
|
|
124
124
|
|
|
125
125
|
# First, extract and restore the trace context from headers
|
|
126
126
|
carrier = {}
|
|
@@ -130,27 +130,27 @@ class SLIMInstrumentor(BaseInstrumentor):
|
|
|
130
130
|
if k.lower() == key.lower():
|
|
131
131
|
carrier[key.lower()] = headers[k]
|
|
132
132
|
|
|
133
|
-
# Restore the trace context BEFORE calling
|
|
133
|
+
# Restore the trace context BEFORE calling set_session_id
|
|
134
134
|
if carrier and traceparent:
|
|
135
135
|
ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
|
|
136
136
|
ctx = W3CBaggagePropagator().extract(carrier=carrier, context=ctx)
|
|
137
137
|
|
|
138
138
|
# Now set execution ID with the restored context
|
|
139
|
-
if
|
|
139
|
+
if session_id and session_id != "None":
|
|
140
140
|
# Pass the traceparent explicitly to prevent new context creation
|
|
141
|
-
|
|
141
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
142
142
|
|
|
143
143
|
# Store in kv_store with thread safety
|
|
144
144
|
with _kv_lock:
|
|
145
|
-
kv_store.set(f"execution.{traceparent}",
|
|
145
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
146
146
|
|
|
147
147
|
# Fallback: check stored execution ID if not found in headers
|
|
148
|
-
if traceparent and (not
|
|
148
|
+
if traceparent and (not session_id or session_id == "None"):
|
|
149
149
|
with _kv_lock:
|
|
150
|
-
|
|
151
|
-
if
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
stored_session_id = kv_store.get(f"execution.{traceparent}")
|
|
151
|
+
if stored_session_id:
|
|
152
|
+
session_id = stored_session_id
|
|
153
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
154
154
|
|
|
155
155
|
# Process payload
|
|
156
156
|
payload = message_dict.get("payload", raw_message)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from ioa_observe.sdk.tracing.context_manager import get_tracer
|
|
5
5
|
from ioa_observe.sdk.tracing.tracing import (
|
|
6
6
|
set_workflow_name,
|
|
7
|
-
|
|
7
|
+
set_session_id,
|
|
8
8
|
get_current_traceparent,
|
|
9
9
|
session_start,
|
|
10
10
|
)
|
|
@@ -12,7 +12,7 @@ from ioa_observe.sdk.tracing.tracing import (
|
|
|
12
12
|
__all__ = [
|
|
13
13
|
"get_tracer",
|
|
14
14
|
"set_workflow_name",
|
|
15
|
-
"
|
|
15
|
+
"set_session_id",
|
|
16
16
|
"get_current_traceparent",
|
|
17
17
|
"session_start",
|
|
18
18
|
]
|
|
@@ -4,7 +4,7 @@ from opentelemetry import baggage
|
|
|
4
4
|
|
|
5
5
|
from ioa_observe.sdk import TracerWrapper
|
|
6
6
|
from ioa_observe.sdk.client import kv_store
|
|
7
|
-
from ioa_observe.sdk.tracing import
|
|
7
|
+
from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
|
|
8
8
|
from opentelemetry import context as otel_context
|
|
9
9
|
from opentelemetry.context import attach
|
|
10
10
|
|
|
@@ -21,7 +21,7 @@ After receiving a message, extract headers and call set_context_from_headers(hea
|
|
|
21
21
|
|
|
22
22
|
def get_current_context_headers():
|
|
23
23
|
"""
|
|
24
|
-
Extracts the current trace context, baggage, and
|
|
24
|
+
Extracts the current trace context, baggage, and session_id into headers.
|
|
25
25
|
"""
|
|
26
26
|
_global_tracer = TracerWrapper().get_tracer()
|
|
27
27
|
with _global_tracer.start_as_current_span("get_current_context_headers"):
|
|
@@ -31,32 +31,32 @@ def get_current_context_headers():
|
|
|
31
31
|
TraceContextTextMapPropagator().inject(carrier, context=current_ctx)
|
|
32
32
|
W3CBaggagePropagator().inject(carrier, context=current_ctx)
|
|
33
33
|
traceparent = carrier.get("traceparent")
|
|
34
|
-
|
|
34
|
+
session_id = None
|
|
35
35
|
if traceparent:
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
carrier["
|
|
36
|
+
session_id = kv_store.get(f"execution.{traceparent}")
|
|
37
|
+
if session_id:
|
|
38
|
+
carrier["session_id"] = session_id
|
|
39
39
|
return carrier
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def set_context_from_headers(headers):
|
|
43
43
|
"""
|
|
44
|
-
Restores the trace context, baggage, and
|
|
44
|
+
Restores the trace context, baggage, and session_id from headers.
|
|
45
45
|
"""
|
|
46
46
|
carrierHeaders = {}
|
|
47
47
|
if "traceparentID" in headers:
|
|
48
48
|
carrierHeaders["traceparent"] = headers["traceparentID"]
|
|
49
49
|
if "executionID" in headers:
|
|
50
|
-
carrierHeaders["
|
|
50
|
+
carrierHeaders["session_id"] = headers["executionID"]
|
|
51
51
|
ctx = TraceContextTextMapPropagator().extract(carrier=carrierHeaders)
|
|
52
52
|
ctx = W3CBaggagePropagator().extract(carrier=carrierHeaders, context=ctx)
|
|
53
53
|
attach(ctx)
|
|
54
|
-
# Restore
|
|
54
|
+
# Restore session_id if present
|
|
55
55
|
traceparent = headers.get("traceparentID")
|
|
56
|
-
|
|
57
|
-
if traceparent and
|
|
58
|
-
|
|
59
|
-
kv_store.set(f"execution.{traceparent}",
|
|
56
|
+
session_id = headers.get("executionID")
|
|
57
|
+
if traceparent and session_id and session_id != "None":
|
|
58
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
59
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
60
60
|
return ctx
|
|
61
61
|
|
|
62
62
|
|
|
@@ -68,16 +68,16 @@ def get_baggage_item(key):
|
|
|
68
68
|
return baggage.get_baggage(key)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def
|
|
71
|
+
def get_current_session_id():
|
|
72
72
|
traceparent = get_current_traceparent()
|
|
73
73
|
if traceparent:
|
|
74
74
|
return kv_store.get(f"execution.{traceparent}")
|
|
75
75
|
return None
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def
|
|
78
|
+
def set_session_id_from_headers(headers):
|
|
79
79
|
traceparent = headers.get("traceparent")
|
|
80
|
-
|
|
81
|
-
if traceparent and
|
|
82
|
-
|
|
83
|
-
kv_store.set(f"execution.{traceparent}",
|
|
80
|
+
session_id = headers.get("session_id")
|
|
81
|
+
if traceparent and session_id:
|
|
82
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
83
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
@@ -19,6 +19,7 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
|
19
19
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
20
20
|
OTLPSpanExporter as GRPCExporter,
|
|
21
21
|
)
|
|
22
|
+
from opentelemetry.metrics import Observation
|
|
22
23
|
from opentelemetry.sdk.resources import Resource
|
|
23
24
|
from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
|
|
24
25
|
from opentelemetry.propagators.textmap import TextMapPropagator
|
|
@@ -42,7 +43,6 @@ from ioa_observe.sdk.client import kv_store
|
|
|
42
43
|
|
|
43
44
|
from ioa_observe.sdk.utils.const import (
|
|
44
45
|
OBSERVE_WORKFLOW_NAME,
|
|
45
|
-
OBSERVE_ENTITY_PATH,
|
|
46
46
|
OBSERVE_ASSOCIATION_PROPERTIES,
|
|
47
47
|
OBSERVE_ENTITY_NAME,
|
|
48
48
|
OBSERVE_PROMPT_TEMPLATE_VARIABLES,
|
|
@@ -129,6 +129,7 @@ class TracerWrapper(object):
|
|
|
129
129
|
return obj
|
|
130
130
|
|
|
131
131
|
obj.__image_uploader = image_uploader
|
|
132
|
+
obj._agent_execution_counts = {} # {(agent_name): [success_count, total_count]}
|
|
132
133
|
TracerWrapper.app_name = TracerWrapper.resource_attributes.get(
|
|
133
134
|
"service.name", "observe"
|
|
134
135
|
)
|
|
@@ -227,6 +228,17 @@ class TracerWrapper(object):
|
|
|
227
228
|
obj.error_counter = meter.create_counter(
|
|
228
229
|
"slim.errors", description="Number of SLIM message errors or drops"
|
|
229
230
|
)
|
|
231
|
+
obj.agent_chain_completion_time_histogram = meter.create_histogram(
|
|
232
|
+
name="gen_ai.client.ioa.agent.end_to_end_chain_completion_time",
|
|
233
|
+
description="Records the end-to-end chain completion time for a single agent",
|
|
234
|
+
unit="s",
|
|
235
|
+
)
|
|
236
|
+
obj.agent_execution_success_rate = meter.create_observable_gauge(
|
|
237
|
+
name="gen_ai.client.ioa.agent.execution_success_rate",
|
|
238
|
+
description="Success rate of agent executions",
|
|
239
|
+
unit="1",
|
|
240
|
+
callbacks=[obj._observe_agent_execution_success_rate],
|
|
241
|
+
)
|
|
230
242
|
if propagator:
|
|
231
243
|
set_global_textmap(propagator)
|
|
232
244
|
|
|
@@ -262,13 +274,13 @@ class TracerWrapper(object):
|
|
|
262
274
|
if workflow_name is not None:
|
|
263
275
|
span.set_attribute(OBSERVE_WORKFLOW_NAME, workflow_name)
|
|
264
276
|
|
|
265
|
-
|
|
266
|
-
if
|
|
267
|
-
span.set_attribute("
|
|
277
|
+
session_id = get_value("session.id")
|
|
278
|
+
if session_id is not None:
|
|
279
|
+
span.set_attribute("session.id", session_id)
|
|
268
280
|
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
span.set_attribute(
|
|
281
|
+
agent_id = get_value("agent_id")
|
|
282
|
+
if agent_id is not None:
|
|
283
|
+
span.set_attribute("agent_id", agent_id)
|
|
272
284
|
|
|
273
285
|
if is_llm_span(span):
|
|
274
286
|
self.llm_call_counter.add(1, attributes=span.attributes)
|
|
@@ -327,8 +339,6 @@ class TracerWrapper(object):
|
|
|
327
339
|
|
|
328
340
|
def span_processor_on_ending(self, span):
|
|
329
341
|
determine_reliability_score(span)
|
|
330
|
-
|
|
331
|
-
# self.rename_span_attribute(span)
|
|
332
342
|
start_time = span.attributes.get("ioa_start_time")
|
|
333
343
|
# publish span to the exporter
|
|
334
344
|
|
|
@@ -336,14 +346,6 @@ class TracerWrapper(object):
|
|
|
336
346
|
latency = (time.time() - start_time) * 1000
|
|
337
347
|
self.response_latency_histogram.record(latency, attributes=span.attributes)
|
|
338
348
|
|
|
339
|
-
def rename_span_attribute(self, span):
|
|
340
|
-
# for each span attribute, if the span attribute name contains traceloop, replace it with ioa
|
|
341
|
-
for key in list(span.attributes.keys()):
|
|
342
|
-
if "traceloop" in key:
|
|
343
|
-
new_key = key.replace("traceloop", "ioa")
|
|
344
|
-
span[new_key] = span.attributes[key]
|
|
345
|
-
del span[key]
|
|
346
|
-
|
|
347
349
|
@staticmethod
|
|
348
350
|
def set_static_params(
|
|
349
351
|
resource_attributes: dict,
|
|
@@ -384,6 +386,24 @@ class TracerWrapper(object):
|
|
|
384
386
|
def get_tracer(self):
|
|
385
387
|
return self.__tracer_provider.get_tracer(TRACER_NAME)
|
|
386
388
|
|
|
389
|
+
def record_agent_execution(self, agent_name: str, success: bool):
|
|
390
|
+
counts = self._agent_execution_counts.setdefault(agent_name, [0, 0])
|
|
391
|
+
if success:
|
|
392
|
+
counts[0] += 1 # success count
|
|
393
|
+
counts[1] += 1 # total count
|
|
394
|
+
|
|
395
|
+
def _observe_agent_execution_success_rate(self, observer):
|
|
396
|
+
measurements = []
|
|
397
|
+
for agent_name, (
|
|
398
|
+
success_count,
|
|
399
|
+
total_count,
|
|
400
|
+
) in self._agent_execution_counts.items():
|
|
401
|
+
rate = (success_count / total_count) if total_count > 0 else 0.0
|
|
402
|
+
measurements.append(
|
|
403
|
+
Observation(value=rate, attributes={"agent_name": agent_name})
|
|
404
|
+
)
|
|
405
|
+
return measurements
|
|
406
|
+
|
|
387
407
|
|
|
388
408
|
def set_association_properties(properties: dict) -> None:
|
|
389
409
|
attach(set_value("association_properties", properties))
|
|
@@ -409,10 +429,10 @@ def session_start():
|
|
|
409
429
|
As a context manager, yields session metadata.
|
|
410
430
|
As a normal function, just sets up the session.
|
|
411
431
|
"""
|
|
412
|
-
|
|
413
|
-
|
|
432
|
+
session_id = TracerWrapper.app_name + "_" + str(uuid.uuid4())
|
|
433
|
+
set_session_id(session_id)
|
|
414
434
|
metadata = {
|
|
415
|
-
"executionID": get_value("
|
|
435
|
+
"executionID": get_value("session.id") or session_id,
|
|
416
436
|
"traceparentID": get_current_traceparent(),
|
|
417
437
|
}
|
|
418
438
|
import inspect
|
|
@@ -431,7 +451,7 @@ def session_start():
|
|
|
431
451
|
return contextlib.nullcontext(metadata)
|
|
432
452
|
|
|
433
453
|
|
|
434
|
-
def
|
|
454
|
+
def set_session_id(session_id: str, traceparent: str = None) -> None:
|
|
435
455
|
"""
|
|
436
456
|
Sets the execution ID in both the key-value store and OpenTelemetry context.
|
|
437
457
|
|
|
@@ -439,10 +459,10 @@ def set_execution_id(execution_id: str, traceparent: str = None) -> None:
|
|
|
439
459
|
proper trace correlation across distributed systems.
|
|
440
460
|
|
|
441
461
|
Args:
|
|
442
|
-
|
|
462
|
+
session_id: The execution ID to set
|
|
443
463
|
traceparent: Optional traceparent to use (if None, will extract from context)
|
|
444
464
|
"""
|
|
445
|
-
if not
|
|
465
|
+
if not session_id:
|
|
446
466
|
return
|
|
447
467
|
|
|
448
468
|
from opentelemetry import trace
|
|
@@ -455,10 +475,10 @@ def set_execution_id(execution_id: str, traceparent: str = None) -> None:
|
|
|
455
475
|
# Store execution ID with provided traceparent
|
|
456
476
|
kv_key = f"execution.{traceparent}"
|
|
457
477
|
if kv_store.get(kv_key) is None:
|
|
458
|
-
kv_store.set(kv_key,
|
|
478
|
+
kv_store.set(kv_key, session_id)
|
|
459
479
|
|
|
460
480
|
# Store in OpenTelemetry context
|
|
461
|
-
attach(set_value("
|
|
481
|
+
attach(set_value("session.id", session_id))
|
|
462
482
|
attach(set_value("current_traceparent", traceparent))
|
|
463
483
|
return
|
|
464
484
|
|
|
@@ -473,7 +493,7 @@ def set_execution_id(execution_id: str, traceparent: str = None) -> None:
|
|
|
473
493
|
else:
|
|
474
494
|
# Only create new span if absolutely necessary (no existing context)
|
|
475
495
|
tracer = trace.get_tracer(__name__)
|
|
476
|
-
with tracer.start_as_current_span("
|
|
496
|
+
with tracer.start_as_current_span("set_session_id"):
|
|
477
497
|
carrier = {}
|
|
478
498
|
TraceContextTextMapPropagator().inject(carrier)
|
|
479
499
|
extracted_traceparent = carrier.get("traceparent")
|
|
@@ -482,10 +502,10 @@ def set_execution_id(execution_id: str, traceparent: str = None) -> None:
|
|
|
482
502
|
if extracted_traceparent:
|
|
483
503
|
kv_key = f"execution.{extracted_traceparent}"
|
|
484
504
|
if kv_store.get(kv_key) is None:
|
|
485
|
-
kv_store.set(kv_key,
|
|
505
|
+
kv_store.set(kv_key, session_id)
|
|
486
506
|
|
|
487
507
|
# Also store in OpenTelemetry context
|
|
488
|
-
attach(set_value("
|
|
508
|
+
attach(set_value("session.id", session_id))
|
|
489
509
|
attach(set_value("current_traceparent", extracted_traceparent))
|
|
490
510
|
|
|
491
511
|
|
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ioa-observe-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.8
|
|
4
4
|
Summary: IOA Observability SDK
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE.md
|
|
8
|
-
Requires-Dist: aiohappyeyeballs
|
|
9
|
-
Requires-Dist: aiohttp
|
|
10
|
-
Requires-Dist: aiosignal
|
|
11
|
-
Requires-Dist: annotated-types
|
|
12
|
-
Requires-Dist: anyio
|
|
13
|
-
Requires-Dist: async-timeout
|
|
14
|
-
Requires-Dist: attrs
|
|
15
|
-
Requires-Dist: backoff
|
|
16
|
-
Requires-Dist: certifi
|
|
17
|
-
Requires-Dist: charset-normalizer
|
|
8
|
+
Requires-Dist: aiohappyeyeballs>=2.4.8
|
|
9
|
+
Requires-Dist: aiohttp>=3.11.18
|
|
10
|
+
Requires-Dist: aiosignal>=1.3.2
|
|
11
|
+
Requires-Dist: annotated-types>=0.7.0
|
|
12
|
+
Requires-Dist: anyio>=4.8.0
|
|
13
|
+
Requires-Dist: async-timeout>=4.0.3
|
|
14
|
+
Requires-Dist: attrs>=25.1.0
|
|
15
|
+
Requires-Dist: backoff>=2.2.1
|
|
16
|
+
Requires-Dist: certifi>=2025.1.31
|
|
17
|
+
Requires-Dist: charset-normalizer>=3.4.1
|
|
18
18
|
Requires-Dist: colorama==0.4.6
|
|
19
|
-
Requires-Dist: Deprecated
|
|
20
|
-
Requires-Dist: distro
|
|
21
|
-
Requires-Dist: exceptiongroup
|
|
22
|
-
Requires-Dist: frozenlist
|
|
23
|
-
Requires-Dist: googleapis-common-protos
|
|
24
|
-
Requires-Dist: grpcio
|
|
25
|
-
Requires-Dist: h11
|
|
26
|
-
Requires-Dist: httpcore
|
|
27
|
-
Requires-Dist: httpx
|
|
28
|
-
Requires-Dist: idna
|
|
29
|
-
Requires-Dist: importlib_metadata
|
|
30
|
-
Requires-Dist: Jinja2
|
|
31
|
-
Requires-Dist: jiter
|
|
32
|
-
Requires-Dist: MarkupSafe
|
|
33
|
-
Requires-Dist: monotonic
|
|
34
|
-
Requires-Dist: multidict
|
|
35
|
-
Requires-Dist: openai
|
|
19
|
+
Requires-Dist: Deprecated>=1.2.18
|
|
20
|
+
Requires-Dist: distro>=1.9.0
|
|
21
|
+
Requires-Dist: exceptiongroup>=1.2.2
|
|
22
|
+
Requires-Dist: frozenlist>=1.5.0
|
|
23
|
+
Requires-Dist: googleapis-common-protos>=1.69.0
|
|
24
|
+
Requires-Dist: grpcio>=1.70.0
|
|
25
|
+
Requires-Dist: h11>=0.16.0
|
|
26
|
+
Requires-Dist: httpcore>=1.0.9
|
|
27
|
+
Requires-Dist: httpx>=0.28.1
|
|
28
|
+
Requires-Dist: idna>=3.10
|
|
29
|
+
Requires-Dist: importlib_metadata>=8.5.0
|
|
30
|
+
Requires-Dist: Jinja2>=3.1.6
|
|
31
|
+
Requires-Dist: jiter>=0.8.2
|
|
32
|
+
Requires-Dist: MarkupSafe>=3.0.2
|
|
33
|
+
Requires-Dist: monotonic>=1.6
|
|
34
|
+
Requires-Dist: multidict>=6.1.0
|
|
35
|
+
Requires-Dist: openai>=1.75.0
|
|
36
36
|
Requires-Dist: opentelemetry-api==1.33.1
|
|
37
37
|
Requires-Dist: opentelemetry-distro
|
|
38
38
|
Requires-Dist: opentelemetry-exporter-otlp==1.33.1
|
|
@@ -52,32 +52,32 @@ Requires-Dist: opentelemetry-sdk==1.33.1
|
|
|
52
52
|
Requires-Dist: opentelemetry-semantic-conventions==0.54b1
|
|
53
53
|
Requires-Dist: opentelemetry-semantic-conventions-ai==0.4.9
|
|
54
54
|
Requires-Dist: opentelemetry-util-http==0.54b1
|
|
55
|
-
Requires-Dist: packaging
|
|
56
|
-
Requires-Dist: propcache
|
|
57
|
-
Requires-Dist: protobuf
|
|
58
|
-
Requires-Dist: pydantic
|
|
59
|
-
Requires-Dist: pydantic_core
|
|
60
|
-
Requires-Dist: python-dateutil
|
|
55
|
+
Requires-Dist: packaging>=24.2
|
|
56
|
+
Requires-Dist: propcache>=0.3.0
|
|
57
|
+
Requires-Dist: protobuf>=5.29.3
|
|
58
|
+
Requires-Dist: pydantic>=2.10.6
|
|
59
|
+
Requires-Dist: pydantic_core>=2.27.2
|
|
60
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
61
61
|
Requires-Dist: regex==2024.11.6
|
|
62
|
-
Requires-Dist: requests
|
|
63
|
-
Requires-Dist: six
|
|
64
|
-
Requires-Dist: sniffio
|
|
65
|
-
Requires-Dist: tenacity
|
|
66
|
-
Requires-Dist: tiktoken
|
|
67
|
-
Requires-Dist: tqdm
|
|
68
|
-
Requires-Dist: typing_extensions
|
|
69
|
-
Requires-Dist: urllib3
|
|
70
|
-
Requires-Dist: wrapt
|
|
71
|
-
Requires-Dist: yarl
|
|
72
|
-
Requires-Dist: zipp
|
|
62
|
+
Requires-Dist: requests>=2.32.3
|
|
63
|
+
Requires-Dist: six>=1.17.0
|
|
64
|
+
Requires-Dist: sniffio>=1.3.1
|
|
65
|
+
Requires-Dist: tenacity>=9.0.0
|
|
66
|
+
Requires-Dist: tiktoken>=0.9.0
|
|
67
|
+
Requires-Dist: tqdm>=4.67.1
|
|
68
|
+
Requires-Dist: typing_extensions>=4.12.2
|
|
69
|
+
Requires-Dist: urllib3>=2.3.0
|
|
70
|
+
Requires-Dist: wrapt>=1.17.2
|
|
71
|
+
Requires-Dist: yarl>=1.18.3
|
|
72
|
+
Requires-Dist: zipp>=3.21.0
|
|
73
73
|
Requires-Dist: langgraph>=0.3.2
|
|
74
74
|
Requires-Dist: langchain>=0.3.19
|
|
75
75
|
Requires-Dist: langchain-openai>=0.3.8
|
|
76
76
|
Requires-Dist: langchain-community>=0.3.25
|
|
77
77
|
Requires-Dist: llama-index>=0.12.34
|
|
78
78
|
Requires-Dist: opentelemetry-instrumentation-requests
|
|
79
|
-
Requires-Dist: opentelemetry-instrumentation-transformers>=0.40.
|
|
80
|
-
Requires-Dist: opentelemetry-instrumentation-crewai>=0.40.
|
|
79
|
+
Requires-Dist: opentelemetry-instrumentation-transformers>=0.40.8
|
|
80
|
+
Requires-Dist: opentelemetry-instrumentation-crewai>=0.40.8
|
|
81
81
|
Requires-Dist: llama-index-utils-workflow>=0.3.1
|
|
82
82
|
Requires-Dist: pytest
|
|
83
83
|
Requires-Dist: pytest-vcr
|
|
@@ -85,6 +85,8 @@ Dynamic: license-file
|
|
|
85
85
|
|
|
86
86
|
# Observe-SDK
|
|
87
87
|
|
|
88
|
+
[](https://pypi.org/project/ioa-observe-sdk/)
|
|
89
|
+
|
|
88
90
|
IOA observability SDK for your multi-agentic application.
|
|
89
91
|
|
|
90
92
|
## Table of Contents
|
|
@@ -119,6 +121,20 @@ Link: [AGNTCY Observability Schema](https://github.com/agntcy/observe/blob/main/
|
|
|
119
121
|
|
|
120
122
|
## Dev
|
|
121
123
|
|
|
124
|
+
Any Opentelemetry compatible backend can be used, but for this guide, we will use ClickhouseDB as the backend database.
|
|
125
|
+
|
|
126
|
+
### Opentelemetry collector
|
|
127
|
+
|
|
128
|
+
The OpenTelemetry Collector offers a vendor-agnostic implementation of how to receive, process and export telemetry data. It removes the need to run, operate, and maintain multiple agents/collectors.
|
|
129
|
+
|
|
130
|
+
### Clickhouse DB
|
|
131
|
+
|
|
132
|
+
ClickhouseDB is used as a backend database to store and query the collected telemetry data efficiently, enabling you to analyze and visualize observability information for your multi-agentic applications.
|
|
133
|
+
|
|
134
|
+
### Grafana (optional)
|
|
135
|
+
|
|
136
|
+
Grafana can be used to visualize the telemetry data collected by the OpenTelemetry Collector and stored in ClickhouseDB.
|
|
137
|
+
|
|
122
138
|
To get started with development, start a Clickhouse DB and an OTel collector container locally using docker-compose like so:
|
|
123
139
|
|
|
124
140
|
```
|
|
@@ -126,6 +142,8 @@ cd deploy/
|
|
|
126
142
|
docker compose up -d
|
|
127
143
|
```
|
|
128
144
|
|
|
145
|
+
Running both locally allows you to test, monitor, and debug your observability setup in a development environment before deploying to production.
|
|
146
|
+
|
|
129
147
|
Ensure the contents of `otel-collector.yaml` is correct.
|
|
130
148
|
|
|
131
149
|
Check the logs of the collector to ensure it is running correctly:
|
|
@@ -134,6 +152,18 @@ Check the logs of the collector to ensure it is running correctly:
|
|
|
134
152
|
docker logs -f otel-collector
|
|
135
153
|
```
|
|
136
154
|
|
|
155
|
+
Viewing data in Clickhouse DB can be done using the Clickhouse client. The collector is configured to export telemetry data to Clickhouse.
|
|
156
|
+
|
|
157
|
+
The clickhouse exporter creates various tables in the Clickhouse DB, including `otel_traces`, which is used to store trace data.
|
|
158
|
+
|
|
159
|
+
For more info, refer to the [OpenTelemetry Clickhouse Exporter documentation](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/clickhouseexporter/README.md)
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
docker exec -it clickhouse-server clickhouse-client
|
|
163
|
+
|
|
164
|
+
select * from otel_traces LIMIT 10;
|
|
165
|
+
```
|
|
166
|
+
|
|
137
167
|
Create a `.env` file with the following content:
|
|
138
168
|
|
|
139
169
|
```bash
|
|
@@ -165,6 +195,28 @@ OPENAI_API_KEY=<KEY> make test
|
|
|
165
195
|
For getting started with the SDK, please refer to the [Getting Started](https://github.com/agntcy/observe/blob/main/GETTING-STARTED.md)
|
|
166
196
|
file. It contains detailed instructions on how to set up and use the SDK effectively.
|
|
167
197
|
|
|
198
|
+
### Grafana
|
|
199
|
+
|
|
200
|
+
To configure Grafana to visualize the telemetry data, follow these steps:
|
|
201
|
+
|
|
202
|
+
1. Spin up Grafana locally using Docker:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
docker run -d -p 3000:3000 --name=grafana grafana/grafana
|
|
206
|
+
```
|
|
207
|
+
2. Access Grafana by navigating to `http://localhost:3000` in your web browser.
|
|
208
|
+
- Default username: `admin`
|
|
209
|
+
- Default password: `admin`
|
|
210
|
+
|
|
211
|
+
3. Add a new data source:
|
|
212
|
+
- Choose "ClickHouse" as the data source type.
|
|
213
|
+
- Set the URL to `http://0.0.0.0:8123`.
|
|
214
|
+
- Configure the authentication settings if necessary.
|
|
215
|
+
- Save and test the connection to ensure it works correctly.
|
|
216
|
+
|
|
217
|
+
Refer to the [Grafana ClickHouse plugin documentation](https://grafana.com/grafana/plugins/grafana-clickhouse-datasource/) for more details on configuring ClickHouse as a data source.
|
|
218
|
+
|
|
219
|
+
|
|
168
220
|
## Contributing
|
|
169
221
|
|
|
170
222
|
Contributions are welcome! Please follow these steps to contribute:
|
|
@@ -9,11 +9,12 @@ ioa_observe/sdk/client/http.py,sha256=LdLYSQPFIhKN5BTB-N78jLO7ITl7jGjA0-qpewEIvO
|
|
|
9
9
|
ioa_observe/sdk/config/__init__.py,sha256=8aVNaw0yRNLFPxlf97iOZLlJVcV81ivSDnudH2m1OIo,572
|
|
10
10
|
ioa_observe/sdk/connectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
ioa_observe/sdk/connectors/slim.py,sha256=NwbKEV7d5NIOqmG8zKqtgGigSJl7kf3QJ65z2gxpsY8,8498
|
|
12
|
-
ioa_observe/sdk/decorators/__init__.py,sha256=
|
|
13
|
-
ioa_observe/sdk/decorators/base.py,sha256=
|
|
14
|
-
ioa_observe/sdk/decorators/util.py,sha256=
|
|
12
|
+
ioa_observe/sdk/decorators/__init__.py,sha256=sNc_CRL2jeAS1o9vxxj-kY6PhS8wB4NLYQUdUBHbjs4,2685
|
|
13
|
+
ioa_observe/sdk/decorators/base.py,sha256=4ytlog5Ub02BqmhWO5csXvplxp5k1AzheSDwRb7P2SA,26821
|
|
14
|
+
ioa_observe/sdk/decorators/util.py,sha256=WMkzmwD7Js0g1BbId9_qR4pwhnaIJdW588zVc5dpqdQ,25399
|
|
15
15
|
ioa_observe/sdk/instrumentations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
ioa_observe/sdk/instrumentations/
|
|
16
|
+
ioa_observe/sdk/instrumentations/a2a.py,sha256=ov_9ckkymf_qFXG0iXVWfxlW-3kFcP-knrM_t-Cf72w,4414
|
|
17
|
+
ioa_observe/sdk/instrumentations/slim.py,sha256=w2-1JRB-I05bqH_Y4-98XMOh0x7xgAJWUYUBpe2alXQ,7656
|
|
17
18
|
ioa_observe/sdk/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
19
|
ioa_observe/sdk/logging/logging.py,sha256=HZxW9s8Due7jgiNkdI38cIjv5rC9D-Flta3RQMOnpow,2891
|
|
19
20
|
ioa_observe/sdk/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -26,19 +27,19 @@ ioa_observe/sdk/metrics/agents/heuristics.py,sha256=lcos_mzxXrTMpNWShW9Biy9pXoh8
|
|
|
26
27
|
ioa_observe/sdk/metrics/agents/recovery_tracker.py,sha256=I3bWmA11Ue2Cfxnn7C9r2zvh7Hqa8z6EV8wqQhDLALI,4811
|
|
27
28
|
ioa_observe/sdk/metrics/agents/tool_call_tracker.py,sha256=qmjaaR7rIaQsqCLlDaiYGUeEO3QijhMwyrcpNjvItzI,3169
|
|
28
29
|
ioa_observe/sdk/metrics/agents/tracker.py,sha256=KN3VFPXrgB1f5VF87ppqS4zG2vxn-9oFCG5K1W2IscE,1361
|
|
29
|
-
ioa_observe/sdk/tracing/__init__.py,sha256=
|
|
30
|
+
ioa_observe/sdk/tracing/__init__.py,sha256=jb3_vIAfrChfeaTNnpUKojnn8uUk_ukdFZ1lLru0kRM,433
|
|
30
31
|
ioa_observe/sdk/tracing/content_allow_list.py,sha256=1fAkpIwUQ7vDwCTkIVrqeltWQtrIbYvj8gz6_7P6NrE,945
|
|
31
32
|
ioa_observe/sdk/tracing/context_manager.py,sha256=O0JEXYa9h8anhW78R8KKBuqS0j4by1E1KXxNIMPnLr8,400
|
|
32
|
-
ioa_observe/sdk/tracing/context_utils.py,sha256
|
|
33
|
+
ioa_observe/sdk/tracing/context_utils.py,sha256=-sYS9vPLI87davV9ubneq5xqbV583CC_c0SmOQS1TAs,2933
|
|
33
34
|
ioa_observe/sdk/tracing/manual.py,sha256=KS6WN-zw9vAACzXYmnMoJm9d1fenYMfvzeK1GrGDPDE,1937
|
|
34
|
-
ioa_observe/sdk/tracing/tracing.py,sha256=
|
|
35
|
+
ioa_observe/sdk/tracing/tracing.py,sha256=jU-qeIccoPUFms2uzYUHoBoACzkCqcdEW6PNzi8uz3M,38883
|
|
35
36
|
ioa_observe/sdk/utils/__init__.py,sha256=UPn182U-UblF_XwXaFpx8F-TmQTbm1LYf9y89uSp5Hw,704
|
|
36
37
|
ioa_observe/sdk/utils/const.py,sha256=GwbHakKPjBL4wLqAVkDrSoKB-8p18EUrbaqPuRuV_xg,1099
|
|
37
38
|
ioa_observe/sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
|
|
38
39
|
ioa_observe/sdk/utils/json_encoder.py,sha256=g4NQ0tTqgWssY6I1D7r4zo0G6PiUo61jhofTAw5-jno,639
|
|
39
40
|
ioa_observe/sdk/utils/package_check.py,sha256=1d1MjxhwoEZIx9dumirT2pRsEWgn-m-SI4npDeEalew,576
|
|
40
|
-
ioa_observe_sdk-1.0.
|
|
41
|
-
ioa_observe_sdk-1.0.
|
|
42
|
-
ioa_observe_sdk-1.0.
|
|
43
|
-
ioa_observe_sdk-1.0.
|
|
44
|
-
ioa_observe_sdk-1.0.
|
|
41
|
+
ioa_observe_sdk-1.0.8.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
|
|
42
|
+
ioa_observe_sdk-1.0.8.dist-info/METADATA,sha256=2hYQs5RbHURU_6Eu91BEbM7WTWeJ2I_aFCzLr3JUrIM,7746
|
|
43
|
+
ioa_observe_sdk-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
44
|
+
ioa_observe_sdk-1.0.8.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
|
|
45
|
+
ioa_observe_sdk-1.0.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|