netra-sdk 0.1.36__tar.gz → 0.1.38__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.
Potentially problematic release.
This version of netra-sdk might be problematic. Click here for more details.
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/PKG-INFO +1 -1
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/__init__.py +3 -2
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/litellm/wrappers.py +114 -102
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/processors/instrumentation_span_processor.py +11 -8
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/session_manager.py +56 -3
- netra_sdk-0.1.38/netra/version.py +1 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/pyproject.toml +1 -1
- netra_sdk-0.1.36/netra/version.py +0 -1
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/LICENCE +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/README.md +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/anonymizer/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/anonymizer/anonymizer.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/anonymizer/base.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/anonymizer/fp_anonymizer.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/config.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/decorators.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/exceptions/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/exceptions/injection.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/exceptions/pii.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/input_scanner.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/aiohttp/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/aiohttp/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/cohere/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/cohere/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/fastapi/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/fastapi/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/google_genai/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/google_genai/config.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/google_genai/utils.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/google_genai/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/httpx/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/httpx/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/instruments.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/litellm/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/litellm/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/mistralai/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/mistralai/config.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/mistralai/utils.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/mistralai/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/openai/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/openai/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/openai/wrappers.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/pydantic_ai/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/pydantic_ai/utils.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/pydantic_ai/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/pydantic_ai/wrappers.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/weaviate/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/instrumentation/weaviate/version.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/pii.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/processors/__init__.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/processors/scrubbing_span_processor.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/processors/session_span_processor.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/scanner.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/span_wrapper.py +0 -0
- {netra_sdk-0.1.36 → netra_sdk-0.1.38}/netra/tracer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: netra-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.38
|
|
4
4
|
Summary: A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: netra,tracing,observability,sdk,ai,llm,vector,database
|
|
@@ -222,16 +222,17 @@ class Netra:
|
|
|
222
222
|
@classmethod
|
|
223
223
|
def set_custom_attributes(cls, key: str, value: Any) -> None:
|
|
224
224
|
"""
|
|
225
|
-
Set custom
|
|
225
|
+
Set a custom attribute on the currently active OpenTelemetry span only.
|
|
226
226
|
|
|
227
227
|
Args:
|
|
228
228
|
key: Custom attribute key
|
|
229
229
|
value: Custom attribute value
|
|
230
230
|
"""
|
|
231
231
|
if key and value:
|
|
232
|
-
SessionManager.
|
|
232
|
+
SessionManager.set_attribute_on_active_span(f"{Config.LIBRARY_NAME}.custom.{key}", value)
|
|
233
233
|
else:
|
|
234
234
|
logger.warning("Both key and value must be provided for custom attributes.")
|
|
235
|
+
return
|
|
235
236
|
|
|
236
237
|
@classmethod
|
|
237
238
|
def set_custom_event(cls, event_name: str, attributes: Any) -> None:
|
|
@@ -47,115 +47,127 @@ def set_request_attributes(span: Span, kwargs: Dict[str, Any], operation_type: s
|
|
|
47
47
|
"""Set request attributes on span"""
|
|
48
48
|
if not span.is_recording():
|
|
49
49
|
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
50
|
+
try:
|
|
51
|
+
# Set operation type
|
|
52
|
+
span.set_attribute(f"{SpanAttributes.LLM_REQUEST_TYPE}", operation_type)
|
|
53
|
+
span.set_attribute(f"{SpanAttributes.LLM_SYSTEM}", "LiteLLM")
|
|
54
|
+
|
|
55
|
+
# Common attributes
|
|
56
|
+
if kwargs.get("model"):
|
|
57
|
+
span.set_attribute(f"{SpanAttributes.LLM_REQUEST_MODEL}", kwargs["model"])
|
|
58
|
+
|
|
59
|
+
if kwargs.get("temperature") is not None:
|
|
60
|
+
span.set_attribute(f"{SpanAttributes.LLM_REQUEST_TEMPERATURE}", kwargs["temperature"])
|
|
61
|
+
|
|
62
|
+
if kwargs.get("max_tokens") is not None:
|
|
63
|
+
span.set_attribute(f"{SpanAttributes.LLM_REQUEST_MAX_TOKENS}", kwargs["max_tokens"])
|
|
64
|
+
|
|
65
|
+
if kwargs.get("stream") is not None:
|
|
66
|
+
span.set_attribute("gen_ai.stream", kwargs["stream"])
|
|
67
|
+
|
|
68
|
+
# Chat completion specific attributes
|
|
69
|
+
if operation_type == "chat" and kwargs.get("messages"):
|
|
70
|
+
messages = kwargs["messages"]
|
|
71
|
+
if isinstance(messages, list) and len(messages) > 0:
|
|
72
|
+
for index, message in enumerate(messages):
|
|
73
|
+
if isinstance(message, dict):
|
|
74
|
+
span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{index}.role", message.get("role", "user"))
|
|
75
|
+
span.set_attribute(
|
|
76
|
+
f"{SpanAttributes.LLM_PROMPTS}.{index}.content", str(message.get("content", ""))
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Embedding specific attributes
|
|
80
|
+
if operation_type == "embedding" and kwargs.get("input"):
|
|
81
|
+
input_data = kwargs["input"]
|
|
82
|
+
if isinstance(input_data, str):
|
|
83
|
+
span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.0.content", input_data)
|
|
84
|
+
elif isinstance(input_data, list):
|
|
85
|
+
for index, text in enumerate(input_data):
|
|
86
|
+
if isinstance(text, str):
|
|
87
|
+
span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{index}.content", text)
|
|
88
|
+
|
|
89
|
+
# Image generation specific attributes
|
|
90
|
+
if operation_type == "image_generation":
|
|
91
|
+
if kwargs.get("prompt"):
|
|
92
|
+
span.set_attribute("gen_ai.prompt", kwargs["prompt"])
|
|
93
|
+
if kwargs.get("n"):
|
|
94
|
+
span.set_attribute("gen_ai.request.n", kwargs["n"])
|
|
95
|
+
if kwargs.get("size"):
|
|
96
|
+
span.set_attribute("gen_ai.request.size", kwargs["size"])
|
|
97
|
+
if kwargs.get("quality"):
|
|
98
|
+
span.set_attribute("gen_ai.request.quality", kwargs["quality"])
|
|
99
|
+
if kwargs.get("style"):
|
|
100
|
+
span.set_attribute("gen_ai.request.style", kwargs["style"])
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"Failed to set attributes for LiteLLM span: {e}")
|
|
99
103
|
|
|
100
104
|
|
|
101
105
|
def set_response_attributes(span: Span, response_dict: Dict[str, Any], operation_type: str) -> None:
|
|
102
106
|
"""Set response attributes on span"""
|
|
103
107
|
if not span.is_recording():
|
|
104
108
|
return
|
|
109
|
+
try:
|
|
110
|
+
if response_dict.get("model"):
|
|
111
|
+
span.set_attribute(f"{SpanAttributes.LLM_RESPONSE_MODEL}", response_dict["model"])
|
|
112
|
+
|
|
113
|
+
if response_dict.get("id"):
|
|
114
|
+
span.set_attribute("gen_ai.response.id", response_dict["id"])
|
|
115
|
+
|
|
116
|
+
# Usage information
|
|
117
|
+
usage = response_dict.get("usage", {})
|
|
118
|
+
if usage:
|
|
119
|
+
if usage.get("prompt_tokens"):
|
|
120
|
+
span.set_attribute(f"{SpanAttributes.LLM_USAGE_PROMPT_TOKENS}", usage["prompt_tokens"])
|
|
121
|
+
if usage.get("completion_tokens"):
|
|
122
|
+
span.set_attribute(f"{SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}", usage["completion_tokens"])
|
|
123
|
+
if usage.get("cache_read_input_tokens"):
|
|
124
|
+
span.set_attribute(
|
|
125
|
+
f"{SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS}", usage["cache_read_input_tokens"]
|
|
126
|
+
)
|
|
127
|
+
if usage.get("cache_creation_input_tokens"):
|
|
128
|
+
span.set_attribute("gen_ai.usage.cache_creation_input_tokens", usage["cache_creation_input_tokens"])
|
|
129
|
+
if usage.get("total_tokens"):
|
|
130
|
+
span.set_attribute(f"{SpanAttributes.LLM_USAGE_TOTAL_TOKENS}", usage["total_tokens"])
|
|
131
|
+
|
|
132
|
+
# Chat completion response content
|
|
133
|
+
if operation_type == "chat":
|
|
134
|
+
choices = response_dict.get("choices", [])
|
|
135
|
+
for index, choice in enumerate(choices):
|
|
136
|
+
if choice.get("message", {}).get("role"):
|
|
137
|
+
span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.{index}.role", choice["message"]["role"])
|
|
138
|
+
if choice.get("message", {}).get("content"):
|
|
139
|
+
span.set_attribute(
|
|
140
|
+
f"{SpanAttributes.LLM_COMPLETIONS}.{index}.content", choice["message"]["content"]
|
|
141
|
+
)
|
|
142
|
+
if choice.get("finish_reason"):
|
|
143
|
+
span.set_attribute(
|
|
144
|
+
f"{SpanAttributes.LLM_COMPLETIONS}.{index}.finish_reason", choice["finish_reason"]
|
|
145
|
+
)
|
|
105
146
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for
|
|
130
|
-
if choice.get("message", {}).get("role"):
|
|
131
|
-
span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.{index}.role", choice["message"]["role"])
|
|
132
|
-
if choice.get("message", {}).get("content"):
|
|
133
|
-
span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.{index}.content", choice["message"]["content"])
|
|
134
|
-
if choice.get("finish_reason"):
|
|
135
|
-
span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.{index}.finish_reason", choice["finish_reason"])
|
|
136
|
-
|
|
137
|
-
# Embedding response content
|
|
138
|
-
elif operation_type == "embedding":
|
|
139
|
-
data = response_dict.get("data", [])
|
|
140
|
-
for index, embedding_data in enumerate(data):
|
|
141
|
-
if embedding_data.get("index") is not None:
|
|
142
|
-
span.set_attribute(f"gen_ai.response.embeddings.{index}.index", embedding_data["index"])
|
|
143
|
-
if embedding_data.get("embedding"):
|
|
144
|
-
# Don't log the actual embedding vector, just its dimensions
|
|
145
|
-
embedding_vector = embedding_data["embedding"]
|
|
146
|
-
if isinstance(embedding_vector, list):
|
|
147
|
-
span.set_attribute(f"gen_ai.response.embeddings.{index}.dimensions", len(embedding_vector))
|
|
148
|
-
|
|
149
|
-
# Image generation response content
|
|
150
|
-
elif operation_type == "image_generation":
|
|
151
|
-
data = response_dict.get("data", [])
|
|
152
|
-
for index, image_data in enumerate(data):
|
|
153
|
-
if image_data.get("url"):
|
|
154
|
-
span.set_attribute(f"gen_ai.response.images.{index}.url", image_data["url"])
|
|
155
|
-
if image_data.get("b64_json"):
|
|
156
|
-
span.set_attribute(f"gen_ai.response.images.{index}.has_b64_json", True)
|
|
157
|
-
if image_data.get("revised_prompt"):
|
|
158
|
-
span.set_attribute(f"gen_ai.response.images.{index}.revised_prompt", image_data["revised_prompt"])
|
|
147
|
+
# Embedding response content
|
|
148
|
+
elif operation_type == "embedding":
|
|
149
|
+
data = response_dict.get("data", [])
|
|
150
|
+
for index, embedding_data in enumerate(data):
|
|
151
|
+
if embedding_data.get("index") is not None:
|
|
152
|
+
span.set_attribute(f"gen_ai.response.embeddings.{index}.index", embedding_data["index"])
|
|
153
|
+
if embedding_data.get("embedding"):
|
|
154
|
+
# Don't log the actual embedding vector, just its dimensions
|
|
155
|
+
embedding_vector = embedding_data["embedding"]
|
|
156
|
+
if isinstance(embedding_vector, list):
|
|
157
|
+
span.set_attribute(f"gen_ai.response.embeddings.{index}.dimensions", len(embedding_vector))
|
|
158
|
+
|
|
159
|
+
# Image generation response content
|
|
160
|
+
elif operation_type == "image_generation":
|
|
161
|
+
data = response_dict.get("data", [])
|
|
162
|
+
for index, image_data in enumerate(data):
|
|
163
|
+
if image_data.get("url"):
|
|
164
|
+
span.set_attribute(f"gen_ai.response.images.{index}.url", image_data["url"])
|
|
165
|
+
if image_data.get("b64_json"):
|
|
166
|
+
span.set_attribute(f"gen_ai.response.images.{index}.has_b64_json", True)
|
|
167
|
+
if image_data.get("revised_prompt"):
|
|
168
|
+
span.set_attribute(f"gen_ai.response.images.{index}.revised_prompt", image_data["revised_prompt"])
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to set attributes for LiteLLM span: {e}")
|
|
159
171
|
|
|
160
172
|
|
|
161
173
|
def completion_wrapper(tracer: Tracer) -> Callable[..., Any]:
|
|
@@ -30,13 +30,15 @@ class InstrumentationSpanProcessor(SpanProcessor): # type: ignore[misc]
|
|
|
30
30
|
if scope is not None:
|
|
31
31
|
name = getattr(scope, "name", None)
|
|
32
32
|
if isinstance(name, str) and name:
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
# Only truncate when coming from known namespaces
|
|
34
|
+
if name.startswith("opentelemetry.instrumentation.") or name.startswith("netra.instrumentation."):
|
|
35
|
+
try:
|
|
36
|
+
base = name.rsplit(".", 1)[-1].strip()
|
|
37
|
+
if base:
|
|
38
|
+
return base
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
# Otherwise, return as-is
|
|
40
42
|
return name
|
|
41
43
|
return None
|
|
42
44
|
|
|
@@ -87,7 +89,8 @@ class InstrumentationSpanProcessor(SpanProcessor): # type: ignore[misc]
|
|
|
87
89
|
|
|
88
90
|
# Set this span's instrumentation name
|
|
89
91
|
name = self._detect_raw_instrumentation_name(span)
|
|
90
|
-
|
|
92
|
+
print(name)
|
|
93
|
+
if name in ALLOWED_INSTRUMENTATION_NAMES:
|
|
91
94
|
span.set_attribute(f"{Config.LIBRARY_NAME}.instrumentation.name", name)
|
|
92
95
|
except Exception:
|
|
93
96
|
pass
|
|
@@ -196,9 +196,21 @@ class SessionManager:
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
@staticmethod
|
|
199
|
-
def set_session_context(
|
|
199
|
+
def set_session_context(
|
|
200
|
+
session_key: str,
|
|
201
|
+
value: Union[str, Dict[str, str]],
|
|
202
|
+
attach_globally: bool = False,
|
|
203
|
+
) -> None:
|
|
200
204
|
"""
|
|
201
|
-
Set session context attributes in
|
|
205
|
+
Set session context attributes in OpenTelemetry baggage.
|
|
206
|
+
|
|
207
|
+
Behavior:
|
|
208
|
+
- Adds values to baggage on the current context.
|
|
209
|
+
- To avoid context token mismatch errors, this method only attaches the
|
|
210
|
+
modified context when it is safe (no active recording span), unless
|
|
211
|
+
forced via attach_globally=True.
|
|
212
|
+
- When not attaching (e.g., inside an active span), it annotates the
|
|
213
|
+
current span with equivalent attributes for visibility.
|
|
202
214
|
|
|
203
215
|
Args:
|
|
204
216
|
session_key: Key to set in baggage (session_id, user_id, tenant_id, or custom_attributes)
|
|
@@ -219,7 +231,20 @@ class SessionManager:
|
|
|
219
231
|
ctx = baggage.set_baggage("custom_keys", ",".join(custom_keys), ctx)
|
|
220
232
|
for key, val in value.items():
|
|
221
233
|
ctx = baggage.set_baggage(f"custom.{key}", str(val), ctx)
|
|
222
|
-
|
|
234
|
+
|
|
235
|
+
# Decide whether to attach globally. We only attach if there is no
|
|
236
|
+
# active recording span (safe point) or if the caller forces it.
|
|
237
|
+
current_span = trace.get_current_span()
|
|
238
|
+
has_active_span = bool(current_span and getattr(current_span, "is_recording", lambda: False)())
|
|
239
|
+
if attach_globally or not has_active_span:
|
|
240
|
+
otel_context.attach(ctx)
|
|
241
|
+
else:
|
|
242
|
+
# Best-effort: annotate the current span for observability
|
|
243
|
+
if isinstance(value, str) and value:
|
|
244
|
+
current_span.set_attribute(f"{Config.LIBRARY_NAME}.session.{session_key}", value)
|
|
245
|
+
elif isinstance(value, dict) and value and session_key == "custom_attributes":
|
|
246
|
+
for key, val in value.items():
|
|
247
|
+
current_span.set_attribute(f"{Config.LIBRARY_NAME}.session.custom.{key}", str(val))
|
|
223
248
|
except Exception as e:
|
|
224
249
|
logger.exception(f"Failed to set session context for key={session_key}: {e}")
|
|
225
250
|
|
|
@@ -321,3 +346,31 @@ class SessionManager:
|
|
|
321
346
|
candidate.set_attribute(attr_key, attr_str)
|
|
322
347
|
except Exception as e:
|
|
323
348
|
logger.exception("Failed setting attribute %s: %s", attr_key, e)
|
|
349
|
+
|
|
350
|
+
@staticmethod
|
|
351
|
+
def set_attribute_on_active_span(attr_key: str, attr_value: Any) -> None:
|
|
352
|
+
"""
|
|
353
|
+
Set an attribute strictly on the currently active OpenTelemetry span.
|
|
354
|
+
|
|
355
|
+
- Does not fall back to any root span.
|
|
356
|
+
- Does not mutate global baggage/context.
|
|
357
|
+
- If no active recording span is present, logs a warning and returns.
|
|
358
|
+
"""
|
|
359
|
+
try:
|
|
360
|
+
span = trace.get_current_span()
|
|
361
|
+
if span and getattr(span, "is_recording", lambda: False)():
|
|
362
|
+
# Convert attr_value to a JSON-safe string if needed
|
|
363
|
+
try:
|
|
364
|
+
if isinstance(attr_value, str):
|
|
365
|
+
v = attr_value
|
|
366
|
+
else:
|
|
367
|
+
import json
|
|
368
|
+
|
|
369
|
+
v = json.dumps(attr_value)
|
|
370
|
+
except Exception:
|
|
371
|
+
v = str(attr_value)
|
|
372
|
+
span.set_attribute(attr_key, v)
|
|
373
|
+
else:
|
|
374
|
+
logger.warning("No active span to set attribute '%s'", attr_key)
|
|
375
|
+
except Exception:
|
|
376
|
+
logger.exception("Failed to set attribute '%s' on active span", attr_key)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.38"
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "netra-sdk"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.38"
|
|
8
8
|
description = "A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Sooraj Thomas",email = "sooraj@keyvalue.systems"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.36"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|