netra-sdk 0.1.29__py3-none-any.whl → 0.1.30__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.

Potentially problematic release.


This version of netra-sdk might be problematic. Click here for more details.

netra/__init__.py CHANGED
@@ -255,5 +255,30 @@ class Netra:
255
255
  """
256
256
  return SpanWrapper(name, attributes, module_name)
257
257
 
258
+ @classmethod
259
+ def set_input(cls, value: Any, span_name: Optional[str] = None) -> None:
260
+ """
261
+ Set custom attribute `netra.span.input` on a target span.
262
+
263
+ Args:
264
+ value: Input payload to record (string or JSON-serializable object)
265
+ span_name: Optional. When provided, sets the attribute on the span registered
266
+ with this name. Otherwise sets on the active span.
267
+ """
268
+ SessionManager.set_attribute_on_target_span(f"{Config.LIBRARY_NAME}.span.input", value, span_name)
269
+
270
+ @classmethod
271
+ def set_output(cls, value: Any, span_name: Optional[str] = None) -> None:
272
+ """
273
+ Set custom attribute `netra.span.output` on a target span.
274
+
275
+ Args:
276
+ value: Output payload to record (string or JSON-serializable object)
277
+ span_name: Optional. When provided, sets the attribute on the span registered
278
+ with this name. Otherwise sets on the active span.
279
+ """
280
+ if value:
281
+ SessionManager.set_attribute_on_target_span(f"{Config.LIBRARY_NAME}.span.output", value, span_name)
282
+
258
283
 
259
284
  __all__ = ["Netra", "UsageModel", "ActionModel"]
netra/config.py CHANGED
@@ -25,6 +25,8 @@ class Config:
25
25
  SDK_NAME = "netra"
26
26
  LIBRARY_NAME = "netra"
27
27
  LIBRARY_VERSION = __version__
28
+ # Maximum length for any attribute value (strings and bytes). Processors should honor this.
29
+ ATTRIBUTE_MAX_LEN = 1000
28
30
 
29
31
  def __init__(
30
32
  self,
netra/decorators.py CHANGED
@@ -7,6 +7,7 @@ Decorators can be applied to both functions and classes.
7
7
  import functools
8
8
  import inspect
9
9
  import json
10
+ import logging
10
11
  from typing import Any, Awaitable, Callable, Dict, Optional, ParamSpec, Tuple, TypeVar, Union, cast
11
12
 
12
13
  from opentelemetry import trace
@@ -14,6 +15,8 @@ from opentelemetry import trace
14
15
  from .config import Config
15
16
  from .session_manager import SessionManager
16
17
 
18
+ logger = logging.getLogger(__name__)
19
+
17
20
  P = ParamSpec("P")
18
21
  R = TypeVar("R")
19
22
 
@@ -84,6 +87,13 @@ def _create_function_wrapper(func: Callable[P, R], entity_type: str, name: Optio
84
87
 
85
88
  tracer = trace.get_tracer(module_name)
86
89
  with tracer.start_as_current_span(span_name) as span:
90
+ # Register the span by name for cross-context attribute setting
91
+ try:
92
+ SessionManager.register_span(span_name, span)
93
+ SessionManager.set_current_span(span)
94
+ except Exception:
95
+ logger.exception("Failed to register span '%s' with SessionManager", span_name)
96
+
87
97
  _add_span_attributes(span, func, args, kwargs, entity_type)
88
98
  try:
89
99
  result = await cast(Awaitable[Any], func(*args, **kwargs))
@@ -93,7 +103,11 @@ def _create_function_wrapper(func: Callable[P, R], entity_type: str, name: Optio
93
103
  span.set_attribute(f"{Config.LIBRARY_NAME}.entity.error", str(e))
94
104
  raise
95
105
  finally:
96
- # Pop entity from stack after function call is done
106
+ # Unregister and pop entity from stack after function call is done
107
+ try:
108
+ SessionManager.unregister_span(span_name, span)
109
+ except Exception:
110
+ logger.exception("Failed to unregister span '%s' from SessionManager", span_name)
97
111
  SessionManager.pop_entity(entity_type)
98
112
 
99
113
  return cast(Callable[P, R], async_wrapper)
@@ -107,6 +121,13 @@ def _create_function_wrapper(func: Callable[P, R], entity_type: str, name: Optio
107
121
 
108
122
  tracer = trace.get_tracer(module_name)
109
123
  with tracer.start_as_current_span(span_name) as span:
124
+ # Register the span by name for cross-context attribute setting
125
+ try:
126
+ SessionManager.register_span(span_name, span)
127
+ SessionManager.set_current_span(span)
128
+ except Exception:
129
+ logger.exception("Failed to register span '%s' with SessionManager", span_name)
130
+
110
131
  _add_span_attributes(span, func, args, kwargs, entity_type)
111
132
  try:
112
133
  result = func(*args, **kwargs)
@@ -116,7 +137,11 @@ def _create_function_wrapper(func: Callable[P, R], entity_type: str, name: Optio
116
137
  span.set_attribute(f"{Config.LIBRARY_NAME}.entity.error", str(e))
117
138
  raise
118
139
  finally:
119
- # Pop entity from stack after function call is done
140
+ # Unregister and pop entity from stack after function call is done
141
+ try:
142
+ SessionManager.unregister_span(span_name, span)
143
+ except Exception:
144
+ logger.exception("Failed to unregister span '%s' from SessionManager", span_name)
120
145
  SessionManager.pop_entity(entity_type)
121
146
 
122
147
  return cast(Callable[P, R], sync_wrapper)
@@ -1,3 +1,4 @@
1
+ from netra.processors.instrumentation_span_processor import InstrumentationSpanProcessor
1
2
  from netra.processors.session_span_processor import SessionSpanProcessor
2
3
 
3
- __all__ = ["SessionSpanProcessor"]
4
+ __all__ = ["SessionSpanProcessor", "InstrumentationSpanProcessor"]
@@ -0,0 +1,102 @@
1
+ import logging
2
+ from typing import Any, Callable, Optional
3
+
4
+ from opentelemetry import context as otel_context
5
+ from opentelemetry import trace
6
+ from opentelemetry.sdk.trace import SpanProcessor
7
+
8
+ from netra.config import Config
9
+ from netra.instrumentation.instruments import InstrumentSet
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ ALLOWED_INSTRUMENTATION_NAMES = {member.value for member in InstrumentSet} # type: ignore[attr-defined]
15
+
16
+
17
+ class InstrumentationSpanProcessor(SpanProcessor): # type: ignore[misc]
18
+ """Span processor to record this span's instrumentation name and wrap set_attribute.
19
+
20
+ - Records raw instrumentation scope name for the span
21
+ - Wraps span.set_attribute to truncate string values to max 1000 chars (also inside simple lists/dicts of strings)
22
+ """
23
+
24
+ def __init__(self) -> None:
25
+ super().__init__()
26
+
27
+ def _detect_raw_instrumentation_name(self, span: trace.Span) -> Optional[str]:
28
+ """Detect the raw instrumentation name for the span."""
29
+ scope = getattr(span, "instrumentation_info", None)
30
+ if scope is not None:
31
+ name = getattr(scope, "name", None)
32
+ if isinstance(name, str) and name:
33
+ # Normalize common pattern like 'opentelemetry.instrumentation.httpx' -> 'httpx'
34
+ try:
35
+ base = name.rsplit(".", 1)[-1].strip()
36
+ if base:
37
+ return base
38
+ except Exception:
39
+ pass
40
+ return name
41
+ return None
42
+
43
+ def _truncate_value(self, value: Any) -> Any:
44
+ """Truncate string values to max 1000 chars (also inside simple lists/dicts of strings)."""
45
+ try:
46
+ if isinstance(value, str):
47
+ return value if len(value) <= Config.ATTRIBUTE_MAX_LEN else value[: Config.ATTRIBUTE_MAX_LEN]
48
+ if isinstance(value, (bytes, bytearray)):
49
+ return value[: Config.ATTRIBUTE_MAX_LEN]
50
+ if isinstance(value, list):
51
+ # Shallow truncate strings inside lists
52
+ return [self._truncate_value(v) if isinstance(v, (str, bytes, bytearray)) else v for v in value]
53
+ if isinstance(value, dict):
54
+ # Shallow truncate strings inside dicts
55
+ return {
56
+ k: self._truncate_value(v) if isinstance(v, (str, bytes, bytearray)) else v
57
+ for k, v in value.items()
58
+ }
59
+ except Exception:
60
+ return value
61
+ return value
62
+
63
+ def on_start(self, span: trace.Span, parent_context: Optional[otel_context.Context] = None) -> None:
64
+ """Start span and wrap set_attribute."""
65
+ try:
66
+ # Wrap set_attribute first so subsequent sets are also processed
67
+ original_set_attribute: Callable[[str, Any], None] = span.set_attribute
68
+
69
+ def wrapped_set_attribute(key: str, value: Any) -> None:
70
+ try:
71
+ # Truncate value(s)
72
+ truncated = self._truncate_value(value)
73
+ # Forward to original
74
+ original_set_attribute(key, truncated)
75
+ # Special rule: if model key set, mark span as llm
76
+ if key == "gen_ai.request.model":
77
+ original_set_attribute(f"{Config.LIBRARY_NAME}.span.type", "llm")
78
+ except Exception:
79
+ # Best-effort; never break span
80
+ try:
81
+ original_set_attribute(key, value)
82
+ except Exception:
83
+ pass
84
+
85
+ # Monkey patch for this span's lifetime
86
+ setattr(span, "set_attribute", wrapped_set_attribute)
87
+
88
+ # Set this span's instrumentation name
89
+ name = self._detect_raw_instrumentation_name(span)
90
+ if name and any(allowed in name for allowed in ALLOWED_INSTRUMENTATION_NAMES):
91
+ span.set_attribute(f"{Config.LIBRARY_NAME}.instrumentation.name", name)
92
+ except Exception:
93
+ pass
94
+
95
+ def on_end(self, span: trace.Span) -> None:
96
+ """End span."""
97
+
98
+ def force_flush(self, timeout_millis: int = 30000) -> None:
99
+ """Force flush span."""
100
+
101
+ def shutdown(self) -> None:
102
+ pass
netra/session_manager.py CHANGED
@@ -28,6 +28,9 @@ class SessionManager:
28
28
  _agent_stack: List[str] = []
29
29
  _span_stack: List[str] = []
30
30
 
31
+ # Span registry: name -> stack of spans (most-recent last)
32
+ _spans_by_name: Dict[str, List[trace.Span]] = {}
33
+
31
34
  @classmethod
32
35
  def set_current_span(cls, span: Optional[trace.Span]) -> None:
33
36
  """
