netra-sdk 0.1.29__tar.gz → 0.1.31__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.

Files changed (56) hide show
  1. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/PKG-INFO +2 -1
  2. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/README.md +1 -0
  3. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/__init__.py +25 -0
  4. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/config.py +2 -0
  5. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/decorators.py +27 -2
  6. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/__init__.py +24 -0
  7. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/instruments.py +2 -0
  8. netra_sdk-0.1.31/netra/instrumentation/litellm/__init__.py +161 -0
  9. netra_sdk-0.1.31/netra/instrumentation/litellm/wrappers.py +557 -0
  10. netra_sdk-0.1.31/netra/instrumentation/pydantic_ai/version.py +1 -0
  11. netra_sdk-0.1.31/netra/processors/__init__.py +4 -0
  12. netra_sdk-0.1.31/netra/processors/instrumentation_span_processor.py +102 -0
  13. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/session_manager.py +88 -0
  14. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/span_wrapper.py +14 -0
  15. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/tracer.py +3 -2
  16. netra_sdk-0.1.31/netra/version.py +1 -0
  17. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/pyproject.toml +1 -1
  18. netra_sdk-0.1.29/netra/processors/__init__.py +0 -3
  19. netra_sdk-0.1.29/netra/version.py +0 -1
  20. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/LICENCE +0 -0
  21. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/anonymizer/__init__.py +0 -0
  22. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/anonymizer/anonymizer.py +0 -0
  23. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/anonymizer/base.py +0 -0
  24. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/anonymizer/fp_anonymizer.py +0 -0
  25. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/exceptions/__init__.py +0 -0
  26. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/exceptions/injection.py +0 -0
  27. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/exceptions/pii.py +0 -0
  28. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/input_scanner.py +0 -0
  29. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/aiohttp/__init__.py +0 -0
  30. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/aiohttp/version.py +0 -0
  31. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/cohere/__init__.py +0 -0
  32. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/cohere/version.py +0 -0
  33. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/fastapi/__init__.py +0 -0
  34. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/fastapi/version.py +0 -0
  35. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/google_genai/__init__.py +0 -0
  36. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/google_genai/config.py +0 -0
  37. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/google_genai/utils.py +0 -0
  38. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/google_genai/version.py +0 -0
  39. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/httpx/__init__.py +0 -0
  40. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/httpx/version.py +0 -0
  41. {netra_sdk-0.1.29/netra/instrumentation/pydantic_ai → netra_sdk-0.1.31/netra/instrumentation/litellm}/version.py +0 -0
  42. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/mistralai/__init__.py +0 -0
  43. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/mistralai/config.py +0 -0
  44. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/mistralai/utils.py +0 -0
  45. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/mistralai/version.py +0 -0
  46. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/openai/__init__.py +0 -0
  47. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/openai/version.py +0 -0
  48. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/openai/wrappers.py +0 -0
  49. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/pydantic_ai/__init__.py +0 -0
  50. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/pydantic_ai/utils.py +0 -0
  51. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/pydantic_ai/wrappers.py +0 -0
  52. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/weaviate/__init__.py +0 -0
  53. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/instrumentation/weaviate/version.py +0 -0
  54. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/pii.py +0 -0
  55. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/processors/session_span_processor.py +0 -0
  56. {netra_sdk-0.1.29 → netra_sdk-0.1.31}/netra/scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: netra-sdk
3
- Version: 0.1.29
3
+ Version: 0.1.31
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
@@ -303,6 +303,7 @@ async def async_span(data):
303
303
  - **CrewAI** - Multi-agent AI systems
304
304
  - **Pydantic AI** - AI model communication standard
305
305
  - **MCP (Model Context Protocol)** - AI model communication standard
306
+ - **LiteLLM** - LLM provider agnostic client
306
307
 
307
308
  ## 🛡️ Privacy Protection & Security
308
309
 
@@ -223,6 +223,7 @@ async def async_span(data):
223
223
  - **CrewAI** - Multi-agent AI systems
224
224
  - **Pydantic AI** - AI model communication standard
225
225
  - **MCP (Model Context Protocol)** - AI model communication standard
226
+ - **LiteLLM** - LLM provider agnostic client
226
227
 
227
228
  ## 🛡️ Privacy Protection & Security
228
229
 
