ioa-observe-sdk 1.0.21__py3-none-any.whl → 1.0.22__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/instrumentations/nats.py +324 -0
- {ioa_observe_sdk-1.0.21.dist-info → ioa_observe_sdk-1.0.22.dist-info}/METADATA +1 -1
- {ioa_observe_sdk-1.0.21.dist-info → ioa_observe_sdk-1.0.22.dist-info}/RECORD +6 -5
- {ioa_observe_sdk-1.0.21.dist-info → ioa_observe_sdk-1.0.22.dist-info}/WHEEL +0 -0
- {ioa_observe_sdk-1.0.21.dist-info → ioa_observe_sdk-1.0.22.dist-info}/licenses/LICENSE.md +0 -0
- {ioa_observe_sdk-1.0.21.dist-info → ioa_observe_sdk-1.0.22.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,324 @@
|
|
|
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 json
|
|
7
|
+
import base64
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
from opentelemetry import baggage, context
|
|
11
|
+
from opentelemetry.baggage.propagation import W3CBaggagePropagator
|
|
12
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
13
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
14
|
+
|
|
15
|
+
from ioa_observe.sdk import TracerWrapper
|
|
16
|
+
from ioa_observe.sdk.client import kv_store
|
|
17
|
+
from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
|
|
18
|
+
|
|
19
|
+
_instruments = ("nats-py >= 2.10.0",)
|
|
20
|
+
_global_tracer = None
|
|
21
|
+
_kv_lock = threading.RLock() # Add thread-safety for kv_store operations
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NATSInstrumentor(BaseInstrumentor):
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__()
|
|
27
|
+
global _global_tracer
|
|
28
|
+
_global_tracer = TracerWrapper().get_tracer()
|
|
29
|
+
|
|
30
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
31
|
+
return _instruments
|
|
32
|
+
|
|
33
|
+
def _instrument(self, **kwargs):
|
|
34
|
+
try:
|
|
35
|
+
import nats
|
|
36
|
+
except ImportError:
|
|
37
|
+
raise ImportError("No module named 'nats'. Please install it first.")
|
|
38
|
+
|
|
39
|
+
# Instrument `publish` method
|
|
40
|
+
original_publish = nats.NATS.publish
|
|
41
|
+
|
|
42
|
+
@functools.wraps(original_publish)
|
|
43
|
+
async def instrumented_publish(self, *args, **kwargs):
|
|
44
|
+
if _global_tracer:
|
|
45
|
+
with _global_tracer.start_as_current_span("nats.publish") as span:
|
|
46
|
+
traceparent = get_current_traceparent()
|
|
47
|
+
span.set_attribute("nats.topic", args[0] if args else None)
|
|
48
|
+
else:
|
|
49
|
+
traceparent = get_current_traceparent()
|
|
50
|
+
|
|
51
|
+
# Thread-safe access to kv_store
|
|
52
|
+
session_id = None
|
|
53
|
+
if traceparent:
|
|
54
|
+
with _kv_lock:
|
|
55
|
+
session_id = kv_store.get(f"execution.{traceparent}")
|
|
56
|
+
if session_id:
|
|
57
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
58
|
+
|
|
59
|
+
headers = {
|
|
60
|
+
"session_id": session_id if session_id else None,
|
|
61
|
+
"traceparent": traceparent,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Set baggage context
|
|
65
|
+
if traceparent and session_id:
|
|
66
|
+
baggage.set_baggage(f"execution.{traceparent}", session_id)
|
|
67
|
+
|
|
68
|
+
# Wrap message with headers - handle different message positions
|
|
69
|
+
message_arg_index = 1 # message will typically be the second argument
|
|
70
|
+
if len(args) > message_arg_index:
|
|
71
|
+
original_args = list(args)
|
|
72
|
+
message = original_args[message_arg_index]
|
|
73
|
+
wrapped_message = NATSInstrumentor._wrap_message_with_headers(
|
|
74
|
+
self, message, headers
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Convert wrapped message back to bytes if needed
|
|
78
|
+
if isinstance(wrapped_message, dict):
|
|
79
|
+
message_to_send = json.dumps(wrapped_message).encode("utf-8")
|
|
80
|
+
else:
|
|
81
|
+
message_to_send = wrapped_message
|
|
82
|
+
|
|
83
|
+
original_args[message_arg_index] = message_to_send
|
|
84
|
+
args = tuple(original_args)
|
|
85
|
+
|
|
86
|
+
return await original_publish(self, *args, **kwargs)
|
|
87
|
+
|
|
88
|
+
nats.NATS.publish = instrumented_publish
|
|
89
|
+
|
|
90
|
+
# Instrument `request` method
|
|
91
|
+
original_request = nats.NATS.request
|
|
92
|
+
|
|
93
|
+
@functools.wraps(original_request)
|
|
94
|
+
async def instrumented_request(self, *args, **kwargs):
|
|
95
|
+
if _global_tracer:
|
|
96
|
+
with _global_tracer.start_as_current_span("nats.request") as span:
|
|
97
|
+
traceparent = get_current_traceparent()
|
|
98
|
+
span.set_attribute("nats.topic", args[0] if args else None)
|
|
99
|
+
else:
|
|
100
|
+
traceparent = get_current_traceparent()
|
|
101
|
+
|
|
102
|
+
# Thread-safe access to kv_store
|
|
103
|
+
session_id = None
|
|
104
|
+
if traceparent:
|
|
105
|
+
with _kv_lock:
|
|
106
|
+
session_id = kv_store.get(f"execution.{traceparent}")
|
|
107
|
+
if session_id:
|
|
108
|
+
kv_store.set(f"execution.{traceparent}", session_id)
|
|
109
|
+
|
|
110
|
+
headers = {
|
|
111
|
+
"session_id": session_id if session_id else None,
|
|
112
|
+
"traceparent": traceparent,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Set baggage context
|
|
116
|
+
if traceparent and session_id:
|
|
117
|
+
baggage.set_baggage(f"execution.{traceparent}", session_id)
|
|
118
|
+
|
|
119
|
+
# Wrap message with headers - handle different message positions
|
|
120
|
+
message_arg_index = 1 # message will typically be the second argument
|
|
121
|
+
if len(args) > message_arg_index:
|
|
122
|
+
original_args = list(args)
|
|
123
|
+
message = original_args[message_arg_index]
|
|
124
|
+
wrapped_message = NATSInstrumentor._wrap_message_with_headers(
|
|
125
|
+
self, message, headers
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Convert wrapped message back to bytes if needed
|
|
129
|
+
if isinstance(wrapped_message, dict):
|
|
130
|
+
message_to_send = json.dumps(wrapped_message).encode("utf-8")
|
|
131
|
+
else:
|
|
132
|
+
message_to_send = wrapped_message
|
|
133
|
+
|
|
134
|
+
original_args[message_arg_index] = message_to_send
|
|
135
|
+
args = tuple(original_args)
|
|
136
|
+
|
|
137
|
+
return await original_request(self, *args, **kwargs)
|
|
138
|
+
|
|
139
|
+
nats.NATS.request = instrumented_request
|
|
140
|
+
|
|
141
|
+
# Instrument `subscribe` method
|
|
142
|
+
original_subscribe = nats.NATS.subscribe
|
|
143
|
+
|
|
144
|
+
@functools.wraps(original_subscribe)
|
|
145
|
+
async def instrumented_subscribe(self, subject, cb=None, *args, **kwargs):
|
|
146
|
+
# Wrap the callback to add tracing spans for message handling
|
|
147
|
+
if (
|
|
148
|
+
cb is not None
|
|
149
|
+
and _global_tracer
|
|
150
|
+
and not getattr(cb, "_is_instrumented", False)
|
|
151
|
+
):
|
|
152
|
+
user_cb = cb # SAVE the original callback
|
|
153
|
+
|
|
154
|
+
@functools.wraps(user_cb)
|
|
155
|
+
async def traced_callback(msg):
|
|
156
|
+
try:
|
|
157
|
+
message_dict = json.loads(msg.data.decode())
|
|
158
|
+
headers = message_dict.get("headers", {})
|
|
159
|
+
|
|
160
|
+
# Extract traceparent and session info from headers
|
|
161
|
+
traceparent = headers.get("traceparent")
|
|
162
|
+
session_id = headers.get("session_id")
|
|
163
|
+
|
|
164
|
+
# Create carrier for context propagation
|
|
165
|
+
carrier = {}
|
|
166
|
+
for key in ["traceparent", "Traceparent", "baggage", "Baggage"]:
|
|
167
|
+
if key.lower() in [k.lower() for k in headers.keys()]:
|
|
168
|
+
for k in headers.keys():
|
|
169
|
+
if k.lower() == key.lower():
|
|
170
|
+
carrier[key.lower()] = headers[k]
|
|
171
|
+
|
|
172
|
+
# Restore trace context
|
|
173
|
+
ctx = None
|
|
174
|
+
if carrier and traceparent:
|
|
175
|
+
ctx = TraceContextTextMapPropagator().extract(
|
|
176
|
+
carrier=carrier
|
|
177
|
+
)
|
|
178
|
+
ctx = W3CBaggagePropagator().extract(
|
|
179
|
+
carrier=carrier, context=ctx
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Activate the restored context
|
|
183
|
+
token = context.attach(ctx)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Set execution ID with the restored context
|
|
187
|
+
if session_id and session_id != "None":
|
|
188
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
189
|
+
|
|
190
|
+
# Store in kv_store with thread safety
|
|
191
|
+
with _kv_lock:
|
|
192
|
+
kv_store.set(
|
|
193
|
+
f"execution.{traceparent}", session_id
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# DON'T detach the context yet - we need it to persist for the callback
|
|
197
|
+
# The context will be cleaned up later or by the garbage collector
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
# Only detach on error
|
|
201
|
+
context.detach(token)
|
|
202
|
+
raise e
|
|
203
|
+
elif traceparent and session_id and session_id != "None":
|
|
204
|
+
# Even without carrier context, set session ID if we have the data
|
|
205
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
206
|
+
|
|
207
|
+
# Fallback: check stored execution ID if not found in headers
|
|
208
|
+
if traceparent and (not session_id or session_id == "None"):
|
|
209
|
+
with _kv_lock:
|
|
210
|
+
stored_session_id = kv_store.get(
|
|
211
|
+
f"execution.{traceparent}"
|
|
212
|
+
)
|
|
213
|
+
if stored_session_id:
|
|
214
|
+
session_id = stored_session_id
|
|
215
|
+
set_session_id(session_id, traceparent=traceparent)
|
|
216
|
+
|
|
217
|
+
# Process and clean the message
|
|
218
|
+
message_to_return = message_dict.copy()
|
|
219
|
+
if "headers" in message_to_return:
|
|
220
|
+
headers_copy = message_to_return["headers"].copy()
|
|
221
|
+
# Remove tracing-specific headers but keep other headers
|
|
222
|
+
headers_copy.pop("traceparent", None)
|
|
223
|
+
headers_copy.pop("session_id", None)
|
|
224
|
+
if headers_copy:
|
|
225
|
+
message_to_return["headers"] = headers_copy
|
|
226
|
+
else:
|
|
227
|
+
message_to_return.pop("headers", None)
|
|
228
|
+
|
|
229
|
+
# Return processed message, update msg.data
|
|
230
|
+
if isinstance(message_to_return, str):
|
|
231
|
+
msg.data = message_to_return.encode("utf-8")
|
|
232
|
+
else:
|
|
233
|
+
msg.data = json.dumps(message_to_return).encode("utf-8")
|
|
234
|
+
|
|
235
|
+
# Now call the original user callback with the modified msg
|
|
236
|
+
ctx = {} if ctx is None else ctx
|
|
237
|
+
if _global_tracer:
|
|
238
|
+
with _global_tracer.start_as_current_span(
|
|
239
|
+
"nats.subscribe.callback", context=ctx
|
|
240
|
+
) as span:
|
|
241
|
+
span.set_attribute("nats.subject", subject)
|
|
242
|
+
span.set_attribute("nats.session_id", session_id)
|
|
243
|
+
await user_cb(msg)
|
|
244
|
+
else:
|
|
245
|
+
await user_cb(msg)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
print(f"Error processing message in traced_callback: {e}")
|
|
248
|
+
await user_cb(msg) # Call original callback even on error
|
|
249
|
+
|
|
250
|
+
traced_callback._is_instrumented = True # mark as instrumented
|
|
251
|
+
cb = traced_callback
|
|
252
|
+
|
|
253
|
+
return await original_subscribe(self, subject, cb=cb, *args, **kwargs)
|
|
254
|
+
|
|
255
|
+
nats.NATS.subscribe = instrumented_subscribe
|
|
256
|
+
|
|
257
|
+
def _wrap_message_with_headers(self, message, headers):
|
|
258
|
+
"""Helper method to wrap messages with headers consistently"""
|
|
259
|
+
if isinstance(message, bytes):
|
|
260
|
+
try:
|
|
261
|
+
decoded_message = message.decode("utf-8")
|
|
262
|
+
try:
|
|
263
|
+
original_message = json.loads(decoded_message)
|
|
264
|
+
if isinstance(original_message, dict):
|
|
265
|
+
wrapped_message = original_message.copy()
|
|
266
|
+
existing_headers = wrapped_message.get("headers", {})
|
|
267
|
+
existing_headers.update(headers)
|
|
268
|
+
wrapped_message["headers"] = existing_headers
|
|
269
|
+
else:
|
|
270
|
+
wrapped_message = {
|
|
271
|
+
"headers": headers,
|
|
272
|
+
"payload": original_message,
|
|
273
|
+
}
|
|
274
|
+
except json.JSONDecodeError:
|
|
275
|
+
wrapped_message = {"headers": headers, "payload": decoded_message}
|
|
276
|
+
except UnicodeDecodeError:
|
|
277
|
+
# Fix type annotation issue by ensuring message is bytes
|
|
278
|
+
encoded_message = (
|
|
279
|
+
message if isinstance(message, bytes) else message.encode("utf-8")
|
|
280
|
+
)
|
|
281
|
+
wrapped_message = {
|
|
282
|
+
"headers": headers,
|
|
283
|
+
"payload": base64.b64encode(encoded_message).decode("utf-8"),
|
|
284
|
+
}
|
|
285
|
+
elif isinstance(message, str):
|
|
286
|
+
try:
|
|
287
|
+
original_message = json.loads(message)
|
|
288
|
+
if isinstance(original_message, dict):
|
|
289
|
+
wrapped_message = original_message.copy()
|
|
290
|
+
existing_headers = wrapped_message.get("headers", {})
|
|
291
|
+
existing_headers.update(headers)
|
|
292
|
+
wrapped_message["headers"] = existing_headers
|
|
293
|
+
else:
|
|
294
|
+
wrapped_message = {"headers": headers, "payload": original_message}
|
|
295
|
+
except json.JSONDecodeError:
|
|
296
|
+
wrapped_message = {"headers": headers, "payload": message}
|
|
297
|
+
elif isinstance(message, dict):
|
|
298
|
+
wrapped_message = message.copy()
|
|
299
|
+
existing_headers = wrapped_message.get("headers", {})
|
|
300
|
+
existing_headers.update(headers)
|
|
301
|
+
wrapped_message["headers"] = existing_headers
|
|
302
|
+
else:
|
|
303
|
+
wrapped_message = {"headers": headers, "payload": json.dumps(message)}
|
|
304
|
+
|
|
305
|
+
return wrapped_message
|
|
306
|
+
|
|
307
|
+
def _uninstrument(self, **kwargs):
|
|
308
|
+
try:
|
|
309
|
+
import nats
|
|
310
|
+
except ImportError:
|
|
311
|
+
raise ImportError("No module named 'nats'. Please install it first.")
|
|
312
|
+
|
|
313
|
+
# Restore the original methods
|
|
314
|
+
methods_to_restore = [
|
|
315
|
+
"publish",
|
|
316
|
+
"request",
|
|
317
|
+
"subscribe",
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
for method_name in methods_to_restore:
|
|
321
|
+
if hasattr(nats.NATS, method_name):
|
|
322
|
+
original_method = getattr(nats.NATS, method_name)
|
|
323
|
+
if hasattr(original_method, "__wrapped__"):
|
|
324
|
+
setattr(nats.NATS, method_name, original_method.__wrapped__)
|
|
@@ -16,6 +16,7 @@ ioa_observe/sdk/decorators/util.py,sha256=IebvH9gwZN1en3LblYJUh4bAV2STl6xmp8WpZz
|
|
|
16
16
|
ioa_observe/sdk/instrumentations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
ioa_observe/sdk/instrumentations/a2a.py,sha256=ZpqvPl4u-yheQzSdBfxnZhWFZ8ntbKni_uaW3IDyjqw,6309
|
|
18
18
|
ioa_observe/sdk/instrumentations/mcp.py,sha256=vRM3ofnn7AMmry2RrfyZnZVPEutLWiDMghx2TSnm0Wk,18569
|
|
19
|
+
ioa_observe/sdk/instrumentations/nats.py,sha256=UOp2AJlm1JkYkwF3xzU_izzohQVQkByjL-AX4n_JRfo,14476
|
|
19
20
|
ioa_observe/sdk/instrumentations/slim.py,sha256=r_vIuYUo-IhSRuYWymqfS26swDZ8_CFDH6p_QbduLKU,43757
|
|
20
21
|
ioa_observe/sdk/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
22
|
ioa_observe/sdk/logging/logging.py,sha256=HZxW9s8Due7jgiNkdI38cIjv5rC9D-Flta3RQMOnpow,2891
|
|
@@ -41,8 +42,8 @@ ioa_observe/sdk/utils/const.py,sha256=d67dUTAH9UpWvUV9GLBUqn1Sc2knJ55dy-e6YoLrvS
|
|
|
41
42
|
ioa_observe/sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
|
|
42
43
|
ioa_observe/sdk/utils/json_encoder.py,sha256=g4NQ0tTqgWssY6I1D7r4zo0G6PiUo61jhofTAw5-jno,639
|
|
43
44
|
ioa_observe/sdk/utils/package_check.py,sha256=1d1MjxhwoEZIx9dumirT2pRsEWgn-m-SI4npDeEalew,576
|
|
44
|
-
ioa_observe_sdk-1.0.
|
|
45
|
-
ioa_observe_sdk-1.0.
|
|
46
|
-
ioa_observe_sdk-1.0.
|
|
47
|
-
ioa_observe_sdk-1.0.
|
|
48
|
-
ioa_observe_sdk-1.0.
|
|
45
|
+
ioa_observe_sdk-1.0.22.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
|
|
46
|
+
ioa_observe_sdk-1.0.22.dist-info/METADATA,sha256=Uces4XbDjeIZ2okZImfwPXNHUtyoO7od75dHthOs56Q,7058
|
|
47
|
+
ioa_observe_sdk-1.0.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
48
|
+
ioa_observe_sdk-1.0.22.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
|
|
49
|
+
ioa_observe_sdk-1.0.22.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|