@@ -48,6 +51,49 @@ class SessionManager:
48
51
  """
49
52
  return cls._current_span
50
53
 
54
+ @classmethod
55
+ def register_span(cls, name: str, span: trace.Span) -> None:
56
+ """
57
+ Register a span under a given name. Supports nested spans with the same name via a stack.
58
+ """
59
+ try:
60
+ stack = cls._spans_by_name.get(name)
61
+ if stack is None:
62
+ cls._spans_by_name[name] = [span]
63
+ else:
64
+ stack.append(span)
65
+ except Exception:
66
+ logger.exception("Failed to register span '%s'", name)
67
+
68
+ @classmethod
69
+ def unregister_span(cls, name: str, span: trace.Span) -> None:
70
+ """
71
+ Unregister a span for a given name. Safe if not present.
72
+ """
73
+ try:
74
+ stack = cls._spans_by_name.get(name)
75
+ if not stack:
76
+ return
77
+ # Remove the last matching instance (normal case)
78
+ for i in range(len(stack) - 1, -1, -1):
79
+ if stack[i] is span:
80
+ stack.pop(i)
81
+ break
82
+ if not stack:
83
+ cls._spans_by_name.pop(name, None)
84
+ except Exception:
85
+ logger.exception("Failed to unregister span '%s'", name)
86
+
87
+ @classmethod
88
+ def get_span_by_name(cls, name: str) -> Optional[trace.Span]:
89
+ """
90
+ Get the most recently registered span with the given name.
91
+ """
92
+ stack = cls._spans_by_name.get(name)
93
+ if stack:
94
+ return stack[-1]
95
+ return None
96
+
51
97
  @classmethod
52
98
  def push_entity(cls, entity_type: str, entity_name: str) -> None:
53
99
  """