@@ -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"]
@@ -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,
@@ -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)
@@ -93,6 +93,10 @@ def init_instrumentations(
93
93
  if CustomInstruments.MISTRALAI in netra_custom_instruments:
94
94
  init_mistral_instrumentor()
95
95
 
96
+ # Initialize LiteLLM instrumentation.
97
+ if CustomInstruments.LITELLM in netra_custom_instruments:
98
+ init_litellm_instrumentation()
99
+
96
100
  # Initialize OpenAI instrumentation.
97
101
  if CustomInstruments.OPENAI in netra_custom_instruments:
98
102
  init_openai_instrumentation()
@@ -435,6 +439,26 @@ def init_mistral_instrumentor() -> bool:
435
439
  return False
436
440
 
437
441
 
442
+ def init_litellm_instrumentation() -> bool:
443
+ """Initialize LiteLLM instrumentation.
444
+
445
+ Returns:
446
+ bool: True if initialization was successful, False otherwise.
447
+ """
448
+ try:
449
+ if is_package_installed("litellm"):
450
+ from netra.instrumentation.litellm import LiteLLMInstrumentor
451
+
452
+ instrumentor = LiteLLMInstrumentor()
453
+ if not instrumentor.is_instrumented_by_opentelemetry:
454
+ instrumentor.instrument()
455
+ return True
456
+ except Exception as e:
457
+ logging.error(f"Error initializing LiteLLM instrumentor: {e}")
458
+ Telemetry().log_exception(e)
459
+ return False
460
+
461
+
438
462
  def init_openai_instrumentation() -> bool:
439
463
  """Initialize OpenAI instrumentation.
440
464
 
@@ -8,6 +8,7 @@ class CustomInstruments(Enum):
8
8
  AIOHTTP = "aiohttp"
9
9
  COHEREAI = "cohere_ai"
10
10
  HTTPX = "httpx"
11
+ LITELLM = "litellm"
11
12
  MISTRALAI = "mistral_ai"
12
13
  OPENAI = "openai"
13
14
  PYDANTIC_AI = "pydantic_ai"
@@ -127,6 +128,7 @@ class InstrumentSet(Enum):
127
128
  KAFKA_PYTHON = "kafka_python"
128
129
  LANCEDB = "lancedb"
129
130
  LANGCHAIN = "langchain"
131
+ LITELLM = "litellm"
130
132
  LLAMA_INDEX = "llama_index"
131
133
  LOGGING = "logging"
132
134
  MARQO = "marqo"
@@ -0,0 +1,161 @@
1
+ import logging
2
+ import time
3
+ from typing import Any, Collection, Dict, Optional
4
+
5
+ from opentelemetry import context as context_api
6
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
7
+ from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
8
+ from opentelemetry.trace import SpanKind, Tracer, get_tracer
9
+ from opentelemetry.trace.status import Status, StatusCode
10
+ from wrapt import wrap_function_wrapper
11
+
12
+ from netra.instrumentation.litellm.version import __version__
13
+ from netra.instrumentation.litellm.wrappers import (
14
+ acompletion_wrapper,
15
+ aembedding_wrapper,
16
+ aimage_generation_wrapper,
17
+ completion_wrapper,
18
+ embedding_wrapper,
19
+ image_generation_wrapper,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _instruments = ("litellm >= 1.0.0",)
25
+
26
+
27
+ class LiteLLMInstrumentor(BaseInstrumentor): # type: ignore[misc]
28
+ """
29
+ Custom LiteLLM instrumentor for Netra SDK with enhanced support for:
30
+ - completion() and acompletion() methods
31
+ - embedding() and aembedding() methods
32
+ - image_generation() and aimage_generation() methods
33
+ - Proper streaming/non-streaming span handling
34
+ - Integration with Netra tracing
35
+ """
36
+
37
+ def instrumentation_dependencies(self) -> Collection[str]:
38
+ return _instruments
39
+
40
+ def _instrument(self, **kwargs): # type: ignore[no-untyped-def]
41
+ """Instrument LiteLLM methods"""
42
+ tracer_provider = kwargs.get("tracer_provider")
43
+ tracer = get_tracer(__name__, __version__, tracer_provider)
44
+
45
+ logger.debug("Starting LiteLLM instrumentation...")
46
+
47
+ # Force import litellm to ensure it's available for wrapping
48
+ try:
49
+ import litellm
50
+ except ImportError as e:
51
+ logger.error(f"Failed to import litellm: {e}")
52
+ return
53
+
54
+ # Store original functions for uninstrumentation
55
+ self._original_completion = getattr(litellm, "completion", None)
56
+ self._original_acompletion = getattr(litellm, "acompletion", None)
57
+ self._original_embedding = getattr(litellm, "embedding", None)
58
+ self._original_aembedding = getattr(litellm, "aembedding", None)
59
+ self._original_image_generation = getattr(litellm, "image_generation", None)
60
+ self._original_aimage_generation = getattr(litellm, "aimage_generation", None)
61
+
62
+ # Chat completions - use direct monkey patching with proper function wrapping
63
+ if self._original_completion:
64
+ try:
65
+
66
+ def instrumented_completion(*args, **kwargs): # type: ignore[no-untyped-def]
67
+ wrapper = completion_wrapper(tracer)
68
+ return wrapper(self._original_completion, None, args, kwargs)
69
+
70
+ litellm.completion = instrumented_completion
71
+ except Exception as e:
72
+ logger.error(f"Failed to monkey-patch litellm.completion: {e}")
73
+
74
+ if self._original_acompletion:
75
+ try:
76
+
77
+ async def instrumented_acompletion(*args, **kwargs): # type: ignore[no-untyped-def]
78
+ wrapper = acompletion_wrapper(tracer)
79
+ return await wrapper(self._original_acompletion, None, args, kwargs)
80
+
81
+ litellm.acompletion = instrumented_acompletion
82
+ except Exception as e:
83
+ logger.error(f"Failed to monkey-patch litellm.acompletion: {e}")
84
+
85
+ # Embeddings
86
+ if self._original_embedding:
87
+ try:
88
+
89
+ def instrumented_embedding(*args, **kwargs): # type: ignore[no-untyped-def]
90
+ wrapper = embedding_wrapper(tracer)
91
+ return wrapper(self._original_embedding, None, args, kwargs)
92
+
93
+ litellm.embedding = instrumented_embedding
94
+ except Exception as e:
95
+ logger.error(f"Failed to monkey-patch litellm.embedding: {e}")
96
+
97
+ if self._original_aembedding:
98
+ try:
99
+
100
+ async def instrumented_aembedding(*args, **kwargs): # type: ignore[no-untyped-def]
101
+ wrapper = aembedding_wrapper(tracer)
102
+ return await wrapper(self._original_aembedding, None, args, kwargs)
103
+
104
+ litellm.aembedding = instrumented_aembedding
105
+ except Exception as e:
106
+ logger.error(f"Failed to monkey-patch litellm.aembedding: {e}")
107
+
108
+ # Image generation
109
+ if self._original_image_generation:
110
+ try:
111
+
112
+ def instrumented_image_generation(*args, **kwargs): # type: ignore[no-untyped-def]
113
+ wrapper = image_generation_wrapper(tracer)
114
+ return wrapper(self._original_image_generation, None, args, kwargs)
115
+
116
+ litellm.image_generation = instrumented_image_generation
117
+ except Exception as e:
118
+ logger.error(f"Failed to monkey-patch litellm.image_generation: {e}")
119
+
120
+ if self._original_aimage_generation:
121
+ try:
122
+
123
+ async def instrumented_aimage_generation(*args, **kwargs): # type: ignore[no-untyped-def]
124
+ wrapper = aimage_generation_wrapper(tracer)
125
+ return await wrapper(self._original_aimage_generation, None, args, kwargs)
126
+
127
+ litellm.aimage_generation = instrumented_aimage_generation
128
+ except Exception as e:
129
+ logger.error(f"Failed to monkey-patch litellm.aimage_generation: {e}")
130
+
131
+ def _uninstrument(self, **kwargs): # type: ignore[no-untyped-def]
132
+ """Uninstrument LiteLLM methods"""
133
+ try:
134
+ import litellm
135
+
136
+ # Restore original functions
137
+ if hasattr(self, "_original_completion") and self._original_completion:
138
+ litellm.completion = self._original_completion
139
+
140
+ if hasattr(self, "_original_acompletion") and self._original_acompletion:
141
+ litellm.acompletion = self._original_acompletion
142
+
143
+ if hasattr(self, "_original_embedding") and self._original_embedding:
144
+ litellm.embedding = self._original_embedding
145
+
146
+ if hasattr(self, "_original_aembedding") and self._original_aembedding:
147
+ litellm.aembedding = self._original_aembedding
148
+
149
+ if hasattr(self, "_original_image_generation") and self._original_image_generation:
150
+ litellm.image_generation = self._original_image_generation
151
+
152
+ if hasattr(self, "_original_aimage_generation") and self._original_aimage_generation:
153
+ litellm.aimage_generation = self._original_aimage_generation
154
+
155
+ except ImportError:
156
+ pass
157
+
158
+
159
+ def should_suppress_instrumentation() -> bool:
160
+ """Check if instrumentation should be suppressed"""
161
+ return context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) is True