ioa-observe-sdk 1.0.20__tar.gz → 1.0.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/PKG-INFO +15 -14
  2. ioa_observe_sdk-1.0.22/ioa_observe/sdk/instrumentations/nats.py +324 -0
  3. ioa_observe_sdk-1.0.22/ioa_observe/sdk/instrumentations/slim.py +958 -0
  4. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe_sdk.egg-info/PKG-INFO +15 -14
  5. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe_sdk.egg-info/SOURCES.txt +1 -0
  6. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe_sdk.egg-info/requires.txt +13 -13
  7. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/pyproject.toml +15 -14
  8. ioa_observe_sdk-1.0.20/ioa_observe/sdk/instrumentations/slim.py +0 -523
  9. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/LICENSE.md +0 -0
  10. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/README.md +0 -0
  11. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/__init__.py +0 -0
  12. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/__init__.py +0 -0
  13. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/client/__init__.py +0 -0
  14. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/client/client.py +0 -0
  15. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/client/http.py +0 -0
  16. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/config/__init__.py +0 -0
  17. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/connectors/__init__.py +0 -0
  18. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/connectors/slim.py +0 -0
  19. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/decorators/__init__.py +0 -0
  20. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/decorators/base.py +0 -0
  21. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/decorators/helpers.py +0 -0
  22. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/decorators/util.py +0 -0
  23. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/instrumentations/__init__.py +0 -0
  24. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/instrumentations/a2a.py +0 -0
  25. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/instrumentations/mcp.py +0 -0
  26. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/instruments.py +0 -0
  27. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/logging/__init__.py +0 -0
  28. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/logging/logging.py +0 -0
  29. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/__init__.py +0 -0
  30. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/agent.py +0 -0
  31. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/agents/__init__.py +0 -0
  32. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/agents/agent_connections.py +0 -0
  33. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/agents/availability.py +0 -0
  34. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/agents/heuristics.py +0 -0
  35. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/agents/recovery_tracker.py +0 -0
  36. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/agents/tool_call_tracker.py +0 -0
  37. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/agents/tracker.py +0 -0
  38. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/metrics/metrics.py +0 -0
  39. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/telemetry.py +0 -0
  40. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/tracing/__init__.py +0 -0
  41. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/tracing/content_allow_list.py +0 -0
  42. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/tracing/context_manager.py +0 -0
  43. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/tracing/context_utils.py +0 -0
  44. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/tracing/manual.py +0 -0
  45. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/tracing/tracing.py +0 -0
  46. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/tracing/transform_span.py +0 -0
  47. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/utils/__init__.py +0 -0
  48. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/utils/const.py +0 -0
  49. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/utils/in_memory_span_exporter.py +0 -0
  50. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/utils/json_encoder.py +0 -0
  51. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/utils/package_check.py +0 -0
  52. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe/sdk/version.py +0 -0
  53. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe_sdk.egg-info/dependency_links.txt +0 -0
  54. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/ioa_observe_sdk.egg-info/top_level.txt +0 -0
  55. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/setup.cfg +0 -0
  56. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/tests/test_client.py +0 -0
  57. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/tests/test_instrumentor.py +0 -0
  58. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/tests/test_manual_instrumentation.py +0 -0
  59. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/tests/test_transform_span.py +0 -0
  60. {ioa_observe_sdk-1.0.20 → ioa_observe_sdk-1.0.22}/tests/test_version.py +0 -0
@@ -1,20 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ioa-observe-sdk
3
- Version: 1.0.20
3
+ Version: 1.0.22
4
4
  Summary: IOA Observability SDK
5
+ License-Expression: Apache-2.0
5
6
  Requires-Python: >=3.10
6
7
  Description-Content-Type: text/markdown
7
8
  License-File: LICENSE.md
8
9
  Requires-Dist: colorama==0.4.6
9
10
  Requires-Dist: requests>=2.32.3
10
- Requires-Dist: opentelemetry-api==1.33.1
11
+ Requires-Dist: opentelemetry-api==1.37.0
11
12
  Requires-Dist: opentelemetry-distro
12
- Requires-Dist: opentelemetry-exporter-otlp==1.33.1
13
- Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.33.1
14
- Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.33.1
15
- Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.33.1
13
+ Requires-Dist: opentelemetry-exporter-otlp==1.37.0
14
+ Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.37.0
15
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.37.0
16
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.37.0
16
17
  Requires-Dist: opentelemetry-instrumentation
17
- Requires-Dist: opentelemetry-instrumentation-logging==0.54b1
18
+ Requires-Dist: opentelemetry-instrumentation-logging==0.58b0
18
19
  Requires-Dist: opentelemetry-instrumentation-openai==0.43.1
19
20
  Requires-Dist: opentelemetry-instrumentation-llamaindex==0.43.1
20
21
  Requires-Dist: opentelemetry-instrumentation-ollama==0.43.1
@@ -26,18 +27,18 @@ Requires-Dist: opentelemetry-instrumentation-crewai==0.43.1
26
27
  Requires-Dist: opentelemetry-instrumentation-google-generativeai==0.43.1
27
28
  Requires-Dist: opentelemetry-instrumentation-groq==0.43.1
28
29
  Requires-Dist: opentelemetry-instrumentation-mistralai==0.43.1
29
- Requires-Dist: opentelemetry-instrumentation-requests==0.54b1
30
+ Requires-Dist: opentelemetry-instrumentation-requests==0.58b0
30
31
  Requires-Dist: opentelemetry-instrumentation-sagemaker==0.43.1
31
- Requires-Dist: opentelemetry-instrumentation-threading==0.54b1
32
+ Requires-Dist: opentelemetry-instrumentation-threading==0.58b0
32
33
  Requires-Dist: opentelemetry-instrumentation-together==0.43.1
33
34
  Requires-Dist: opentelemetry-instrumentation-transformers==0.43.1
34
- Requires-Dist: opentelemetry-instrumentation-urllib3==0.54b1
35
+ Requires-Dist: opentelemetry-instrumentation-urllib3==0.58b0
35
36
  Requires-Dist: opentelemetry-instrumentation-vertexai==0.43.1
36
- Requires-Dist: opentelemetry-proto==1.33.1
37
- Requires-Dist: opentelemetry-sdk==1.33.1
38
- Requires-Dist: opentelemetry-semantic-conventions==0.54b1
37
+ Requires-Dist: opentelemetry-proto==1.37.0
38
+ Requires-Dist: opentelemetry-sdk==1.37.0
39
+ Requires-Dist: opentelemetry-semantic-conventions==0.58b0
39
40
  Requires-Dist: opentelemetry-semantic-conventions-ai>=0.4.11
40
- Requires-Dist: opentelemetry-util-http==0.54b1
41
+ Requires-Dist: opentelemetry-util-http==0.58b0
41
42
  Requires-Dist: langgraph>=0.3.2
42
43
  Requires-Dist: langchain>=0.3.19
43
44
  Requires-Dist: langchain-openai>=0.3.8
@@ -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__)