netra-sdk 0.1.0__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 +148 -0
- netra/anonymizer/__init__.py +7 -0
- netra/anonymizer/anonymizer.py +79 -0
- netra/anonymizer/base.py +159 -0
- netra/anonymizer/fp_anonymizer.py +182 -0
- netra/config.py +111 -0
- netra/decorators.py +167 -0
- netra/exceptions/__init__.py +6 -0
- netra/exceptions/injection.py +33 -0
- netra/exceptions/pii.py +46 -0
- netra/input_scanner.py +142 -0
- netra/instrumentation/__init__.py +257 -0
- netra/instrumentation/aiohttp/__init__.py +378 -0
- netra/instrumentation/aiohttp/version.py +1 -0
- netra/instrumentation/cohere/__init__.py +446 -0
- netra/instrumentation/cohere/version.py +1 -0
- netra/instrumentation/google_genai/__init__.py +506 -0
- netra/instrumentation/google_genai/config.py +5 -0
- netra/instrumentation/google_genai/utils.py +31 -0
- netra/instrumentation/google_genai/version.py +1 -0
- netra/instrumentation/httpx/__init__.py +545 -0
- netra/instrumentation/httpx/version.py +1 -0
- netra/instrumentation/instruments.py +78 -0
- netra/instrumentation/mistralai/__init__.py +545 -0
- netra/instrumentation/mistralai/config.py +5 -0
- netra/instrumentation/mistralai/utils.py +30 -0
- netra/instrumentation/mistralai/version.py +1 -0
- netra/instrumentation/weaviate/__init__.py +121 -0
- netra/instrumentation/weaviate/version.py +1 -0
- netra/pii.py +757 -0
- netra/processors/__init__.py +4 -0
- netra/processors/session_span_processor.py +55 -0
- netra/processors/span_aggregation_processor.py +365 -0
- netra/scanner.py +104 -0
- netra/session.py +185 -0
- netra/session_manager.py +96 -0
- netra/tracer.py +99 -0
- netra/version.py +1 -0
- netra_sdk-0.1.0.dist-info/LICENCE +201 -0
- netra_sdk-0.1.0.dist-info/METADATA +573 -0
- netra_sdk-0.1.0.dist-info/RECORD +42 -0
- netra_sdk-0.1.0.dist-info/WHEEL +4 -0
netra/decorators.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Netra decorator utilities.
|
|
2
|
+
|
|
3
|
+
This module provides decorators for common patterns in Netra SDK.
|
|
4
|
+
Decorators can be applied to both functions and classes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Awaitable, Callable, Dict, Optional, ParamSpec, Tuple, TypeVar, Union, cast
|
|
11
|
+
|
|
12
|
+
from opentelemetry import trace
|
|
13
|
+
|
|
14
|
+
from .config import Config
|
|
15
|
+
|
|
16
|
+
P = ParamSpec("P")
|
|
17
|
+
R = TypeVar("R")
|
|
18
|
+
|
|
19
|
+
F_Callable = TypeVar("F_Callable", bound=Callable[..., Any])
|
|
20
|
+
C = TypeVar("C", bound=type)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _serialize_value(value: Any) -> str:
|
|
24
|
+
"""Safely serialize a value to string for span attributes."""
|
|
25
|
+
try:
|
|
26
|
+
if isinstance(value, (str, int, float, bool, type(None))):
|
|
27
|
+
return str(value)
|
|
28
|
+
elif isinstance(value, (list, dict, tuple)):
|
|
29
|
+
return json.dumps(value, default=str)[:1000] # Limit size
|
|
30
|
+
else:
|
|
31
|
+
return str(value)[:1000] # Limit size
|
|
32
|
+
except Exception:
|
|
33
|
+
return str(type(value).__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _add_span_attributes(
|
|
37
|
+
span: trace.Span, func: Callable[..., Any], args: Tuple[Any, ...], kwargs: Dict[str, Any], entity_type: str
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Helper function to add span attributes from function parameters."""
|
|
40
|
+
span.set_attribute(f"{Config.LIBRARY_NAME}.entity.type", entity_type)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
sig = inspect.signature(func)
|
|
44
|
+
param_names = list(sig.parameters.keys())
|
|
45
|
+
input_data = {}
|
|
46
|
+
|
|
47
|
+
for i, arg in enumerate(args):
|
|
48
|
+
if i < len(param_names):
|
|
49
|
+
param_name = param_names[i]
|
|
50
|
+
if param_name not in ("self", "cls"):
|
|
51
|
+
input_data[param_name] = _serialize_value(arg)
|
|
52
|
+
|
|
53
|
+
for key, value in kwargs.items():
|
|
54
|
+
input_data[key] = _serialize_value(value)
|
|
55
|
+
|
|
56
|
+
if input_data:
|
|
57
|
+
span.set_attribute(f"{Config.LIBRARY_NAME}.entity.input", json.dumps(input_data))
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
span.set_attribute(f"{Config.LIBRARY_NAME}.input_error", str(e))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _add_output_attributes(span: trace.Span, result: Any) -> None:
|
|
64
|
+
"""Helper function to add output attributes to span."""
|
|
65
|
+
try:
|
|
66
|
+
serialized_output = _serialize_value(result)
|
|
67
|
+
span.set_attribute(f"{Config.LIBRARY_NAME}.entity.output", serialized_output)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
span.set_attribute(f"{Config.LIBRARY_NAME}.entity.output_error", str(e))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _create_function_wrapper(func: Callable[P, R], entity_type: str, name: Optional[str] = None) -> Callable[P, R]:
|
|
73
|
+
module_name = func.__name__
|
|
74
|
+
is_async = inspect.iscoroutinefunction(func)
|
|
75
|
+
span_name = name if name is not None else func.__name__
|
|
76
|
+
|
|
77
|
+
if is_async:
|
|
78
|
+
|
|
79
|
+
@functools.wraps(func)
|
|
80
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
81
|
+
tracer = trace.get_tracer(module_name)
|
|
82
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
83
|
+
_add_span_attributes(span, func, args, kwargs, entity_type)
|
|
84
|
+
try:
|
|
85
|
+
result = await cast(Awaitable[Any], func(*args, **kwargs))
|
|
86
|
+
_add_output_attributes(span, result)
|
|
87
|
+
return result
|
|
88
|
+
except Exception as e:
|
|
89
|
+
span.set_attribute(f"{Config.LIBRARY_NAME}.entity.error", str(e))
|
|
90
|
+
span.record_exception(e)
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
return cast(Callable[P, R], async_wrapper)
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
|
|
97
|
+
@functools.wraps(func)
|
|
98
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
99
|
+
tracer = trace.get_tracer(module_name)
|
|
100
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
101
|
+
_add_span_attributes(span, func, args, kwargs, entity_type)
|
|
102
|
+
try:
|
|
103
|
+
result = func(*args, **kwargs)
|
|
104
|
+
_add_output_attributes(span, result)
|
|
105
|
+
return result
|
|
106
|
+
except Exception as e:
|
|
107
|
+
span.set_attribute(f"{Config.LIBRARY_NAME}.entity.error", str(e))
|
|
108
|
+
span.record_exception(e)
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
return cast(Callable[P, R], sync_wrapper)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _wrap_class_methods(cls: C, entity_type: str, name: Optional[str] = None) -> C:
|
|
115
|
+
class_name = name if name is not None else cls.__name__
|
|
116
|
+
for attr_name in cls.__dict__:
|
|
117
|
+
attr = getattr(cls, attr_name)
|
|
118
|
+
if attr_name.startswith("_"):
|
|
119
|
+
continue
|
|
120
|
+
if callable(attr) and inspect.isfunction(attr):
|
|
121
|
+
method_span_name = f"{class_name}.{attr_name}"
|
|
122
|
+
wrapped_method = _create_function_wrapper(attr, entity_type, method_span_name)
|
|
123
|
+
setattr(cls, attr_name, wrapped_method)
|
|
124
|
+
return cls
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def workflow(
|
|
128
|
+
target: Union[Callable[P, R], C, None] = None, *, name: Optional[str] = None
|
|
129
|
+
) -> Union[Callable[P, R], C, Callable[[Callable[P, R]], Callable[P, R]]]:
|
|
130
|
+
def decorator(obj: Union[Callable[P, R], C]) -> Union[Callable[P, R], C]:
|
|
131
|
+
if inspect.isclass(obj):
|
|
132
|
+
return _wrap_class_methods(cast(C, obj), "workflow", name)
|
|
133
|
+
else:
|
|
134
|
+
return _create_function_wrapper(cast(Callable[P, R], obj), "workflow", name)
|
|
135
|
+
|
|
136
|
+
if target is not None:
|
|
137
|
+
return decorator(target)
|
|
138
|
+
return decorator
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def agent(
|
|
142
|
+
target: Union[Callable[P, R], C, None] = None, *, name: Optional[str] = None
|
|
143
|
+
) -> Union[Callable[P, R], C, Callable[[Callable[P, R]], Callable[P, R]]]:
|
|
144
|
+
def decorator(obj: Union[Callable[P, R], C]) -> Union[Callable[P, R], C]:
|
|
145
|
+
if inspect.isclass(obj):
|
|
146
|
+
return _wrap_class_methods(cast(C, obj), "agent", name)
|
|
147
|
+
else:
|
|
148
|
+
return _create_function_wrapper(cast(Callable[P, R], obj), "agent", name)
|
|
149
|
+
|
|
150
|
+
if target is not None:
|
|
151
|
+
return decorator(target)
|
|
152
|
+
return decorator
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def task(
|
|
156
|
+
target: Union[Callable[P, R], C, None] = None, *, name: Optional[str] = None
|
|
157
|
+
) -> Union[Callable[P, R], C, Callable[[Callable[P, R]], Callable[P, R]]]:
|
|
158
|
+
def decorator(obj: Union[Callable[P, R], C]) -> Union[Callable[P, R], C]:
|
|
159
|
+
if inspect.isclass(obj):
|
|
160
|
+
return _wrap_class_methods(cast(C, obj), "task", name)
|
|
161
|
+
else:
|
|
162
|
+
# When obj is a function, it should be type Callable[P, R]
|
|
163
|
+
return _create_function_wrapper(cast(Callable[P, R], obj), "task", name)
|
|
164
|
+
|
|
165
|
+
if target is not None:
|
|
166
|
+
return decorator(target)
|
|
167
|
+
return decorator
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# File: netra/exceptions/injection.py
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InjectionException(Exception):
|
|
7
|
+
"""
|
|
8
|
+
Raised when prompt injection is detected in input and blocking is enabled.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
message (str): Human-readable explanation of why blocking occurred.
|
|
12
|
+
has_violation (bool): True if prompt injection was detected in the provided text.
|
|
13
|
+
violations (List[str]): List of violation types that were detected.
|
|
14
|
+
is_blocked (bool): True if blocking is enabled and prompt injection was detected.
|
|
15
|
+
violation_actions (Dict[str, List[str]]): Dictionary mapping action types to lists of violations.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str = "Input blocked due to detected injection.",
|
|
21
|
+
has_violation: bool = True,
|
|
22
|
+
violations: Optional[List[str]] = None,
|
|
23
|
+
is_blocked: bool = True,
|
|
24
|
+
violation_actions: Optional[Dict[str, List[str]]] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
# Always pass the message to the base Exception constructor
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
|
|
29
|
+
# Store structured attributes
|
|
30
|
+
self.has_violation: bool = has_violation
|
|
31
|
+
self.violations: List[str] = violations or []
|
|
32
|
+
self.is_blocked: bool = is_blocked
|
|
33
|
+
self.violation_actions: Dict[str, List[str]] = violation_actions or {}
|
netra/exceptions/pii.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# File: netra/exceptions/pii.py
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PIIBlockedException(Exception):
|
|
7
|
+
"""
|
|
8
|
+
Raised when PII is detected in input and blocking is enabled.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
message (str): Human-readable explanation of why blocking occurred.
|
|
12
|
+
has_pii (bool): True if PII was detected in the provided text.
|
|
13
|
+
pii_entities (Dict[str, int]): Mapping from PII label to number of occurrences.
|
|
14
|
+
masked_text (Union[str, List[Dict[str, str]], List[Any], None]): Input text after masking PII spans.
|
|
15
|
+
Can be a string for simple inputs, a list of dicts for chat messages,
|
|
16
|
+
or a list of BaseMessage objects for LangChain inputs.
|
|
17
|
+
is_blocked (bool): True if blocking is enabled and PII was detected.
|
|
18
|
+
pii_actions (Dict[str, List[str]]): Dictionary mapping action types to lists of PII entities.
|
|
19
|
+
original_text (Union[str, List[Dict[str, str]], List[str], List[Any], None]): The original text used to call the detect() method.
|
|
20
|
+
Can be a string, list of strings, list of dictionaries, or any other type.
|
|
21
|
+
hashed_entities (Dict[str, str]): Dictionary mapping hashed entity values to their original values.
|
|
22
|
+
Only populated when using Anonymizer for masking.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message: str = "Input blocked due to detected PII.",
|
|
28
|
+
has_pii: bool = True,
|
|
29
|
+
pii_entities: Optional[Dict[str, int]] = None,
|
|
30
|
+
masked_text: Optional[Union[str, List[Dict[str, str]], List[Any]]] = None,
|
|
31
|
+
pii_actions: Optional[Dict[Any, List[str]]] = None,
|
|
32
|
+
is_blocked: bool = True,
|
|
33
|
+
original_text: Optional[Union[str, List[Dict[str, str]], List[str], List[Any]]] = None,
|
|
34
|
+
hashed_entities: Optional[Dict[str, str]] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
# Always pass the message to the base Exception constructor
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
|
|
39
|
+
# Store structured attributes
|
|
40
|
+
self.has_pii: bool = has_pii
|
|
41
|
+
self.pii_entities: Dict[str, int] = pii_entities or {}
|
|
42
|
+
self.masked_text: Optional[Union[str, List[Dict[str, str]], List[Any]]] = masked_text
|
|
43
|
+
self.pii_actions: Dict[Any, List[str]] = pii_actions or {}
|
|
44
|
+
self.is_blocked: bool = is_blocked
|
|
45
|
+
self.original_text: Optional[Union[str, List[Dict[str, str]], List[str], List[Any]]] = original_text
|
|
46
|
+
self.hashed_entities: Dict[str, str] = hashed_entities or {}
|
netra/input_scanner.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input Scanner module for Netra SDK to implement LLM guard scanning options.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface for scanning input prompts using
|
|
5
|
+
various scanner implementations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any, Dict, List, Union
|
|
13
|
+
|
|
14
|
+
from netra import Netra
|
|
15
|
+
from netra.exceptions import InjectionException
|
|
16
|
+
from netra.scanner import Scanner
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ScanResult:
|
|
23
|
+
"""
|
|
24
|
+
Result of running input scanning on prompts.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
has_violation: True if any violations were detected
|
|
28
|
+
violations: List of violation types that were detected
|
|
29
|
+
is_blocked: True if the input should be blocked
|
|
30
|
+
violation_actions: Dictionary mapping action types to lists of violations
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
has_violation: bool = False
|
|
34
|
+
violations: List[str] = field(default_factory=list)
|
|
35
|
+
is_blocked: bool = False
|
|
36
|
+
violation_actions: Dict[str, List[str]] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ScannerType(Enum):
|
|
40
|
+
"""
|
|
41
|
+
Enum representing the available scanner types.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
PROMPT_INJECTION = "prompt_injection"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class InputScanner:
|
|
48
|
+
"""
|
|
49
|
+
A factory class for creating input scanners.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, scanner_types: List[Union[str, ScannerType]] = [ScannerType.PROMPT_INJECTION]):
|
|
53
|
+
self.scanner_types = scanner_types
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _get_scanner(scanner_type: Union[str, ScannerType], **kwargs: Any) -> Scanner:
|
|
57
|
+
"""
|
|
58
|
+
Factory function to get a scanner instance based on the specified type.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
scanner_type: The type of scanner to create (e.g., "prompt_injection" or ScannerType.PROMPT_INJECTION)
|
|
62
|
+
**kwargs: Additional parameters to pass to the scanner constructor
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Scanner: An instance of the appropriate scanner
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: If the specified scanner type is not supported
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(scanner_type, ScannerType):
|
|
71
|
+
scanner_type = scanner_type.value
|
|
72
|
+
|
|
73
|
+
if scanner_type == ScannerType.PROMPT_INJECTION.value:
|
|
74
|
+
match_type = None
|
|
75
|
+
try:
|
|
76
|
+
# Try to import from llm_guard if available
|
|
77
|
+
from llm_guard.input_scanners.prompt_injection import MatchType
|
|
78
|
+
|
|
79
|
+
match_type = kwargs.get("match_type", MatchType.FULL)
|
|
80
|
+
except ImportError:
|
|
81
|
+
logger.warning(
|
|
82
|
+
"llm-guard package is not installed. Using default match type. "
|
|
83
|
+
"To enable full functionality, install with: pip install 'netra-sdk[llm_guard]'"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
from netra.scanner import PromptInjection
|
|
87
|
+
|
|
88
|
+
threshold_value = kwargs.get("threshold", 0.5)
|
|
89
|
+
if not isinstance(threshold_value, (int, float)):
|
|
90
|
+
logger.info(f"Invalid threshold value: {threshold_value}")
|
|
91
|
+
threshold = 0.5
|
|
92
|
+
else:
|
|
93
|
+
threshold = float(threshold_value)
|
|
94
|
+
|
|
95
|
+
return PromptInjection(threshold=threshold, match_type=match_type)
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError(f"Unsupported scanner type: {scanner_type}")
|
|
98
|
+
|
|
99
|
+
def scan(self, prompt: str, is_blocked: bool = False) -> ScanResult:
|
|
100
|
+
violations_detected = []
|
|
101
|
+
for scanner_type in self.scanner_types:
|
|
102
|
+
try:
|
|
103
|
+
scanner = self._get_scanner(scanner_type)
|
|
104
|
+
scanner.scan(prompt)
|
|
105
|
+
except ValueError as e:
|
|
106
|
+
raise ValueError(f"Invalid value type: {e}")
|
|
107
|
+
except InjectionException as error:
|
|
108
|
+
violations_detected.append(error.violations[0])
|
|
109
|
+
|
|
110
|
+
# Create dynamic violation actions mapping based on detected violations and blocking status
|
|
111
|
+
violations_actions = {}
|
|
112
|
+
if violations_detected:
|
|
113
|
+
if is_blocked:
|
|
114
|
+
violations_actions["BLOCK"] = violations_detected
|
|
115
|
+
else:
|
|
116
|
+
violations_actions["FLAG"] = violations_detected
|
|
117
|
+
|
|
118
|
+
Netra.set_custom_event(
|
|
119
|
+
event_name="violation_detected",
|
|
120
|
+
attributes={
|
|
121
|
+
"has_violation": True,
|
|
122
|
+
"violations": violations_detected,
|
|
123
|
+
"is_blocked": is_blocked,
|
|
124
|
+
"violation_actions": json.dumps(violations_actions),
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if is_blocked and violations_detected:
|
|
129
|
+
raise InjectionException(
|
|
130
|
+
message=f"Input blocked: detected {', '.join(violations_detected)}.",
|
|
131
|
+
has_violation=True,
|
|
132
|
+
violations=violations_detected,
|
|
133
|
+
is_blocked=True,
|
|
134
|
+
violation_actions=violations_actions,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return ScanResult(
|
|
138
|
+
has_violation=bool(violations_detected),
|
|
139
|
+
violations=violations_detected,
|
|
140
|
+
violation_actions=violations_actions,
|
|
141
|
+
is_blocked=bool(is_blocked and violations_detected),
|
|
142
|
+
)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Callable, Optional, Set
|
|
3
|
+
|
|
4
|
+
from traceloop.sdk import Instruments, Telemetry
|
|
5
|
+
from traceloop.sdk.utils.package_check import is_package_installed
|
|
6
|
+
|
|
7
|
+
from netra.instrumentation.instruments import CustomInstruments, NetraInstruments
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def init_instrumentations(
|
|
11
|
+
should_enrich_metrics: bool,
|
|
12
|
+
base64_image_uploader: Optional[Callable[[str, str, str], str]],
|
|
13
|
+
instruments: Optional[Set[NetraInstruments]] = None,
|
|
14
|
+
block_instruments: Optional[Set[NetraInstruments]] = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
from traceloop.sdk.tracing.tracing import init_instrumentations
|
|
17
|
+
|
|
18
|
+
traceloop_instruments = set()
|
|
19
|
+
traceloop_block_instruments = set()
|
|
20
|
+
netra_custom_instruments = set()
|
|
21
|
+
netra_custom_block_instruments = set()
|
|
22
|
+
if instruments is not None:
|
|
23
|
+
for instrument in instruments:
|
|
24
|
+
if instrument.origin == CustomInstruments: # type: ignore[attr-defined]
|
|
25
|
+
netra_custom_instruments.add(getattr(CustomInstruments, instrument.name))
|
|
26
|
+
else:
|
|
27
|
+
traceloop_instruments.add(getattr(Instruments, instrument.name))
|
|
28
|
+
if block_instruments is not None:
|
|
29
|
+
for instrument in block_instruments:
|
|
30
|
+
if instrument.origin == CustomInstruments: # type: ignore[attr-defined]
|
|
31
|
+
netra_custom_block_instruments.add(getattr(CustomInstruments, instrument.name))
|
|
32
|
+
else:
|
|
33
|
+
traceloop_block_instruments.add(getattr(Instruments, instrument.name))
|
|
34
|
+
traceloop_block_instruments.update(
|
|
35
|
+
{
|
|
36
|
+
Instruments.WEAVIATE,
|
|
37
|
+
Instruments.QDRANT,
|
|
38
|
+
Instruments.GOOGLE_GENERATIVEAI,
|
|
39
|
+
Instruments.MISTRAL,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
init_instrumentations(
|
|
44
|
+
should_enrich_metrics=should_enrich_metrics,
|
|
45
|
+
base64_image_uploader=base64_image_uploader,
|
|
46
|
+
instruments=traceloop_instruments,
|
|
47
|
+
block_instruments=traceloop_block_instruments,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
netra_custom_instruments = netra_custom_instruments or set(CustomInstruments)
|
|
51
|
+
netra_custom_instruments = netra_custom_instruments - netra_custom_block_instruments
|
|
52
|
+
# Initialize Google GenAI instrumentation.
|
|
53
|
+
if CustomInstruments.GOOGLE_GENERATIVEAI in netra_custom_instruments:
|
|
54
|
+
init_google_genai_instrumentation()
|
|
55
|
+
|
|
56
|
+
# Initialize FastAPI instrumentation.
|
|
57
|
+
if CustomInstruments.FASTAPI in netra_custom_instruments:
|
|
58
|
+
init_fastapi_instrumentation()
|
|
59
|
+
|
|
60
|
+
# Initialize Qdrant instrumentation.
|
|
61
|
+
if CustomInstruments.QDRANTDB in netra_custom_instruments:
|
|
62
|
+
init_qdrant_instrumentation()
|
|
63
|
+
|
|
64
|
+
# Initialize Weaviate instrumentation.
|
|
65
|
+
if CustomInstruments.WEAVIATEDB in netra_custom_instruments:
|
|
66
|
+
init_weviate_instrumentation()
|
|
67
|
+
|
|
68
|
+
# Initialize HTTPX instrumentation.
|
|
69
|
+
if CustomInstruments.HTTPX in netra_custom_instruments:
|
|
70
|
+
init_httpx_instrumentation()
|
|
71
|
+
|
|
72
|
+
# Initialize AIOHTTP instrumentation.
|
|
73
|
+
if CustomInstruments.AIOHTTP in netra_custom_instruments:
|
|
74
|
+
init_aiohttp_instrumentation()
|
|
75
|
+
|
|
76
|
+
# Initialize Cohere instrumentation.
|
|
77
|
+
if CustomInstruments.COHEREAI in netra_custom_instruments:
|
|
78
|
+
init_cohere_instrumentation()
|
|
79
|
+
|
|
80
|
+
if CustomInstruments.MISTRALAI in netra_custom_instruments:
|
|
81
|
+
init_mistral_instrumentor()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def init_google_genai_instrumentation() -> bool:
|
|
85
|
+
"""Initialize Google GenAI instrumentation.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
bool: True if initialization was successful, False otherwise.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
if is_package_installed("google-genai"):
|
|
92
|
+
Telemetry().capture("instrumentation:genai:init")
|
|
93
|
+
from netra.instrumentation.google_genai import GoogleGenAiInstrumentor
|
|
94
|
+
|
|
95
|
+
instrumentor = GoogleGenAiInstrumentor(
|
|
96
|
+
exception_logger=lambda e: Telemetry().log_exception(e),
|
|
97
|
+
)
|
|
98
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
99
|
+
instrumentor.instrument()
|
|
100
|
+
return True
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logging.error(f"Error initializing Google GenAI instrumentor: {e}")
|
|
103
|
+
Telemetry().log_exception(e)
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def init_fastapi_instrumentation() -> bool:
|
|
108
|
+
"""Initialize FastAPI instrumentation.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
bool: True if initialization was successful, False otherwise.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
if not is_package_installed("fastapi"):
|
|
115
|
+
return True
|
|
116
|
+
from fastapi import FastAPI
|
|
117
|
+
|
|
118
|
+
original_init = FastAPI.__init__
|
|
119
|
+
|
|
120
|
+
def _patched_init(self: FastAPI, *args: Any, **kwargs: Any) -> None:
|
|
121
|
+
original_init(self, *args, **kwargs)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
125
|
+
|
|
126
|
+
FastAPIInstrumentor().instrument_app(self)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logging.warning(f"Failed to auto-instrument FastAPI: {e}")
|
|
129
|
+
|
|
130
|
+
FastAPI.__init__ = _patched_init
|
|
131
|
+
return True
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logging.error(f"Error initializing FastAPI instrumentor: {e}")
|
|
134
|
+
Telemetry().log_exception(e)
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def init_qdrant_instrumentation() -> bool:
|
|
139
|
+
"""Initialize Qdrant instrumentation.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
bool: True if initialization was successful, False otherwise.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
if is_package_installed("qdrant-client"):
|
|
146
|
+
from opentelemetry.instrumentation.qdrant import QdrantInstrumentor
|
|
147
|
+
|
|
148
|
+
instrumentor = QdrantInstrumentor()
|
|
149
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
150
|
+
instrumentor.instrument()
|
|
151
|
+
return True
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logging.error(f"Error initializing Qdrant instrumentor: {e}")
|
|
154
|
+
Telemetry().log_exception(e)
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def init_weviate_instrumentation() -> bool:
|
|
159
|
+
"""Initialize Weaviate instrumentation.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
bool: True if initialization was successful, False otherwise.
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
if is_package_installed("weaviate-client"):
|
|
166
|
+
from netra.instrumentation.weaviate import WeaviateInstrumentor
|
|
167
|
+
|
|
168
|
+
instrumentor = WeaviateInstrumentor()
|
|
169
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
170
|
+
instrumentor.instrument()
|
|
171
|
+
return True
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logging.error(f"Error initializing Weaviate instrumentor: {e}")
|
|
174
|
+
Telemetry().log_exception(e)
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def init_httpx_instrumentation() -> bool:
|
|
179
|
+
"""Initialize HTTPX instrumentation.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
bool: True if initialization was successful, False otherwise.
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
if is_package_installed("httpx"):
|
|
186
|
+
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
|
187
|
+
|
|
188
|
+
instrumentor = HTTPXClientInstrumentor()
|
|
189
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
190
|
+
instrumentor.instrument()
|
|
191
|
+
return True
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logging.error(f"Error initializing HTTPX instrumentor: {e}")
|
|
194
|
+
Telemetry().log_exception(e)
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def init_aiohttp_instrumentation() -> bool:
|
|
199
|
+
"""Initialize AIOHTTP instrumentation.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
bool: True if initialization was successful, False otherwise.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
if is_package_installed("aiohttp"):
|
|
206
|
+
from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor
|
|
207
|
+
|
|
208
|
+
instrumentor = AioHttpClientInstrumentor()
|
|
209
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
210
|
+
instrumentor.instrument()
|
|
211
|
+
return True
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logging.error(f"Error initializing AIOHTTP instrumentor: {e}")
|
|
214
|
+
Telemetry().log_exception(e)
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def init_cohere_instrumentation() -> bool:
|
|
219
|
+
"""Initialize Cohere instrumentation.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
bool: True if initialization was successful, False otherwise.
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
if is_package_installed("cohere"):
|
|
226
|
+
from netra.instrumentation.cohere import CohereInstrumentor
|
|
227
|
+
|
|
228
|
+
instrumentor = CohereInstrumentor()
|
|
229
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
230
|
+
instrumentor.instrument()
|
|
231
|
+
return True
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logging.error(f"Error initializing Cohere instrumentor: {e}")
|
|
234
|
+
Telemetry().log_exception(e)
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def init_mistral_instrumentor() -> bool:
|
|
239
|
+
"""Initialize Mistral instrumentation.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
bool: True if initialization was successful, False otherwise.
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
if is_package_installed("mistralai"):
|
|
246
|
+
from netra.instrumentation.mistralai import MistralAiInstrumentor
|
|
247
|
+
|
|
248
|
+
instrumentor = MistralAiInstrumentor(
|
|
249
|
+
exception_logger=lambda e: Telemetry().log_exception(e),
|
|
250
|
+
)
|
|
251
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
252
|
+
instrumentor.instrument()
|
|
253
|
+
return True
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logging.error(f"-----Error initializing Mistral instrumentor: {e}")
|
|
256
|
+
Telemetry().log_exception(e)
|
|
257
|
+
return False
|