ioa-observe-sdk 1.0.15__py3-none-any.whl → 1.0.16__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/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.16.dist-info}/METADATA +3 -1
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.16.dist-info}/RECORD +14 -12
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.16.dist-info}/WHEEL +0 -0
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.16.dist-info}/licenses/LICENSE.md +0 -0
- {ioa_observe_sdk-1.0.15.dist-info → ioa_observe_sdk-1.0.16.dist-info}/top_level.txt +0 -0
ioa_observe/sdk/__init__.py
CHANGED
|
@@ -89,7 +89,7 @@ class Observe:
|
|
|
89
89
|
|
|
90
90
|
if (
|
|
91
91
|
observe_sync_enabled
|
|
92
|
-
and api_endpoint.find("observe.com") != -1
|
|
92
|
+
and api_endpoint.find("agntcy-observe.com") != -1
|
|
93
93
|
and api_key
|
|
94
94
|
and (exporter is None)
|
|
95
95
|
and (processor is None)
|
|
@@ -112,13 +112,13 @@ class Observe:
|
|
|
112
112
|
if (
|
|
113
113
|
not exporter
|
|
114
114
|
and not processor
|
|
115
|
-
and api_endpoint == "https://api.observe.com"
|
|
115
|
+
and api_endpoint == "https://api.agntcy-observe.com"
|
|
116
116
|
and not api_key
|
|
117
117
|
):
|
|
118
118
|
print(
|
|
119
119
|
Fore.RED
|
|
120
120
|
+ "Error: Missing observe API key,"
|
|
121
|
-
+ " go to https://app.observe.com/settings/api-keys to create one"
|
|
121
|
+
+ " go to https://app.agntcy-observe.com/settings/api-keys to create one"
|
|
122
122
|
)
|
|
123
123
|
print("Set the OBSERVE_API_KEY environment variable to the key")
|
|
124
124
|
print(Fore.RESET)
|
ioa_observe/sdk/client/client.py
CHANGED
|
@@ -27,7 +27,7 @@ class Client:
|
|
|
27
27
|
self,
|
|
28
28
|
api_key: str,
|
|
29
29
|
app_name: str = sys.argv[0],
|
|
30
|
-
api_endpoint: str = "https://api.observe.com",
|
|
30
|
+
api_endpoint: str = "https://api.agntcy-observe.com",
|
|
31
31
|
):
|
|
32
32
|
"""
|
|
33
33
|
Initialize a new observe client.
|
|
@@ -35,13 +35,13 @@ class Client:
|
|
|
35
35
|
Args:
|
|
36
36
|
api_key (str): Your observe API key
|
|
37
37
|
app_name (Optional[str], optional): The name of your application. Defaults to sys.argv[0].
|
|
38
|
-
api_endpoint (Optional[str], optional): Custom API endpoint. Defaults to https://api.observe.com.
|
|
38
|
+
api_endpoint (Optional[str], optional): Custom API endpoint. Defaults to https://api.agntcy-observe.com.
|
|
39
39
|
"""
|
|
40
40
|
if not api_key or not api_key.strip():
|
|
41
41
|
raise ValueError("API key is required")
|
|
42
42
|
|
|
43
43
|
self.app_name = app_name
|
|
44
|
-
self.api_endpoint = api_endpoint or "https://api.observe.com"
|
|
44
|
+
self.api_endpoint = api_endpoint or "https://api.agntcy-observe.com"
|
|
45
45
|
self.api_key = api_key
|
|
46
46
|
self._http = HTTPClient(
|
|
47
47
|
base_url=self.api_endpoint, api_key=self.api_key, version=__version__
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright AGNTCY Contributors (https://github.com/agntcy)
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import inspect
|
|
4
|
-
from typing import Optional, Union, TypeVar, Callable,
|
|
4
|
+
from typing import Optional, Union, TypeVar, Callable, Any
|
|
5
5
|
|
|
6
6
|
from typing_extensions import ParamSpec
|
|
7
7
|
|
|
@@ -14,7 +14,7 @@ from ioa_observe.sdk.utils.const import ObserveSpanKindValues
|
|
|
14
14
|
|
|
15
15
|
P = ParamSpec("P")
|
|
16
16
|
R = TypeVar("R")
|
|
17
|
-
F = TypeVar("F", bound=Callable[
|
|
17
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def task(
|
|
@@ -7,7 +7,7 @@ import traceback
|
|
|
7
7
|
from functools import wraps
|
|
8
8
|
import os
|
|
9
9
|
import types
|
|
10
|
-
from typing import Optional, TypeVar, Callable,
|
|
10
|
+
from typing import Optional, TypeVar, Callable, Any
|
|
11
11
|
import inspect
|
|
12
12
|
|
|
13
13
|
from ioa_observe.sdk.decorators.helpers import (
|
|
@@ -53,7 +53,7 @@ from ioa_observe.sdk.metrics.agent import topology_dynamism, determinism_score
|
|
|
53
53
|
P = ParamSpec("P")
|
|
54
54
|
|
|
55
55
|
R = TypeVar("R")
|
|
56
|
-
F = TypeVar("F", bound=Callable[
|
|
56
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def _is_json_size_valid(json_str: str) -> bool:
|
|
@@ -14,7 +14,7 @@ from ioa_observe.sdk import TracerWrapper
|
|
|
14
14
|
from ioa_observe.sdk.client import kv_store
|
|
15
15
|
from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
|
|
16
16
|
|
|
17
|
-
_instruments = ("a2a-sdk >= 0.
|
|
17
|
+
_instruments = ("a2a-sdk >= 0.3.0",)
|
|
18
18
|
_global_tracer = None
|
|
19
19
|
_kv_lock = threading.RLock() # Add thread-safety for kv_store operations
|
|
20
20
|
|
|
@@ -34,15 +34,14 @@ class A2AInstrumentor(BaseInstrumentor):
|
|
|
34
34
|
if importlib.util.find_spec("a2a") is None:
|
|
35
35
|
raise ImportError("No module named 'a2a-sdk'. Please install it first.")
|
|
36
36
|
|
|
37
|
-
# Instrument
|
|
37
|
+
# Instrument client send_message
|
|
38
38
|
from a2a.client import A2AClient
|
|
39
39
|
|
|
40
40
|
original_send_message = A2AClient.send_message
|
|
41
41
|
|
|
42
42
|
@functools.wraps(original_send_message)
|
|
43
|
-
async def instrumented_send_message(
|
|
44
|
-
|
|
45
|
-
):
|
|
43
|
+
async def instrumented_send_message(self, request, *args, **kwargs):
|
|
44
|
+
# Put context into A2A message metadata instead of HTTP headers
|
|
46
45
|
with _global_tracer.start_as_current_span("a2a.send_message"):
|
|
47
46
|
traceparent = get_current_traceparent()
|
|
48
47
|
session_id = None
|
|
@@ -50,47 +49,101 @@ class A2AInstrumentor(BaseInstrumentor):
|
|
|
50
49
|
session_id = kv_store.get(f"execution.{traceparent}")
|
|
51
50
|
if session_id:
|
|
52
51
|
kv_store.set(f"execution.{traceparent}", session_id)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
|
|
53
|
+
# Ensure metadata dict exists
|
|
54
|
+
try:
|
|
55
|
+
md = getattr(request.params, "metadata", None)
|
|
56
|
+
except AttributeError:
|
|
57
|
+
md = None
|
|
58
|
+
metadata = md if isinstance(md, dict) else {}
|
|
59
|
+
|
|
60
|
+
observe_meta = dict(metadata.get("observe", {}))
|
|
61
|
+
|
|
62
|
+
# Inject W3C trace context + baggage into observe_meta
|
|
63
|
+
TraceContextTextMapPropagator().inject(carrier=observe_meta)
|
|
64
|
+
W3CBaggagePropagator().inject(carrier=observe_meta)
|
|
65
|
+
|
|
66
|
+
if traceparent:
|
|
67
|
+
observe_meta["traceparent"] = traceparent
|
|
58
68
|
if session_id:
|
|
59
|
-
|
|
69
|
+
observe_meta["session_id"] = session_id
|
|
60
70
|
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
71
|
|
|
64
|
-
|
|
72
|
+
metadata["observe"] = observe_meta
|
|
73
|
+
|
|
74
|
+
# Write back metadata (pydantic models are mutable by default in v2)
|
|
75
|
+
try:
|
|
76
|
+
request.params.metadata = metadata
|
|
77
|
+
except Exception:
|
|
78
|
+
# Fallback
|
|
79
|
+
request = request.model_copy(
|
|
80
|
+
update={
|
|
81
|
+
"params": request.params.model_copy(
|
|
82
|
+
update={"metadata": metadata}
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Call through without transport-specific kwargs
|
|
88
|
+
return await original_send_message(self, request, *args, **kwargs)
|
|
65
89
|
|
|
66
90
|
A2AClient.send_message = instrumented_send_message
|
|
67
91
|
|
|
92
|
+
# Instrument server handler
|
|
68
93
|
from a2a.server.request_handlers import DefaultRequestHandler
|
|
69
94
|
|
|
70
95
|
original_server_on_message_send = DefaultRequestHandler.on_message_send
|
|
71
96
|
|
|
72
97
|
@functools.wraps(original_server_on_message_send)
|
|
73
|
-
async def
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
async def instrumented_on_message_send(self, params, context):
|
|
99
|
+
# Read context from A2A message metadata (transport-agnostic)
|
|
100
|
+
try:
|
|
101
|
+
metadata = getattr(params, "metadata", {}) or {}
|
|
102
|
+
except Exception:
|
|
103
|
+
metadata = {}
|
|
104
|
+
|
|
105
|
+
carrier = {}
|
|
106
|
+
observe_meta = metadata.get("observe", {}) or {}
|
|
107
|
+
# Accept keys we inject
|
|
108
|
+
for k in ("traceparent", "baggage", "session_id"):
|
|
109
|
+
if k in observe_meta:
|
|
110
|
+
carrier[k] = observe_meta[k]
|
|
111
|
+
|
|
112
|
+
token = None
|
|
113
|
+
if carrier.get("traceparent"):
|
|
114
|
+
# Extract and attach parent context
|
|
84
115
|
ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
|
|
85
116
|
ctx = W3CBaggagePropagator().extract(carrier=carrier, context=ctx)
|
|
117
|
+
try:
|
|
118
|
+
from opentelemetry import context as otel_ctx
|
|
119
|
+
|
|
120
|
+
token = otel_ctx.attach(ctx)
|
|
121
|
+
except Exception:
|
|
122
|
+
token = None
|
|
123
|
+
|
|
124
|
+
session_id = observe_meta.get("session_id")
|
|
86
125
|
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)
|
|
126
|
+
set_session_id(session_id, traceparent=carrier.get("traceparent"))
|
|
127
|
+
kv_store.set(f"execution.{carrier.get('traceparent')}", session_id)
|
|
90
128
|
|
|
91
|
-
|
|
129
|
+
try:
|
|
130
|
+
return await original_server_on_message_send(self, params, context)
|
|
131
|
+
finally:
|
|
132
|
+
if token is not None:
|
|
133
|
+
try:
|
|
134
|
+
otel_ctx.detach(token)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
DefaultRequestHandler.on_message_send = instrumented_on_message_send
|
|
139
|
+
|
|
140
|
+
# from a2a.client import A2AClient
|
|
141
|
+
|
|
142
|
+
# A2AClient.send_message = instrumented_send_message
|
|
143
|
+
|
|
144
|
+
# from a2a.server.request_handlers import DefaultRequestHandler
|
|
92
145
|
|
|
93
|
-
DefaultRequestHandler.on_message_send
|
|
146
|
+
# original_server_on_message_send = DefaultRequestHandler.on_message_send
|
|
94
147
|
|
|
95
148
|
def _uninstrument(self, **kwargs):
|
|
96
149
|
import importlib
|
|
@@ -103,7 +156,7 @@ class A2AInstrumentor(BaseInstrumentor):
|
|
|
103
156
|
|
|
104
157
|
A2AClient.send_message = A2AClient.send_message.__wrapped__
|
|
105
158
|
|
|
106
|
-
# Uninstrument
|
|
159
|
+
# Uninstrument server handler
|
|
107
160
|
from a2a.server.request_handlers import DefaultRequestHandler
|
|
108
161
|
|
|
109
162
|
DefaultRequestHandler.on_message_send = (
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
# Copyright AGNTCY Contributors (https://github.com/agntcy)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, AsyncGenerator, Callable, Collection, Tuple, cast, Union
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import traceback
|
|
10
|
+
import re
|
|
11
|
+
from http import HTTPStatus
|
|
12
|
+
|
|
13
|
+
from opentelemetry import context, propagate
|
|
14
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
15
|
+
from opentelemetry.instrumentation.utils import unwrap
|
|
16
|
+
from opentelemetry.trace import get_tracer, Tracer
|
|
17
|
+
from wrapt import ObjectProxy, register_post_import_hook, wrap_function_wrapper
|
|
18
|
+
from opentelemetry.trace.status import Status, StatusCode
|
|
19
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
20
|
+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
|
21
|
+
|
|
22
|
+
from ..utils.const import (
|
|
23
|
+
MCP_REQUEST_ID,
|
|
24
|
+
MCP_METHOD_NAME,
|
|
25
|
+
MCP_REQUEST_ARGUMENT,
|
|
26
|
+
MCP_RESPONSE_VALUE,
|
|
27
|
+
OBSERVE_ENTITY_OUTPUT,
|
|
28
|
+
OBSERVE_ENTITY_INPUT,
|
|
29
|
+
)
|
|
30
|
+
from ..version import __version__
|
|
31
|
+
|
|
32
|
+
_instruments = ("mcp >= 1.6.0",)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Config:
|
|
36
|
+
exception_logger = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def dont_throw(func):
|
|
40
|
+
"""
|
|
41
|
+
A decorator that wraps the passed in function and logs exceptions instead of throwing them.
|
|
42
|
+
|
|
43
|
+
@param func: The function to wrap
|
|
44
|
+
@return: The wrapper function
|
|
45
|
+
"""
|
|
46
|
+
# Obtain a logger specific to the function's module
|
|
47
|
+
logger = logging.getLogger(func.__module__)
|
|
48
|
+
|
|
49
|
+
def wrapper(*args, **kwargs):
|
|
50
|
+
try:
|
|
51
|
+
return func(*args, **kwargs)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.debug(
|
|
54
|
+
"failed to trace in %s, error: %s",
|
|
55
|
+
func.__name__,
|
|
56
|
+
traceback.format_exc(),
|
|
57
|
+
)
|
|
58
|
+
if Config.exception_logger:
|
|
59
|
+
Config.exception_logger(e)
|
|
60
|
+
|
|
61
|
+
return wrapper
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class McpInstrumentor(BaseInstrumentor):
|
|
65
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
66
|
+
return _instruments
|
|
67
|
+
|
|
68
|
+
def _instrument(self, **kwargs):
|
|
69
|
+
tracer_provider = kwargs.get("tracer_provider")
|
|
70
|
+
tracer = get_tracer(__name__, __version__, tracer_provider)
|
|
71
|
+
|
|
72
|
+
register_post_import_hook(
|
|
73
|
+
lambda _: wrap_function_wrapper(
|
|
74
|
+
"mcp.client.sse", "sse_client", self._transport_wrapper(tracer)
|
|
75
|
+
),
|
|
76
|
+
"mcp.client.sse",
|
|
77
|
+
)
|
|
78
|
+
register_post_import_hook(
|
|
79
|
+
lambda _: wrap_function_wrapper(
|
|
80
|
+
"mcp.server.sse",
|
|
81
|
+
"SseServerTransport.connect_sse",
|
|
82
|
+
self._transport_wrapper(tracer),
|
|
83
|
+
),
|
|
84
|
+
"mcp.server.sse",
|
|
85
|
+
)
|
|
86
|
+
register_post_import_hook(
|
|
87
|
+
lambda _: wrap_function_wrapper(
|
|
88
|
+
"mcp.client.stdio", "stdio_client", self._transport_wrapper(tracer)
|
|
89
|
+
),
|
|
90
|
+
"mcp.client.stdio",
|
|
91
|
+
)
|
|
92
|
+
register_post_import_hook(
|
|
93
|
+
lambda _: wrap_function_wrapper(
|
|
94
|
+
"mcp.server.stdio", "stdio_server", self._transport_wrapper(tracer)
|
|
95
|
+
),
|
|
96
|
+
"mcp.server.stdio",
|
|
97
|
+
)
|
|
98
|
+
register_post_import_hook(
|
|
99
|
+
lambda _: wrap_function_wrapper(
|
|
100
|
+
"mcp.server.session",
|
|
101
|
+
"ServerSession.__init__",
|
|
102
|
+
self._base_session_init_wrapper(tracer),
|
|
103
|
+
),
|
|
104
|
+
"mcp.server.session",
|
|
105
|
+
)
|
|
106
|
+
register_post_import_hook(
|
|
107
|
+
lambda _: wrap_function_wrapper(
|
|
108
|
+
"mcp.client.streamable_http",
|
|
109
|
+
"streamablehttp_client",
|
|
110
|
+
self._transport_wrapper(tracer),
|
|
111
|
+
),
|
|
112
|
+
"mcp.client.streamable_http",
|
|
113
|
+
)
|
|
114
|
+
register_post_import_hook(
|
|
115
|
+
lambda _: wrap_function_wrapper(
|
|
116
|
+
"mcp.server.streamable_http",
|
|
117
|
+
"StreamableHTTPServerTransport.connect",
|
|
118
|
+
self._transport_wrapper(tracer),
|
|
119
|
+
),
|
|
120
|
+
"mcp.server.streamable_http",
|
|
121
|
+
)
|
|
122
|
+
wrap_function_wrapper(
|
|
123
|
+
"mcp.shared.session",
|
|
124
|
+
"BaseSession.send_request",
|
|
125
|
+
self.patch_mcp_client(tracer),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _uninstrument(self, **kwargs):
|
|
129
|
+
unwrap("mcp.client.stdio", "stdio_client")
|
|
130
|
+
unwrap("mcp.server.stdio", "stdio_server")
|
|
131
|
+
|
|
132
|
+
def _transport_wrapper(self, tracer):
|
|
133
|
+
@asynccontextmanager
|
|
134
|
+
async def traced_method(
|
|
135
|
+
wrapped: Callable[..., Any], instance: Any, args: Any, kwargs: Any
|
|
136
|
+
) -> AsyncGenerator[
|
|
137
|
+
Union[
|
|
138
|
+
Tuple[InstrumentedStreamReader, InstrumentedStreamWriter],
|
|
139
|
+
Tuple[InstrumentedStreamReader, InstrumentedStreamWriter, Any],
|
|
140
|
+
],
|
|
141
|
+
None,
|
|
142
|
+
]:
|
|
143
|
+
async with wrapped(*args, **kwargs) as result:
|
|
144
|
+
try:
|
|
145
|
+
read_stream, write_stream = result
|
|
146
|
+
yield (
|
|
147
|
+
InstrumentedStreamReader(read_stream, tracer),
|
|
148
|
+
InstrumentedStreamWriter(write_stream, tracer),
|
|
149
|
+
)
|
|
150
|
+
except ValueError:
|
|
151
|
+
try:
|
|
152
|
+
read_stream, write_stream, get_session_id_callback = result
|
|
153
|
+
yield (
|
|
154
|
+
InstrumentedStreamReader(read_stream, tracer),
|
|
155
|
+
InstrumentedStreamWriter(write_stream, tracer),
|
|
156
|
+
get_session_id_callback,
|
|
157
|
+
)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logging.warning(
|
|
160
|
+
f"mcp instrumentation _transport_wrapper exception: {e}"
|
|
161
|
+
)
|
|
162
|
+
yield result
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logging.warning(
|
|
165
|
+
f"mcp instrumentation transport_wrapper exception: {e}"
|
|
166
|
+
)
|
|
167
|
+
yield result
|
|
168
|
+
|
|
169
|
+
return traced_method
|
|
170
|
+
|
|
171
|
+
def _base_session_init_wrapper(self, tracer):
|
|
172
|
+
def traced_method(
|
|
173
|
+
wrapped: Callable[..., None], instance: Any, args: Any, kwargs: Any
|
|
174
|
+
) -> None:
|
|
175
|
+
wrapped(*args, **kwargs)
|
|
176
|
+
reader = getattr(instance, "_incoming_message_stream_reader", None)
|
|
177
|
+
writer = getattr(instance, "_incoming_message_stream_writer", None)
|
|
178
|
+
if reader and writer:
|
|
179
|
+
setattr(
|
|
180
|
+
instance,
|
|
181
|
+
"_incoming_message_stream_reader",
|
|
182
|
+
ContextAttachingStreamReader(reader, tracer),
|
|
183
|
+
)
|
|
184
|
+
setattr(
|
|
185
|
+
instance,
|
|
186
|
+
"_incoming_message_stream_writer",
|
|
187
|
+
ContextSavingStreamWriter(writer, tracer),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return traced_method
|
|
191
|
+
|
|
192
|
+
def patch_mcp_client(self, tracer: Tracer):
|
|
193
|
+
@dont_throw
|
|
194
|
+
async def traced_method(wrapped, instance, args, kwargs):
|
|
195
|
+
meta = None
|
|
196
|
+
method = None
|
|
197
|
+
params = None
|
|
198
|
+
if len(args) > 0 and hasattr(args[0].root, "method"):
|
|
199
|
+
method = args[0].root.method
|
|
200
|
+
if len(args) > 0 and hasattr(args[0].root, "params"):
|
|
201
|
+
params = args[0].root.params
|
|
202
|
+
if params:
|
|
203
|
+
if hasattr(args[0].root.params, "meta"):
|
|
204
|
+
meta = args[0].root.params.meta
|
|
205
|
+
|
|
206
|
+
with tracer.start_as_current_span(f"{method}.mcp") as span:
|
|
207
|
+
span.set_attribute(OBSERVE_ENTITY_INPUT, f"{serialize(args[0])}")
|
|
208
|
+
from ioa_observe.sdk.client import kv_store
|
|
209
|
+
from ioa_observe.sdk.tracing import get_current_traceparent
|
|
210
|
+
|
|
211
|
+
traceparent = get_current_traceparent()
|
|
212
|
+
session_id = None
|
|
213
|
+
if traceparent:
|
|
214
|
+
session_id = kv_store.get(f"execution.{traceparent}")
|
|
215
|
+
if session_id:
|
|
216
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
217
|
+
|
|
218
|
+
meta = meta or {}
|
|
219
|
+
if isinstance(meta, dict):
|
|
220
|
+
meta["session.id"] = session_id
|
|
221
|
+
meta["traceparent"] = traceparent
|
|
222
|
+
else:
|
|
223
|
+
# If meta is an object, convert it to a dict
|
|
224
|
+
meta = {
|
|
225
|
+
"session.id": session_id,
|
|
226
|
+
"traceparent": traceparent,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if meta and len(args) > 0:
|
|
230
|
+
carrier = {}
|
|
231
|
+
TraceContextTextMapPropagator().inject(carrier)
|
|
232
|
+
|
|
233
|
+
args[0].root.params.meta = meta
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
result = await wrapped(*args, **kwargs)
|
|
237
|
+
span.set_attribute(
|
|
238
|
+
OBSERVE_ENTITY_OUTPUT,
|
|
239
|
+
serialize(result),
|
|
240
|
+
)
|
|
241
|
+
if hasattr(result, "isError") and result.isError:
|
|
242
|
+
if len(result.content) > 0:
|
|
243
|
+
span.set_status(
|
|
244
|
+
Status(StatusCode.ERROR, f"{result.content[0].text}")
|
|
245
|
+
)
|
|
246
|
+
error_type = get_error_type(result.content[0].text)
|
|
247
|
+
if error_type is not None:
|
|
248
|
+
span.set_attribute(ERROR_TYPE, error_type)
|
|
249
|
+
else:
|
|
250
|
+
span.set_status(Status(StatusCode.OK))
|
|
251
|
+
return result
|
|
252
|
+
except Exception as e:
|
|
253
|
+
span.set_attribute(ERROR_TYPE, type(e).__name__)
|
|
254
|
+
span.record_exception(e)
|
|
255
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
256
|
+
raise
|
|
257
|
+
|
|
258
|
+
return traced_method
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_error_type(error_message):
|
|
262
|
+
if not isinstance(error_message, str):
|
|
263
|
+
return None
|
|
264
|
+
match = re.search(r"\b(4\d{2}|5\d{2})\b", error_message)
|
|
265
|
+
if match:
|
|
266
|
+
num = int(match.group())
|
|
267
|
+
if 400 <= num <= 599:
|
|
268
|
+
return HTTPStatus(num).name
|
|
269
|
+
else:
|
|
270
|
+
return None
|
|
271
|
+
else:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def serialize(request, depth=0, max_depth=4):
|
|
276
|
+
"""Serialize input args to MCP server into JSON.
|
|
277
|
+
The function accepts input object and converts into JSON
|
|
278
|
+
keeping depth in mind to prevent creating large nested JSON"""
|
|
279
|
+
if depth > max_depth:
|
|
280
|
+
return {}
|
|
281
|
+
depth += 1
|
|
282
|
+
|
|
283
|
+
def is_serializable(request):
|
|
284
|
+
try:
|
|
285
|
+
json.dumps(request)
|
|
286
|
+
return True
|
|
287
|
+
except Exception:
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
if is_serializable(request):
|
|
291
|
+
return json.dumps(request)
|
|
292
|
+
else:
|
|
293
|
+
result = {}
|
|
294
|
+
try:
|
|
295
|
+
if hasattr(request, "model_dump_json"):
|
|
296
|
+
return request.model_dump_json()
|
|
297
|
+
if hasattr(request, "__dict__"):
|
|
298
|
+
for attrib in request.__dict__:
|
|
299
|
+
if not attrib.startswith("_"):
|
|
300
|
+
if type(request.__dict__[attrib]) in [
|
|
301
|
+
bool,
|
|
302
|
+
str,
|
|
303
|
+
int,
|
|
304
|
+
float,
|
|
305
|
+
type(None),
|
|
306
|
+
]:
|
|
307
|
+
result[str(attrib)] = request.__dict__[attrib]
|
|
308
|
+
else:
|
|
309
|
+
result[str(attrib)] = serialize(
|
|
310
|
+
request.__dict__[attrib], depth
|
|
311
|
+
)
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
return json.dumps(result)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class InstrumentedStreamReader(ObjectProxy): # type: ignore
|
|
318
|
+
def __init__(self, wrapped, tracer):
|
|
319
|
+
super().__init__(wrapped)
|
|
320
|
+
self._tracer = tracer
|
|
321
|
+
|
|
322
|
+
async def __aenter__(self) -> Any:
|
|
323
|
+
return await self.__wrapped__.__aenter__()
|
|
324
|
+
|
|
325
|
+
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
|
|
326
|
+
return await self.__wrapped__.__aexit__(exc_type, exc_value, traceback)
|
|
327
|
+
|
|
328
|
+
@dont_throw
|
|
329
|
+
async def __aiter__(self) -> AsyncGenerator[Any, None]:
|
|
330
|
+
from mcp.types import JSONRPCMessage, JSONRPCRequest
|
|
331
|
+
from mcp.shared.message import SessionMessage
|
|
332
|
+
|
|
333
|
+
async for item in self.__wrapped__:
|
|
334
|
+
if isinstance(item, SessionMessage):
|
|
335
|
+
request = cast(JSONRPCMessage, item.message).root
|
|
336
|
+
elif type(item) is JSONRPCMessage:
|
|
337
|
+
request = cast(JSONRPCMessage, item).root
|
|
338
|
+
else:
|
|
339
|
+
yield item
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
if not isinstance(request, JSONRPCRequest):
|
|
343
|
+
yield item
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
if request.params:
|
|
347
|
+
# Check both _meta and meta fields
|
|
348
|
+
meta = request.params.get("_meta") or getattr(
|
|
349
|
+
request.params, "meta", None
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if meta:
|
|
353
|
+
if isinstance(meta, dict):
|
|
354
|
+
session_id = meta.get("session.id")
|
|
355
|
+
traceparent = meta.get("traceparent")
|
|
356
|
+
carrier = meta
|
|
357
|
+
else:
|
|
358
|
+
session_id = getattr(meta, "session.id", None)
|
|
359
|
+
traceparent = getattr(meta, "traceparent", None)
|
|
360
|
+
# Convert object to dict for propagate.extract
|
|
361
|
+
carrier = {}
|
|
362
|
+
if session_id:
|
|
363
|
+
carrier["session.id"] = session_id
|
|
364
|
+
|
|
365
|
+
if carrier and traceparent:
|
|
366
|
+
ctx = propagate.extract(carrier)
|
|
367
|
+
|
|
368
|
+
# Add session_id extraction and storage like in a2a.py
|
|
369
|
+
if session_id and session_id != "None":
|
|
370
|
+
from ioa_observe.sdk.client import kv_store
|
|
371
|
+
from ioa_observe.sdk.tracing import set_session_id
|
|
372
|
+
|
|
373
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
374
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
375
|
+
|
|
376
|
+
restore = context.attach(ctx)
|
|
377
|
+
try:
|
|
378
|
+
yield item
|
|
379
|
+
continue
|
|
380
|
+
finally:
|
|
381
|
+
context.detach(restore)
|
|
382
|
+
yield item
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class InstrumentedStreamWriter(ObjectProxy): # type: ignore
|
|
386
|
+
def __init__(self, wrapped, tracer):
|
|
387
|
+
super().__init__(wrapped)
|
|
388
|
+
self._tracer = tracer
|
|
389
|
+
|
|
390
|
+
async def __aenter__(self) -> Any:
|
|
391
|
+
return await self.__wrapped__.__aenter__()
|
|
392
|
+
|
|
393
|
+
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
|
|
394
|
+
return await self.__wrapped__.__aexit__(exc_type, exc_value, traceback)
|
|
395
|
+
|
|
396
|
+
@dont_throw
|
|
397
|
+
async def send(self, item: Any) -> Any:
|
|
398
|
+
from mcp.types import JSONRPCMessage, JSONRPCRequest
|
|
399
|
+
from mcp.shared.message import SessionMessage
|
|
400
|
+
|
|
401
|
+
if isinstance(item, SessionMessage):
|
|
402
|
+
request = cast(JSONRPCMessage, item.message).root
|
|
403
|
+
elif type(item) is JSONRPCMessage:
|
|
404
|
+
request = cast(JSONRPCMessage, item).root
|
|
405
|
+
else:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
with self._tracer.start_as_current_span("ResponseStreamWriter") as span:
|
|
409
|
+
if hasattr(request, "result"):
|
|
410
|
+
span.set_attribute(MCP_RESPONSE_VALUE, f"{serialize(request.result)}")
|
|
411
|
+
if "isError" in request.result:
|
|
412
|
+
if request.result["isError"] is True:
|
|
413
|
+
span.set_status(
|
|
414
|
+
Status(
|
|
415
|
+
StatusCode.ERROR,
|
|
416
|
+
f"{request.result['content'][0]['text']}",
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
error_type = get_error_type(
|
|
420
|
+
request.result["content"][0]["text"]
|
|
421
|
+
)
|
|
422
|
+
if error_type is not None:
|
|
423
|
+
span.set_attribute(ERROR_TYPE, error_type)
|
|
424
|
+
if hasattr(request, "id"):
|
|
425
|
+
span.set_attribute(MCP_REQUEST_ID, f"{request.id}")
|
|
426
|
+
|
|
427
|
+
if not isinstance(request, JSONRPCRequest):
|
|
428
|
+
return await self.__wrapped__.send(item)
|
|
429
|
+
meta = None
|
|
430
|
+
if not request.params:
|
|
431
|
+
request.params = {}
|
|
432
|
+
meta = request.params.setdefault("_meta", {})
|
|
433
|
+
|
|
434
|
+
propagate.get_global_textmap().inject(meta)
|
|
435
|
+
return await self.__wrapped__.send(item)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@dataclass(slots=True, frozen=True)
|
|
439
|
+
class ItemWithContext:
|
|
440
|
+
item: Any
|
|
441
|
+
ctx: context.Context
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class ContextSavingStreamWriter(ObjectProxy): # type: ignore
|
|
445
|
+
def __init__(self, wrapped, tracer):
|
|
446
|
+
super().__init__(wrapped)
|
|
447
|
+
self._tracer = tracer
|
|
448
|
+
|
|
449
|
+
async def __aenter__(self) -> Any:
|
|
450
|
+
return await self.__wrapped__.__aenter__()
|
|
451
|
+
|
|
452
|
+
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
|
|
453
|
+
return await self.__wrapped__.__aexit__(exc_type, exc_value, traceback)
|
|
454
|
+
|
|
455
|
+
@dont_throw
|
|
456
|
+
async def send(self, item: Any) -> Any:
|
|
457
|
+
with self._tracer.start_as_current_span("RequestStreamWriter") as span:
|
|
458
|
+
if hasattr(item, "request_id"):
|
|
459
|
+
span.set_attribute(MCP_REQUEST_ID, f"{item.request_id}")
|
|
460
|
+
if hasattr(item, "request"):
|
|
461
|
+
if hasattr(item.request, "root"):
|
|
462
|
+
if hasattr(item.request.root, "method"):
|
|
463
|
+
span.set_attribute(
|
|
464
|
+
MCP_METHOD_NAME,
|
|
465
|
+
f"{item.request.root.method}",
|
|
466
|
+
)
|
|
467
|
+
if hasattr(item.request.root, "params"):
|
|
468
|
+
span.set_attribute(
|
|
469
|
+
MCP_REQUEST_ARGUMENT,
|
|
470
|
+
f"{serialize(item.request.root.params)}",
|
|
471
|
+
)
|
|
472
|
+
ctx = context.get_current()
|
|
473
|
+
return await self.__wrapped__.send(ItemWithContext(item, ctx))
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class ContextAttachingStreamReader(ObjectProxy): # type: ignore
|
|
477
|
+
def __init__(self, wrapped, tracer):
|
|
478
|
+
super().__init__(wrapped)
|
|
479
|
+
self._tracer = tracer
|
|
480
|
+
|
|
481
|
+
async def __aenter__(self) -> Any:
|
|
482
|
+
return await self.__wrapped__.__aenter__()
|
|
483
|
+
|
|
484
|
+
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
|
|
485
|
+
return await self.__wrapped__.__aexit__(exc_type, exc_value, traceback)
|
|
486
|
+
|
|
487
|
+
async def __aiter__(self) -> AsyncGenerator[Any, None]:
|
|
488
|
+
async for item in self.__wrapped__:
|
|
489
|
+
item_with_context = cast(ItemWithContext, item)
|
|
490
|
+
restore = context.attach(item_with_context.ctx)
|
|
491
|
+
try:
|
|
492
|
+
yield item_with_context.item
|
|
493
|
+
finally:
|
|
494
|
+
context.detach(restore)
|
|
@@ -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.16
|
|
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,20 +1,21 @@
|
|
|
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=
|
|
17
|
+
ioa_observe/sdk/instrumentations/a2a.py,sha256=ZpqvPl4u-yheQzSdBfxnZhWFZ8ntbKni_uaW3IDyjqw,6309
|
|
18
|
+
ioa_observe/sdk/instrumentations/mcp.py,sha256=vRM3ofnn7AMmry2RrfyZnZVPEutLWiDMghx2TSnm0Wk,18569
|
|
18
19
|
ioa_observe/sdk/instrumentations/slim.py,sha256=J5e6XeshH55xXaUiT9_j4R_n6VQELzBjgRAU-AgZGOg,11435
|
|
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
|
|
@@ -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.16.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
|
|
45
|
+
ioa_observe_sdk-1.0.16.dist-info/METADATA,sha256=j8GCTBcPggbbhImBhXvPU6JobBfV-HLwuE_2952sLao,7027
|
|
46
|
+
ioa_observe_sdk-1.0.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
47
|
+
ioa_observe_sdk-1.0.16.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
|
|
48
|
+
ioa_observe_sdk-1.0.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|