@@ -190,3 +236,45 @@ class SessionManager:
190
236
  span.add_event(name=name, attributes=attributes, timestamp=timestamp_ns)
191
237
  except Exception as e:
192
238
  logger.exception(f"Failed to add custom event: {name} - {e}")
239
+
240
+ @classmethod
241
+ def set_attribute_on_target_span(cls, attr_key: str, attr_value: Any, span_name: Optional[str] = None) -> None:
242
+ """
243
+ Best-effort setter to annotate the active span with the provided attribute.
244
+
245
+ If span_name is provided, we look up that span via SessionManager's registry and set
246
+ the attribute on that span explicitly. Otherwise, we annotate the active span.
247
+ We first try the OpenTelemetry current span; if that's invalid, we fall back to
248
+ the SDK-managed current span from `SessionManager`.
249
+ """
250
+ try:
251
+ # Convert attribute value to a JSON-safe string representation
252
+ try:
253
+ if isinstance(attr_value, str):
254
+ attr_str = attr_value
255
+ else:
256
+ import json
257
+
258
+ attr_str = json.dumps(attr_value)
259
+ except Exception:
260
+ attr_str = str(attr_value)
261
+
262
+ # If a target span name is provided, use the registry for explicit lookup
263
+ if span_name is not None:
264
+ target = cls.get_span_by_name(span_name)
265
+ if target is None:
266
+ logger.debug("No span found with name '%s' to set attribute %s", span_name, attr_key)
267
+ return
268
+ target.set_attribute(attr_key, attr_str)
269
+ return
270
+
271
+ # Otherwise annotate the active span
272
+ current_span = trace.get_current_span()
273
+ has_valid_current = getattr(current_span, "is_recording", None) is not None and current_span.is_recording()
274
+ candidate = current_span if has_valid_current else cls.get_current_span()
275
+ if candidate is None:
276
+ logger.debug("No active span found to set attribute %s", attr_key)
277
+ return
278
+ candidate.set_attribute(attr_key, attr_str)
279
+ except Exception as e:
280
+ logger.exception("Failed setting attribute %s: %s", attr_key, e)
netra/span_wrapper.py CHANGED
@@ -11,6 +11,7 @@ from opentelemetry.trace.propagation import set_span_in_context
11
11
  from pydantic import BaseModel
