ioa-observe-sdk 1.0.15__py3-none-any.whl → 1.0.17__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/__init__.py +3 -3
- ioa_observe/sdk/client/client.py +3 -3
- ioa_observe/sdk/decorators/__init__.py +2 -2
- ioa_observe/sdk/decorators/base.py +2 -2
- ioa_observe/sdk/instrumentations/a2a.py +84 -31
- ioa_observe/sdk/instrumentations/mcp.py +494 -0
- ioa_observe/sdk/instrumentations/slim.py +376 -128
- ioa_observe/sdk/tracing/tracing.py +214 -9
- ioa_observe/sdk/tracing/transform_span.py +210 -0
- ioa_observe/sdk/utils/const.py +7 -0
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.17.dist-info}/METADATA +3 -1
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.17.dist-info}/RECORD +15 -13
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.17.dist-info}/WHEEL +0 -0
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.17.dist-info}/licenses/LICENSE.md +0 -0
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.17.dist-info}/top_level.txt +0 -0
|
@@ -38,6 +38,10 @@ from opentelemetry.semconv_ai import SpanAttributes
|
|
|
38
38
|
from ioa_observe.sdk import Telemetry
|
|
39
39
|
from ioa_observe.sdk.instruments import Instruments
|
|
40
40
|
from ioa_observe.sdk.tracing.content_allow_list import ContentAllowList
|
|
41
|
+
from ioa_observe.sdk.tracing.transform_span import (
|
|
42
|
+
transform_json_object_configurable,
|
|
43
|
+
validate_transformer_rules,
|
|
44
|
+
)
|
|
41
45
|
from ioa_observe.sdk.utils import is_notebook
|
|
42
46
|
from ioa_observe.sdk.client import kv_store
|
|
43
47
|
|
|
@@ -106,8 +110,8 @@ def determine_reliability_score(span):
|
|
|
106
110
|
class TracerWrapper(object):
|
|
107
111
|
resource_attributes: dict = {}
|
|
108
112
|
enable_content_tracing: bool = True
|
|
109
|
-
endpoint: str =
|
|
110
|
-
app_name: str =
|
|
113
|
+
endpoint: str = None
|
|
114
|
+
app_name: str = None
|
|
111
115
|
headers: Dict[str, str] = {}
|
|
112
116
|
__tracer_provider: TracerProvider = None
|
|
113
117
|
__disabled: bool = False
|
|
@@ -129,7 +133,10 @@ class TracerWrapper(object):
|
|
|
129
133
|
return obj
|
|
130
134
|
|
|
131
135
|
obj.__image_uploader = image_uploader
|
|
132
|
-
|
|
136
|
+
# {(agent_name): [success_count, total_count]}
|
|
137
|
+
obj._agent_execution_counts = {}
|
|
138
|
+
# Track spans that have been processed to avoid duplicates
|
|
139
|
+
obj._processed_spans = set()
|
|
133
140
|
TracerWrapper.app_name = TracerWrapper.resource_attributes.get(
|
|
134
141
|
"service.name", "observe"
|
|
135
142
|
)
|
|
@@ -139,6 +146,7 @@ class TracerWrapper(object):
|
|
|
139
146
|
Telemetry().capture("tracer:init", {"processor": "custom"})
|
|
140
147
|
obj.__spans_processor: SpanProcessor = processor
|
|
141
148
|
obj.__spans_processor_original_on_start = processor.on_start
|
|
149
|
+
obj.__spans_processor_original_on_end = processor.on_end
|
|
142
150
|
else:
|
|
143
151
|
if exporter:
|
|
144
152
|
Telemetry().capture(
|
|
@@ -175,9 +183,10 @@ class TracerWrapper(object):
|
|
|
175
183
|
schedule_delay_millis=5000,
|
|
176
184
|
)
|
|
177
185
|
obj.__spans_processor_original_on_start = None
|
|
186
|
+
obj.__spans_processor_original_on_end = obj.__spans_processor.on_end
|
|
178
187
|
|
|
179
188
|
obj.__spans_processor.on_start = obj._span_processor_on_start
|
|
180
|
-
|
|
189
|
+
obj.__spans_processor.on_end = obj.span_processor_on_ending
|
|
181
190
|
obj.__tracer_provider.add_span_processor(obj.__spans_processor)
|
|
182
191
|
# Custom metric, for example
|
|
183
192
|
meter = get_meter("observe")
|
|
@@ -338,14 +347,114 @@ class TracerWrapper(object):
|
|
|
338
347
|
self.number_active_agents.add(count, attributes=span.attributes)
|
|
339
348
|
|
|
340
349
|
def span_processor_on_ending(self, span):
|
|
350
|
+
# Check if this span has already been processed to avoid duplicate processing
|
|
351
|
+
# Added for avoid duplicate on_ending with manual triggers
|
|
352
|
+
# from decorators (@tool, @workflow) in base.py
|
|
353
|
+
span_id = span.context.span_id
|
|
354
|
+
if span_id in self._processed_spans:
|
|
355
|
+
# This span was already processed, skip to avoid duplicates
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
# Mark this span as processed
|
|
359
|
+
self._processed_spans.add(span_id)
|
|
360
|
+
|
|
341
361
|
determine_reliability_score(span)
|
|
342
362
|
start_time = span.attributes.get("ioa_start_time")
|
|
343
|
-
|
|
363
|
+
|
|
364
|
+
# Apply transformations if enabled
|
|
365
|
+
apply_transform = get_value("apply_transform")
|
|
366
|
+
if apply_transform:
|
|
367
|
+
transformer_rules = get_value("transformer_rules")
|
|
368
|
+
if transformer_rules:
|
|
369
|
+
try:
|
|
370
|
+
# Convert span to dict for transformation
|
|
371
|
+
span_dict = self._span_to_dict(span)
|
|
372
|
+
# Apply transformation
|
|
373
|
+
transformed_span_dict = transform_json_object_configurable(
|
|
374
|
+
span_dict, transformer_rules
|
|
375
|
+
)
|
|
376
|
+
# Update span with transformed data
|
|
377
|
+
self._update_span_from_dict(span, transformed_span_dict)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logging.error(f"Error applying span transformation: {e}")
|
|
344
380
|
|
|
345
381
|
if start_time is not None:
|
|
346
382
|
latency = (time.time() - start_time) * 1000
|
|
347
383
|
self.response_latency_histogram.record(latency, attributes=span.attributes)
|
|
348
384
|
|
|
385
|
+
# Call original on_end method if it exists
|
|
386
|
+
if (
|
|
387
|
+
hasattr(self, "_TracerWrapper__spans_processor_original_on_end")
|
|
388
|
+
and self.__spans_processor_original_on_end
|
|
389
|
+
):
|
|
390
|
+
self.__spans_processor_original_on_end(span)
|
|
391
|
+
|
|
392
|
+
def _span_to_dict(self, span):
|
|
393
|
+
"""Convert span to dictionary for transformation."""
|
|
394
|
+
span_dict = {
|
|
395
|
+
"name": span.name,
|
|
396
|
+
"attributes": dict(span.attributes) if span.attributes else {},
|
|
397
|
+
"status": {
|
|
398
|
+
"status_code": span.status.status_code.name
|
|
399
|
+
if span.status and span.status.status_code
|
|
400
|
+
else None,
|
|
401
|
+
"description": span.status.description if span.status else None,
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
return span_dict
|
|
405
|
+
|
|
406
|
+
def _update_span_from_dict(self, span, span_dict):
|
|
407
|
+
"""Update span with transformed data."""
|
|
408
|
+
# Update span name if it was transformed
|
|
409
|
+
if "name" in span_dict and span_dict["name"] != span.name:
|
|
410
|
+
# Directly modify the internal name attribute
|
|
411
|
+
if hasattr(span, "_name"):
|
|
412
|
+
span._name = span_dict["name"]
|
|
413
|
+
|
|
414
|
+
# Update attributes if they were transformed
|
|
415
|
+
if "attributes" in span_dict:
|
|
416
|
+
# Try multiple approaches to update span attributes
|
|
417
|
+
updated = False
|
|
418
|
+
|
|
419
|
+
# Method 1: Try using set_attribute if available and mutable
|
|
420
|
+
if (
|
|
421
|
+
hasattr(span, "set_attribute")
|
|
422
|
+
and hasattr(span, "_ended")
|
|
423
|
+
and not span._ended
|
|
424
|
+
):
|
|
425
|
+
try:
|
|
426
|
+
# Clear existing attributes by setting them to None
|
|
427
|
+
if hasattr(span, "_attributes"):
|
|
428
|
+
keys_to_remove = list(span._attributes.keys())
|
|
429
|
+
for key in keys_to_remove:
|
|
430
|
+
span.set_attribute(key, None)
|
|
431
|
+
|
|
432
|
+
# Set new attributes
|
|
433
|
+
for key, value in span_dict["attributes"].items():
|
|
434
|
+
span.set_attribute(key, value)
|
|
435
|
+
updated = True
|
|
436
|
+
except (AttributeError, TypeError):
|
|
437
|
+
pass
|
|
438
|
+
|
|
439
|
+
# Method 2: Direct attribute manipulation
|
|
440
|
+
if not updated:
|
|
441
|
+
try:
|
|
442
|
+
if hasattr(span, "_attributes"):
|
|
443
|
+
span._attributes.clear()
|
|
444
|
+
span._attributes.update(span_dict["attributes"])
|
|
445
|
+
updated = True
|
|
446
|
+
elif hasattr(span, "attributes") and hasattr(
|
|
447
|
+
span.attributes, "clear"
|
|
448
|
+
):
|
|
449
|
+
span.attributes.clear()
|
|
450
|
+
span.attributes.update(span_dict["attributes"])
|
|
451
|
+
updated = True
|
|
452
|
+
except (AttributeError, TypeError):
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
if not updated:
|
|
456
|
+
logging.warning("Cannot modify span attributes - span may be finalized")
|
|
457
|
+
|
|
349
458
|
@staticmethod
|
|
350
459
|
def set_static_params(
|
|
351
460
|
resource_attributes: dict,
|
|
@@ -423,14 +532,101 @@ def set_workflow_name(workflow_name: str) -> None:
|
|
|
423
532
|
attach(set_value("workflow_name", workflow_name))
|
|
424
533
|
|
|
425
534
|
|
|
426
|
-
def
|
|
535
|
+
def _parse_boolean_env(env_value: str) -> bool:
|
|
536
|
+
"""
|
|
537
|
+
Parse boolean value from environment variable string.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
env_value (str): Environment variable value to parse
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
bool: Parsed boolean value
|
|
544
|
+
|
|
545
|
+
Accepts: "0", "1", "true", "false", "True", "False"
|
|
546
|
+
"""
|
|
547
|
+
if env_value.lower() in ("true", "1"):
|
|
548
|
+
return True
|
|
549
|
+
elif env_value.lower() in ("false", "0"):
|
|
550
|
+
return False
|
|
551
|
+
else:
|
|
552
|
+
raise ValueError(f"Invalid boolean value: {env_value}")
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def session_start(apply_transform: bool = False):
|
|
427
556
|
"""
|
|
428
557
|
Can be used as a context manager or a normal function.
|
|
429
558
|
As a context manager, yields session metadata.
|
|
430
559
|
As a normal function, just sets up the session.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
apply_transform (bool): If True, enables span transformation based on
|
|
563
|
+
rules loaded from SPAN_TRANSFORMER_RULES_FILE env.
|
|
564
|
+
Can be overridden by
|
|
565
|
+
SPAN_TRANSFORMER_RULES_ENABLED env var.
|
|
431
566
|
"""
|
|
432
|
-
session_id = TracerWrapper.app_name + "_" + str(uuid.uuid4())
|
|
567
|
+
session_id = (TracerWrapper.app_name or "observe") + "_" + str(uuid.uuid4())
|
|
433
568
|
set_session_id(session_id)
|
|
569
|
+
|
|
570
|
+
# Check if environment variable overrides the apply_transform parameter
|
|
571
|
+
transformer_enabled_env = os.getenv("SPAN_TRANSFORMER_RULES_ENABLED")
|
|
572
|
+
if transformer_enabled_env:
|
|
573
|
+
try:
|
|
574
|
+
apply_transform = _parse_boolean_env(transformer_enabled_env)
|
|
575
|
+
except ValueError as e:
|
|
576
|
+
logging.error(
|
|
577
|
+
"Invalid SPAN_TRANSFORMER_RULES_ENABLED value: "
|
|
578
|
+
f"{e}. Using parameter value: {apply_transform}"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# Handle transformation flag
|
|
582
|
+
if apply_transform:
|
|
583
|
+
transformer_rules_file = os.getenv("SPAN_TRANSFORMER_RULES_FILE")
|
|
584
|
+
if not transformer_rules_file:
|
|
585
|
+
logging.error(
|
|
586
|
+
"SPAN_TRANSFORMER_RULES_FILE environment variable "
|
|
587
|
+
"not set. Disabling transformation."
|
|
588
|
+
)
|
|
589
|
+
apply_transform = False
|
|
590
|
+
elif not os.path.exists(transformer_rules_file):
|
|
591
|
+
logging.error(
|
|
592
|
+
"Transformer rules file not found: "
|
|
593
|
+
f"{transformer_rules_file}. Disabling "
|
|
594
|
+
"transformation."
|
|
595
|
+
)
|
|
596
|
+
apply_transform = False
|
|
597
|
+
else:
|
|
598
|
+
try:
|
|
599
|
+
with open(transformer_rules_file, "r") as f:
|
|
600
|
+
transformer_rules = json.load(f)
|
|
601
|
+
# Validate structure and rules
|
|
602
|
+
validate_transformer_rules(transformer_rules)
|
|
603
|
+
attach(set_value("apply_transform", True))
|
|
604
|
+
attach(set_value("transformer_rules", transformer_rules))
|
|
605
|
+
except json.JSONDecodeError as e:
|
|
606
|
+
logging.error(
|
|
607
|
+
"Failed to load transformer rules from "
|
|
608
|
+
f"{transformer_rules_file}: {e}. Disabling "
|
|
609
|
+
"transformation."
|
|
610
|
+
)
|
|
611
|
+
apply_transform = False
|
|
612
|
+
except ValueError:
|
|
613
|
+
logging.error(
|
|
614
|
+
"Invalid transformer rules structure. "
|
|
615
|
+
"Expected 'RULES' section. "
|
|
616
|
+
"Disabling transformation."
|
|
617
|
+
)
|
|
618
|
+
apply_transform = False
|
|
619
|
+
except (json.JSONDecodeError, Exception) as e:
|
|
620
|
+
logging.error(
|
|
621
|
+
"Failed to load transformer rules from "
|
|
622
|
+
f"{transformer_rules_file}: {e}. "
|
|
623
|
+
"Disabling transformation."
|
|
624
|
+
)
|
|
625
|
+
apply_transform = False
|
|
626
|
+
|
|
627
|
+
if not apply_transform:
|
|
628
|
+
attach(set_value("apply_transform", False))
|
|
629
|
+
|
|
434
630
|
metadata = {
|
|
435
631
|
"executionID": get_value("session.id") or session_id,
|
|
436
632
|
"traceparentID": get_current_traceparent(),
|
|
@@ -581,14 +777,23 @@ def is_llm_span(span) -> bool:
|
|
|
581
777
|
|
|
582
778
|
|
|
583
779
|
def init_spans_exporter(api_endpoint: str, headers: Dict[str, str]) -> SpanExporter:
|
|
584
|
-
if
|
|
780
|
+
if api_endpoint and (
|
|
781
|
+
"http" in api_endpoint.lower() or "https" in api_endpoint.lower()
|
|
782
|
+
):
|
|
585
783
|
return HTTPExporter(
|
|
586
784
|
endpoint=f"{api_endpoint}/v1/traces",
|
|
587
785
|
headers=headers,
|
|
588
786
|
compression=Compression.Gzip,
|
|
589
787
|
)
|
|
590
|
-
|
|
788
|
+
elif api_endpoint:
|
|
591
789
|
return GRPCExporter(endpoint=f"{api_endpoint}", headers=headers)
|
|
790
|
+
else:
|
|
791
|
+
# Default to HTTP exporter with localhost when endpoint is None
|
|
792
|
+
return HTTPExporter(
|
|
793
|
+
endpoint="http://localhost:4318/v1/traces",
|
|
794
|
+
headers=headers,
|
|
795
|
+
compression=Compression.Gzip,
|
|
796
|
+
)
|
|
592
797
|
|
|
593
798
|
|
|
594
799
|
def init_tracer_provider(resource: Resource) -> TracerProvider:
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Copyright AGNTCY Contributors (https://github.com/agntcy)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def validate_transformer_rules(config):
|
|
6
|
+
"""
|
|
7
|
+
Validates the transformer rules configuration.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
config (dict): The configuration dictionary to validate.
|
|
11
|
+
|
|
12
|
+
Raises:
|
|
13
|
+
ValueError: If the configuration is invalid.
|
|
14
|
+
|
|
15
|
+
Validation rules:
|
|
16
|
+
- Must have RULES section containing a list of transformation rules
|
|
17
|
+
- Each rule must have a 'path' field (list of strings)
|
|
18
|
+
- Path length = 1: Global transformation (e.g., ["old_key"])
|
|
19
|
+
- Path length > 1: Path-specific transformation
|
|
20
|
+
(e.g., ["attributes", "nested_key"])
|
|
21
|
+
- action_conflict must be one of: SKIP, REPLACE, DELETE
|
|
22
|
+
- DELETE action should not have a 'rename' field
|
|
23
|
+
"""
|
|
24
|
+
if not isinstance(config, dict):
|
|
25
|
+
raise ValueError("Configuration must be a dictionary")
|
|
26
|
+
|
|
27
|
+
if "RULES" not in config:
|
|
28
|
+
raise ValueError("Configuration must contain 'RULES' section")
|
|
29
|
+
|
|
30
|
+
rules_section = config.get("RULES", [])
|
|
31
|
+
if not isinstance(rules_section, list):
|
|
32
|
+
raise ValueError("RULES section must be a list")
|
|
33
|
+
|
|
34
|
+
for i, rule in enumerate(rules_section):
|
|
35
|
+
if not isinstance(rule, dict):
|
|
36
|
+
raise ValueError(f"Rule {i} must be a dictionary")
|
|
37
|
+
|
|
38
|
+
# Check required fields
|
|
39
|
+
if "path" not in rule:
|
|
40
|
+
raise ValueError(f"Rule {i} must have 'path' field")
|
|
41
|
+
|
|
42
|
+
if not isinstance(rule["path"], list):
|
|
43
|
+
raise ValueError(f"Rule {i} 'path' must be a list")
|
|
44
|
+
|
|
45
|
+
if not rule["path"]:
|
|
46
|
+
raise ValueError(f"Rule {i} 'path' cannot be empty")
|
|
47
|
+
|
|
48
|
+
# Check action_conflict
|
|
49
|
+
action_conflict = rule.get("action_conflict", "REPLACE")
|
|
50
|
+
if action_conflict not in ["SKIP", "REPLACE", "DELETE"]:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Rule {i} action_conflict must be one of: SKIP, REPLACE, DELETE"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Check rename field for non-DELETE actions
|
|
56
|
+
if action_conflict != "DELETE" and "rename" not in rule:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Rule {i} with {action_conflict} action must have 'rename' field"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Check rename field for DELETE action
|
|
62
|
+
if action_conflict == "DELETE" and "rename" in rule:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Rule {i} with DELETE action should not have 'rename' field"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def transform_json_object_configurable(data, config, current_path=()):
|
|
69
|
+
"""
|
|
70
|
+
Recursively transforms a JSON object (dict or list) based on a unified
|
|
71
|
+
configuration that contains transformation rules for both global and
|
|
72
|
+
path-specific key renames, along with conflict resolution strategies.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
data (dict or list or primitive): The JSON object or part of it
|
|
76
|
+
to transform.
|
|
77
|
+
config (dict): A dictionary containing transformation rules:
|
|
78
|
+
{
|
|
79
|
+
"RULES": [
|
|
80
|
+
{
|
|
81
|
+
"path": ["old_key"],
|
|
82
|
+
"rename": "new_key",
|
|
83
|
+
"action_conflict": "SKIP"|"REPLACE"|"DELETE"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"path": ["attributes",
|
|
87
|
+
"traceloop.span.kind"],
|
|
88
|
+
"rename": "ioa_observe.span_kind",
|
|
89
|
+
"action_conflict": "REPLACE"
|
|
90
|
+
},
|
|
91
|
+
...
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
current_path (tuple): The current path (sequence of keys) from the
|
|
95
|
+
root to the current data element.
|
|
96
|
+
Used for path-specific lookups.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
dict or list or primitive: The transformed JSON object.
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(data, dict):
|
|
102
|
+
new_data = {}
|
|
103
|
+
for key, value in data.items():
|
|
104
|
+
full_key_path = current_path + (key,)
|
|
105
|
+
|
|
106
|
+
# Recursively transform the value first
|
|
107
|
+
transformed_value = transform_json_object_configurable(
|
|
108
|
+
value, config, full_key_path
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Default to no rename and 'REPLACE' conflict strategy
|
|
112
|
+
target_new_key = key
|
|
113
|
+
action_on_conflict = "REPLACE"
|
|
114
|
+
rule_applied = False
|
|
115
|
+
|
|
116
|
+
# Check for matching rules based on path
|
|
117
|
+
rules = config.get("RULES", [])
|
|
118
|
+
for rule in rules:
|
|
119
|
+
rule_path = tuple(rule["path"])
|
|
120
|
+
|
|
121
|
+
# Check if this rule applies to the current path
|
|
122
|
+
if len(rule_path) == 1:
|
|
123
|
+
# Global rule: applies if the key matches
|
|
124
|
+
if rule_path[0] == key:
|
|
125
|
+
action_on_conflict = rule.get("action_conflict", "REPLACE")
|
|
126
|
+
|
|
127
|
+
if action_on_conflict == "DELETE":
|
|
128
|
+
# DELETE action: skip adding this key entirely
|
|
129
|
+
rule_applied = True
|
|
130
|
+
break
|
|
131
|
+
elif "rename" in rule:
|
|
132
|
+
target_new_key = rule["rename"]
|
|
133
|
+
rule_applied = True
|
|
134
|
+
break
|
|
135
|
+
else:
|
|
136
|
+
# Path-specific rule: applies if full path matches
|
|
137
|
+
if full_key_path == rule_path:
|
|
138
|
+
action_on_conflict = rule.get("action_conflict", "REPLACE")
|
|
139
|
+
|
|
140
|
+
if action_on_conflict == "DELETE":
|
|
141
|
+
# DELETE action: skip adding this key entirely
|
|
142
|
+
rule_applied = True
|
|
143
|
+
break
|
|
144
|
+
elif "rename" in rule:
|
|
145
|
+
target_new_key = rule["rename"]
|
|
146
|
+
rule_applied = True
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
# Skip to next iteration if DELETE action was applied
|
|
150
|
+
if rule_applied and action_on_conflict == "DELETE":
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Now, decide which key to use in new_data based on rules and
|
|
154
|
+
# conflict strategy
|
|
155
|
+
if rule_applied and target_new_key != key:
|
|
156
|
+
# A rename was proposed
|
|
157
|
+
if action_on_conflict == "SKIP" and (
|
|
158
|
+
target_new_key in data or target_new_key in new_data
|
|
159
|
+
):
|
|
160
|
+
# If target key already exists in original data OR new_data
|
|
161
|
+
# AND action is SKIP, keep the original key and its
|
|
162
|
+
# transformed value. The rename is effectively "skipped".
|
|
163
|
+
new_data[key] = transformed_value
|
|
164
|
+
else:
|
|
165
|
+
# If action is REPLACE, or target key doesn't exist,
|
|
166
|
+
# then perform the rename.
|
|
167
|
+
# This will either add a new key or overwrite an
|
|
168
|
+
# existing one.
|
|
169
|
+
new_data[target_new_key] = transformed_value
|
|
170
|
+
else:
|
|
171
|
+
# No rule applied or DELETE handled above
|
|
172
|
+
new_data[key] = transformed_value
|
|
173
|
+
|
|
174
|
+
return new_data
|
|
175
|
+
elif isinstance(data, list):
|
|
176
|
+
new_list = []
|
|
177
|
+
for item in data:
|
|
178
|
+
# For list items, the path context usually doesn't change
|
|
179
|
+
# for the elements themselves
|
|
180
|
+
new_list.append(
|
|
181
|
+
transform_json_object_configurable(item, config, current_path)
|
|
182
|
+
)
|
|
183
|
+
return new_list
|
|
184
|
+
else:
|
|
185
|
+
# Base case: primitive types (str, int, float, bool, None)
|
|
186
|
+
# are returned as is
|
|
187
|
+
return data
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def transform_list_of_json_objects_configurable(json_objects_list, config):
|
|
191
|
+
"""
|
|
192
|
+
Transforms a list of JSON objects by applying key replacements based on
|
|
193
|
+
the provided unified configuration.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
json_objects_list (list): A list of Python dictionary objects
|
|
197
|
+
(parsed JSON).
|
|
198
|
+
config (dict): The unified configuration dictionary for
|
|
199
|
+
transformations.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
list: A new list containing the transformed JSON objects.
|
|
203
|
+
"""
|
|
204
|
+
if not isinstance(json_objects_list, list):
|
|
205
|
+
raise TypeError("Input must be a list of JSON objects.")
|
|
206
|
+
|
|
207
|
+
transformed_list = [
|
|
208
|
+
transform_json_object_configurable(obj, config) for obj in json_objects_list
|
|
209
|
+
]
|
|
210
|
+
return transformed_list
|
ioa_observe/sdk/utils/const.py
CHANGED
|
@@ -21,6 +21,13 @@ OBSERVE_PROMPT_VERSION_HASH = "ioa_observe.prompt.version_hash"
|
|
|
21
21
|
OBSERVE_PROMPT_TEMPLATE = "ioa_observe.prompt.template"
|
|
22
22
|
OBSERVE_PROMPT_TEMPLATE_VARIABLES = "ioa_observe.prompt.template_variables"
|
|
23
23
|
|
|
24
|
+
# MCP
|
|
25
|
+
MCP_METHOD_NAME = "mcp.method.name"
|
|
26
|
+
MCP_REQUEST_ARGUMENT = "mcp.request.argument"
|
|
27
|
+
MCP_REQUEST_ID = "mcp.request.id"
|
|
28
|
+
MCP_SESSION_INIT_OPTIONS = "mcp.session.init_options"
|
|
29
|
+
MCP_RESPONSE_VALUE = "mcp.response.value"
|
|
30
|
+
|
|
24
31
|
|
|
25
32
|
class ObserveSpanKindValues(Enum):
|
|
26
33
|
WORKFLOW = "workflow"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ioa-observe-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.17
|
|
4
4
|
Summary: IOA Observability SDK
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -82,6 +82,8 @@ This schema is designed to provide comprehensive observability for Multi-Agent S
|
|
|
82
82
|
|
|
83
83
|
Link: [AGNTCY Observability Schema](https://github.com/agntcy/observe/blob/main/schema/)
|
|
84
84
|
|
|
85
|
+
An option is made available for transforming spans attributes exported by using options via env variables (SPAN_TRANSFORMER_RULES_ENABLED, SPAN_TRANSFORMER_RULES_FILE). Please read [transform](./sdk/tracing/transform_span.py).
|
|
86
|
+
|
|
85
87
|
## Dev
|
|
86
88
|
|
|
87
89
|
Any Opentelemetry compatible backend can be used, but for this guide, we will use ClickhouseDB as the backend database.
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
ioa_observe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
ioa_observe/sdk/__init__.py,sha256=
|
|
2
|
+
ioa_observe/sdk/__init__.py,sha256=GgEjiEhjqvbWh37FCrH1LQb-NROudJCZS4qa9j0Tyic,8845
|
|
3
3
|
ioa_observe/sdk/instruments.py,sha256=cA5Yq1BYFovMrYUNYQXua-JXsMtMOa_YOn6yiJZNwLg,576
|
|
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=
|
|
7
|
+
ioa_observe/sdk/client/client.py,sha256=6TVOo_E1ulE3WO_CYG7oPgeucs-qegOA09uTO3yQiyk,2112
|
|
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
|
|
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=
|
|
12
|
+
ioa_observe/sdk/decorators/__init__.py,sha256=GUZs_HA57bQTLSgo7GAnaofAapk2Y-NuE6md0HPSH3s,3603
|
|
13
|
+
ioa_observe/sdk/decorators/base.py,sha256=FLICXVOFzv6yuCQL99G5Do5h1WrKJNsegvfm5KHe9v8,30173
|
|
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=
|
|
18
|
-
ioa_observe/sdk/instrumentations/
|
|
17
|
+
ioa_observe/sdk/instrumentations/a2a.py,sha256=ZpqvPl4u-yheQzSdBfxnZhWFZ8ntbKni_uaW3IDyjqw,6309
|
|
18
|
+
ioa_observe/sdk/instrumentations/mcp.py,sha256=vRM3ofnn7AMmry2RrfyZnZVPEutLWiDMghx2TSnm0Wk,18569
|
|
19
|
+
ioa_observe/sdk/instrumentations/slim.py,sha256=9KS9slZKB7-oC-2SH2s2FKB2JHXXHt4_xPhLvxJD-fk,22456
|
|
19
20
|
ioa_observe/sdk/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
21
|
ioa_observe/sdk/logging/logging.py,sha256=HZxW9s8Due7jgiNkdI38cIjv5rC9D-Flta3RQMOnpow,2891
|
|
21
22
|
ioa_observe/sdk/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -33,14 +34,15 @@ ioa_observe/sdk/tracing/content_allow_list.py,sha256=1fAkpIwUQ7vDwCTkIVrqeltWQtr
|
|
|
33
34
|
ioa_observe/sdk/tracing/context_manager.py,sha256=O0JEXYa9h8anhW78R8KKBuqS0j4by1E1KXxNIMPnLr8,400
|
|
34
35
|
ioa_observe/sdk/tracing/context_utils.py,sha256=-sYS9vPLI87davV9ubneq5xqbV583CC_c0SmOQS1TAs,2933
|
|
35
36
|
ioa_observe/sdk/tracing/manual.py,sha256=KS6WN-zw9vAACzXYmnMoJm9d1fenYMfvzeK1GrGDPDE,1937
|
|
36
|
-
ioa_observe/sdk/tracing/tracing.py,sha256=
|
|
37
|
+
ioa_observe/sdk/tracing/tracing.py,sha256=lwmatGwviSrf-hlBxRyxa0kv9AQdP302R3duBgNKsSU,47127
|
|
38
|
+
ioa_observe/sdk/tracing/transform_span.py,sha256=XTApi_gJxum7ynvhtcoCfDyK8VVOj91Q1DT6hAeLHA8,8419
|
|
37
39
|
ioa_observe/sdk/utils/__init__.py,sha256=UPn182U-UblF_XwXaFpx8F-TmQTbm1LYf9y89uSp5Hw,704
|
|
38
|
-
ioa_observe/sdk/utils/const.py,sha256=
|
|
40
|
+
ioa_observe/sdk/utils/const.py,sha256=d67dUTAH9UpWvUV9GLBUqn1Sc2knJ55dy-e6YoLrvSo,1318
|
|
39
41
|
ioa_observe/sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
|
|
40
42
|
ioa_observe/sdk/utils/json_encoder.py,sha256=g4NQ0tTqgWssY6I1D7r4zo0G6PiUo61jhofTAw5-jno,639
|
|
41
43
|
ioa_observe/sdk/utils/package_check.py,sha256=1d1MjxhwoEZIx9dumirT2pRsEWgn-m-SI4npDeEalew,576
|
|
42
|
-
ioa_observe_sdk-1.0.
|
|
43
|
-
ioa_observe_sdk-1.0.
|
|
44
|
-
ioa_observe_sdk-1.0.
|
|
45
|
-
ioa_observe_sdk-1.0.
|
|
46
|
-
ioa_observe_sdk-1.0.
|
|
44
|
+
ioa_observe_sdk-1.0.17.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
|
|
45
|
+
ioa_observe_sdk-1.0.17.dist-info/METADATA,sha256=jarZLIoRaEz4yKIwUHHKmAwL2e7hkKe_uhTTT6iwyiw,7027
|
|
46
|
+
ioa_observe_sdk-1.0.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
47
|
+
ioa_observe_sdk-1.0.17.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
|
|
48
|
+
ioa_observe_sdk-1.0.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|