12
12
 
13
13
  from netra.config import Config
14
+ from netra.session_manager import SessionManager
14
15
 
15
16
  # Configure logging
16
17
  logging.basicConfig(level=logging.INFO)
@@ -84,6 +85,14 @@ class SpanWrapper:
84
85
  ctx = set_span_in_context(self.span)
85
86
  self.context_token = context_api.attach(ctx)
86
87
 
88
+ # Register with SessionManager for name-based lookup
89
+ try:
90
+ SessionManager.register_span(self.name, self.span)
91
+ # Optionally set as current span for SDK consumers that rely on it
92
+ SessionManager.set_current_span(self.span)
93
+ except Exception:
94
+ logger.exception("Failed to register span '%s' with SessionManager", self.name)
95
+
87
96
  logger.info(f"Started span wrapper: {self.name}")
88
97
  return self
89
98
 
@@ -120,6 +129,11 @@ class SpanWrapper:
120
129
 
121
130
  # End OpenTelemetry span and detach context
122
131
  if self.span:
132
+ # Unregister from SessionManager before ending span
133
+ try:
134
+ SessionManager.unregister_span(self.name, self.span)
135
+ except Exception:
136
+ logger.exception("Failed to unregister span '%s' from SessionManager", self.name)
123
137
  self.span.end()
124
138
  if self.context_token:
125
139
  context_api.detach(self.context_token)
netra/tracer.py CHANGED
@@ -65,9 +65,10 @@ class Tracer:
65
65
  endpoint=self._format_endpoint(self.cfg.otlp_endpoint),
66
66
  headers=self.cfg.headers,
67
67
  )
68
- # Add span processors for session span processing and data aggregation processing
69
- from netra.processors import SessionSpanProcessor
68
+ # Add span processors: first instrumentation wrapper, then session processor
69
+ from netra.processors import InstrumentationSpanProcessor, SessionSpanProcessor
70
70
 
71
+ provider.add_span_processor(InstrumentationSpanProcessor())
71
72
  provider.add_span_processor(SessionSpanProcessor())
72
73
 
73
74
  # Install appropriate span processor
netra/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.29"
1
+ __version__ = "0.1.30"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: netra-sdk
3
- Version: 0.1.29
3
+ Version: 0.1.30
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
@@ -1,10 +1,10 @@
1
- netra/__init__.py,sha256=EIMbq0ycEIolmyk7uGXkmgvydfWArNk-soBIEQijsiY,9612
1
+ netra/__init__.py,sha256=4eqBwPZP86tsxnUJC-Q7dEVpFAIoZcp5vgmOyKwQslo,10740
2
2
  netra/anonymizer/__init__.py,sha256=KeGPPZqKVZbtkbirEKYTYhj6aZHlakjdQhD7QHqBRio,133
3
3
  netra/anonymizer/anonymizer.py,sha256=IcrYkdwWrFauGWUeAW-0RwrSUM8VSZCFNtoywZhvIqU,3778
4
4
  netra/anonymizer/base.py,sha256=ytPxHCUD2OXlEY6fNTuMmwImNdIjgj294I41FIgoXpU,5946
5
5
  netra/anonymizer/fp_anonymizer.py,sha256=_6svIYmE0eejdIMkhKBUWCNjGtGimtrGtbLvPSOp8W4,6493
6
- netra/config.py,sha256=zmTssfUExG8viSMKN9Yfd8i--7S4oUQT8IyvwAI4C2U,5950
7
- netra/decorators.py,sha256=TqORBs0Xv_CZHOAwrWkpvscMj4FgOJsgo1CRYoG6C7Y,7435
6
+ netra/config.py,sha256=-_2iXoMP92pifMFLOt1rjpRvEnqHWc3O9usFIGhxnwA,6075
7
+ netra/decorators.py,sha256=yuQP02sdvTRIYkv-myNcP8q7dmPq3ME1AJZxJtryayI,8720
8
8
  netra/exceptions/__init__.py,sha256=uDgcBxmC4WhdS7HRYQk_TtJyxH1s1o6wZmcsnSHLAcM,174
9
9
  netra/exceptions/injection.py,sha256=ke4eUXRYUFJkMZgdSyPPkPt5PdxToTI6xLEBI0hTWUQ,1332
10
10
  netra/exceptions/pii.py,sha256=MT4p_x-zH3VtYudTSxw1Z9qQZADJDspq64WrYqSWlZc,2438
@@ -37,14 +37,15 @@ netra/instrumentation/pydantic_ai/wrappers.py,sha256=6cfIRvELBS4d9G9TttNYcHGueNI
37
37
  netra/instrumentation/weaviate/__init__.py,sha256=EOlpWxobOLHYKqo_kMct_7nu26x1hr8qkeG5_h99wtg,4330
38
38
  netra/instrumentation/weaviate/version.py,sha256=PiCZHjonujPbnIn0KmD3Yl68hrjPRG_oKe5vJF3mmG8,24
39
39
  netra/pii.py,sha256=Rn4SjgTJW_aw9LcbjLuMqF3fKd9b1ndlYt1CaK51Ge0,33125
40
- netra/processors/__init__.py,sha256=wfnSskRBtMT90hO7LqFJoEW374LgoH_gnTxhynqtByI,109
40
+ netra/processors/__init__.py,sha256=2E18QGVwVl8nEpT-EViEZqGLdVVejMTEH1wZNugDmYU,230
41
+ netra/processors/instrumentation_span_processor.py,sha256=Ef5FTr8O5FLHcIkBAW3ueU1nlkV2DuOi-y5iIwHzldQ,4252
41
42
  netra/processors/session_span_processor.py,sha256=qcsBl-LnILWefsftI8NQhXDGb94OWPc8LvzhVA0JS_c,2432
42
43
  netra/scanner.py,sha256=kyDpeZiscCPb6pjuhS-sfsVj-dviBFRepdUWh0sLoEY,11554
43
- netra/session_manager.py,sha256=Ks8A6B9RYe0tV8PeWYuN0M-UMZqa47uquuw6D7C1vCE,6701
44
- netra/span_wrapper.py,sha256=ec2WLYTRLZ02WSSCYEsMn1PgUGVji9rFyq_CRCV9rog,7388
45
- netra/tracer.py,sha256=In5QPVLz_6BxrolWpav9EuR9_hirD2UUIlyY75QUaKk,3450
46
- netra/version.py,sha256=A-lFHZ4YpCrWZ6nw3tlt_yurFJ00mInm3gR6hz51Eww,23
47
- netra_sdk-0.1.29.dist-info/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
48
- netra_sdk-0.1.29.dist-info/METADATA,sha256=XKGT69ygEEA6hbSinO1heKikakeIngiMqotgwQbFyeQ,28151
49
- netra_sdk-0.1.29.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
50
- netra_sdk-0.1.29.dist-info/RECORD,,
44
+ netra/session_manager.py,sha256=AoQa-k4dFcq7PeOD8G8DNzhLzL1JrHUW6b_y8mRyTQo,10255
45
+ netra/span_wrapper.py,sha256=lGuV1F4Q5I_swIoIof5myzOQCFmGFdtrpgfQt7dTTus,8105
46
+ netra/tracer.py,sha256=YiuijB_5DBOLVgE39Lj3thWVmUqHLcqbdFVB0HGovW0,3543
47
+ netra/version.py,sha256=2GUJJyX8g8EAXKUqyj7DGVzG-jNXOGaqVSWilvGYuX8,23
48
+ netra_sdk-0.1.30.dist-info/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
49
+ netra_sdk-0.1.30.dist-info/METADATA,sha256=9quLP0B1XfOdcQWU4vsIY44Gf2CwWiF_er2-Wa6wKvM,28151
50
+ netra_sdk-0.1.30.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
51
+ netra_sdk-0.1.30.dist-info/RECORD,,