netra-sdk 0.1.29__py3-none-any.whl → 0.1.31__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 +25 -0
- netra/config.py +2 -0
- netra/decorators.py +27 -2
- netra/instrumentation/__init__.py +24 -0
- netra/instrumentation/instruments.py +2 -0
- netra/instrumentation/litellm/__init__.py +161 -0
- netra/instrumentation/litellm/version.py +1 -0
- netra/instrumentation/litellm/wrappers.py +557 -0
- netra/processors/__init__.py +2 -1
- netra/processors/instrumentation_span_processor.py +102 -0
- netra/session_manager.py +88 -0
- netra/span_wrapper.py +14 -0
- netra/tracer.py +3 -2
- netra/version.py +1 -1
- {netra_sdk-0.1.29.dist-info → netra_sdk-0.1.31.dist-info}/METADATA +2 -1
- {netra_sdk-0.1.29.dist-info → netra_sdk-0.1.31.dist-info}/RECORD +18 -14
- {netra_sdk-0.1.29.dist-info → netra_sdk-0.1.31.dist-info}/LICENCE +0 -0
- {netra_sdk-0.1.29.dist-info → netra_sdk-0.1.31.dist-info}/WHEEL +0 -0
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
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from collections.abc import Awaitable
|
|
4
|
+
from typing import Any, AsyncIterator, Callable, Dict, Iterator, Tuple
|
|
5
|
+
|
|
6
|
+
from opentelemetry import context as context_api
|
|
7
|
+
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
|
8
|
+
from opentelemetry.semconv_ai import (
|
|
9
|
+
SpanAttributes,
|
|
10
|
+
)
|
|
11
|
+
from opentelemetry.trace import Span, SpanKind, Tracer
|
|
12
|
+
from opentelemetry.trace.status import Status, StatusCode
|
|
13
|
+
from wrapt import ObjectProxy
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
COMPLETION_SPAN_NAME = "litellm.completion"
|
|
18
|
+
EMBEDDING_SPAN_NAME = "litellm.embedding"
|
|
19
|
+
IMAGE_GENERATION_SPAN_NAME = "litellm.image_generation"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def should_suppress_instrumentation() -> bool:
|
|
23
|
+
"""Check if instrumentation should be suppressed"""
|
|
24
|
+
return context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) is True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_streaming_response(response: Any) -> bool:
|
|
28
|
+
"""Check if response is a streaming response"""
|
|
29
|
+
return hasattr(response, "__iter__") and not isinstance(response, (str, bytes, dict))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def model_as_dict(obj: Any) -> Dict[str, Any]:
|
|
33
|
+
"""Convert LiteLLM model object to dictionary"""
|
|
34
|
+
if hasattr(obj, "model_dump"):
|
|
35
|
+
result = obj.model_dump()
|
|
36
|
+
return result if isinstance(result, dict) else {}
|
|
37
|
+
elif hasattr(obj, "to_dict"):
|
|
38
|
+
result = obj.to_dict()
|
|
39
|
+
return result if isinstance(result, dict) else {}
|
|
40
|
+
elif isinstance(obj, dict):
|
|
41
|
+
return obj
|
|
42
|
+
else:
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def set_request_attributes(span: Span, kwargs: Dict[str, Any], operation_type: str) -> None:
|
|
47
|
+
"""Set request attributes on span"""
|
|
48
|
+
if not span.is_recording():
|
|
49
|
+
return
|
|
50
|
+
|
|
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(f"{SpanAttributes.LLM_PROMPTS}.{index}.content", str(message.get("content", "")))
|
|
76
|
+
|
|
77
|
+
# Embedding specific attributes
|
|
78
|
+
if operation_type == "embedding" and kwargs.get("input"):
|
|
79
|
+
input_data = kwargs["input"]
|
|
80
|
+
if isinstance(input_data, str):
|
|
81
|
+
span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.0.content", input_data)
|
|
82
|
+
elif isinstance(input_data, list):
|
|
83
|
+
for index, text in enumerate(input_data):
|
|
84
|
+
if isinstance(text, str):
|
|
85
|
+
span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{index}.content", text)
|
|
86
|
+
|
|
87
|
+
# Image generation specific attributes
|
|
88
|
+
if operation_type == "image_generation":
|
|
89
|
+
if kwargs.get("prompt"):
|
|
90
|
+
span.set_attribute("gen_ai.prompt", kwargs["prompt"])
|
|
91
|
+
if kwargs.get("n"):
|
|
92
|
+
span.set_attribute("gen_ai.request.n", kwargs["n"])
|
|
93
|
+
if kwargs.get("size"):
|
|
94
|
+
span.set_attribute("gen_ai.request.size", kwargs["size"])
|
|
95
|
+
if kwargs.get("quality"):
|
|
96
|
+
span.set_attribute("gen_ai.request.quality", kwargs["quality"])
|
|
97
|
+
if kwargs.get("style"):
|
|
98
|
+
span.set_attribute("gen_ai.request.style", kwargs["style"])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def set_response_attributes(span: Span, response_dict: Dict[str, Any], operation_type: str) -> None:
|
|
102
|
+
"""Set response attributes on span"""
|
|
103
|
+
if not span.is_recording():
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if response_dict.get("model"):
|
|
107
|
+
span.set_attribute(f"{SpanAttributes.LLM_RESPONSE_MODEL}", response_dict["model"])
|
|
108
|
+
|
|
109
|
+
if response_dict.get("id"):
|
|
110
|
+
span.set_attribute("gen_ai.response.id", response_dict["id"])
|
|
111
|
+
|
|
112
|
+
# Usage information
|
|
113
|
+
usage = response_dict.get("usage", {})
|
|
114
|
+
if usage:
|
|
115
|
+
if usage.get("prompt_tokens"):
|
|
116
|
+
span.set_attribute(f"{SpanAttributes.LLM_USAGE_PROMPT_TOKENS}", usage["prompt_tokens"])
|
|
117
|
+
if usage.get("completion_tokens"):
|
|
118
|
+
span.set_attribute(f"{SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}", usage["completion_tokens"])
|
|
119
|
+
if usage.get("cache_read_input_tokens"):
|
|
120
|
+
span.set_attribute(f"{SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS}", usage["cache_read_input_tokens"])
|
|
121
|
+
if usage.get("cache_creation_input_tokens"):
|
|
122
|
+
span.set_attribute("gen_ai.usage.cache_creation_input_tokens", usage["cache_creation_input_tokens"])
|
|
123
|
+
if usage.get("total_tokens"):
|
|
124
|
+
span.set_attribute(f"{SpanAttributes.LLM_USAGE_TOTAL_TOKENS}", usage["total_tokens"])
|
|
125
|
+
|
|
126
|
+
# Chat completion response content
|
|
127
|
+
if operation_type == "chat":
|
|
128
|
+
choices = response_dict.get("choices", [])
|
|
129
|
+
for index, choice in enumerate(choices):
|
|
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"])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def completion_wrapper(tracer: Tracer) -> Callable[..., Any]:
|
|
162
|
+
"""Wrapper for LiteLLM completion function"""
|
|
163
|
+
|
|
164
|
+
def wrapper(wrapped: Callable[..., Any], instance: Any, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Any:
|
|
165
|
+
logger.debug(f"LiteLLM completion wrapper called with model: {kwargs.get('model')}")
|
|
166
|
+
|
|
167
|
+
if should_suppress_instrumentation():
|
|
168
|
+
logger.debug("LiteLLM instrumentation suppressed")
|
|
169
|
+
return wrapped(*args, **kwargs)
|
|
170
|
+
|
|
171
|
+
# Check if streaming
|
|
172
|
+
is_streaming = kwargs.get("stream", False)
|
|
173
|
+
|
|
174
|
+
if is_streaming:
|
|
175
|
+
# Use start_span for streaming - returns span directly
|
|
176
|
+
span = tracer.start_span(
|
|
177
|
+
COMPLETION_SPAN_NAME, kind=SpanKind.CLIENT, attributes={"llm.request.type": "chat"}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
set_request_attributes(span, kwargs, "chat")
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
start_time = time.time()
|
|
184
|
+
response = wrapped(*args, **kwargs)
|
|
185
|
+
|
|
186
|
+
return StreamingWrapper(span=span, response=response, start_time=start_time, request_kwargs=kwargs)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
189
|
+
span.record_exception(e)
|
|
190
|
+
span.end()
|
|
191
|
+
raise
|
|
192
|
+
else:
|
|
193
|
+
# Use start_as_current_span for non-streaming - returns context manager
|
|
194
|
+
with tracer.start_as_current_span(
|
|
195
|
+
COMPLETION_SPAN_NAME, kind=SpanKind.CLIENT, attributes={"llm.request.type": "chat"}
|
|
196
|
+
) as span:
|
|
197
|
+
set_request_attributes(span, kwargs, "chat")
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
start_time = time.time()
|
|
201
|
+
response = wrapped(*args, **kwargs)
|
|
202
|
+
end_time = time.time()
|
|
203
|
+
|
|
204
|
+
response_dict = model_as_dict(response)
|
|
205
|
+
set_response_attributes(span, response_dict, "chat")
|
|
206
|
+
|
|
207
|
+
span.set_attribute("llm.response.duration", end_time - start_time)
|
|
208
|
+
span.set_status(Status(StatusCode.OK))
|
|
209
|
+
|
|
210
|
+
return response
|
|
211
|
+
except Exception as e:
|
|
212
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
213
|
+
raise
|
|
214
|
+
|
|
215
|
+
return wrapper
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def acompletion_wrapper(tracer: Tracer) -> Callable[..., Awaitable[Any]]:
|
|
219
|
+
"""Async wrapper for LiteLLM acompletion function"""
|
|
220
|
+
|
|
221
|
+
async def wrapper(
|
|
222
|
+
wrapped: Callable[..., Awaitable[Any]], instance: Any, args: Tuple[Any, ...], kwargs: Dict[str, Any]
|
|
223
|
+
) -> Any:
|
|
224
|
+
if should_suppress_instrumentation():
|
|
225
|
+
return await wrapped(*args, **kwargs)
|
|
226
|
+
|
|
227
|
+
# Check if streaming
|
|
228
|
+
is_streaming = kwargs.get("stream", False)
|
|
229
|
+
|
|
230
|
+
if is_streaming:
|
|
231
|
+
# Use start_span for streaming - returns span directly
|
|
232
|
+
span = tracer.start_span(
|
|
233
|
+
COMPLETION_SPAN_NAME, kind=SpanKind.CLIENT, attributes={"llm.request.type": "chat"}
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
set_request_attributes(span, kwargs, "chat")
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
start_time = time.time()
|
|
240
|
+
response = await wrapped(*args, **kwargs)
|
|
241
|
+
|
|
242
|
+
return AsyncStreamingWrapper(span=span, response=response, start_time=start_time, request_kwargs=kwargs)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
245
|
+
span.record_exception(e)
|
|
246
|
+
span.end()
|
|
247
|
+
raise
|
|
248
|
+
else:
|
|
249
|
+
# Use start_as_current_span for non-streaming - returns context manager
|
|
250
|
+
with tracer.start_as_current_span(
|
|
251
|
+
COMPLETION_SPAN_NAME, kind=SpanKind.CLIENT, attributes={"llm.request.type": "chat"}
|
|
252
|
+
) as span:
|
|
253
|
+
set_request_attributes(span, kwargs, "chat")
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
start_time = time.time()
|
|
257
|
+
response = await wrapped(*args, **kwargs)
|
|
258
|
+
end_time = time.time()
|
|
259
|
+
|
|
260
|
+
response_dict = model_as_dict(response)
|
|
261
|
+
set_response_attributes(span, response_dict, "chat")
|
|
262
|
+
|
|
263
|
+
span.set_attribute("llm.response.duration", end_time - start_time)
|
|
264
|
+
span.set_status(Status(StatusCode.OK))
|
|
265
|
+
|
|
266
|
+
return response
|
|
267
|
+
except Exception as e:
|
|
268
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
269
|
+
raise
|
|
270
|
+
|
|
271
|
+
return wrapper
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def embedding_wrapper(tracer: Tracer) -> Callable[..., Any]:
|
|
275
|
+
"""Wrapper for LiteLLM embedding function"""
|
|
276
|
+
|
|
277
|
+
def wrapper(wrapped: Callable[..., Any], instance: Any, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Any:
|
|
278
|
+
if should_suppress_instrumentation():
|
|
279
|
+
return wrapped(*args, **kwargs)
|
|
280
|
+
|
|
281
|
+
# Embeddings are never streaming, always use start_as_current_span
|
|
282
|
+
with tracer.start_as_current_span(
|
|
283
|
+
EMBEDDING_SPAN_NAME, kind=SpanKind.CLIENT, attributes={"llm.request.type": "embedding"}
|
|
284
|
+
) as span:
|
|
285
|
+
set_request_attributes(span, kwargs, "embedding")
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
start_time = time.time()
|
|
289
|
+
response = wrapped(*args, **kwargs)
|
|
290
|
+
end_time = time.time()
|
|
291
|
+
|
|
292
|
+
response_dict = model_as_dict(response)
|
|
293
|
+
set_response_attributes(span, response_dict, "embedding")
|
|
294
|
+
|
|
295
|
+
span.set_attribute("llm.response.duration", end_time - start_time)
|
|
296
|
+
span.set_status(Status(StatusCode.OK))
|
|
297
|
+
|
|
298
|
+
return response
|
|
299
|
+
except Exception as e:
|
|
300
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
301
|
+
raise
|
|
302
|
+
|
|
303
|
+
return wrapper
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def aembedding_wrapper(tracer: Tracer) -> Callable[..., Awaitable[Any]]:
|
|
307
|
+
"""Async wrapper for LiteLLM aembedding function"""
|
|
308
|
+
|
|
309
|
+
async def wrapper(
|
|
310
|
+
wrapped: Callable[..., Awaitable[Any]], instance: Any, args: Tuple[Any, ...], kwargs: Dict[str, Any]
|
|
311
|
+
) -> Any:
|
|
312
|
+
if should_suppress_instrumentation():
|
|
313
|
+
return await wrapped(*args, **kwargs)
|
|
314
|
+
|
|
315
|
+
# Embeddings are never streaming, always use start_as_current_span
|
|
316
|
+
with tracer.start_as_current_span(
|
|
317
|
+
EMBEDDING_SPAN_NAME, kind=SpanKind.CLIENT, attributes={"llm.request.type": "embedding"}
|
|
318
|
+
) as span:
|
|
319
|
+
set_request_attributes(span, kwargs, "embedding")
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
start_time = time.time()
|
|
323
|
+
response = await wrapped(*args, **kwargs)
|
|
324
|
+
end_time = time.time()
|
|
325
|
+
|
|
326
|
+
response_dict = model_as_dict(response)
|
|
327
|
+
set_response_attributes(span, response_dict, "embedding")
|
|
328
|
+
|
|
329
|
+
span.set_attribute("llm.response.duration", end_time - start_time)
|
|
330
|
+
span.set_status(Status(StatusCode.OK))
|
|
331
|
+
|
|
332
|
+
return response
|
|
333
|
+
except Exception as e:
|
|
334
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
335
|
+
raise
|
|
336
|
+
|
|
337
|
+
return wrapper
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def image_generation_wrapper(tracer: Tracer) -> Callable[..., Any]:
|
|
341
|
+
"""Wrapper for LiteLLM image_generation function"""
|
|
342
|
+
|
|
343
|
+
def wrapper(wrapped: Callable[..., Any], instance: Any, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Any:
|
|
344
|
+
if should_suppress_instrumentation():
|
|
345
|
+
return wrapped(*args, **kwargs)
|
|
346
|
+
|
|
347
|
+
# Image generation is never streaming, always use start_as_current_span
|
|
348
|
+
with tracer.start_as_current_span(
|
|
349
|
+
IMAGE_GENERATION_SPAN_NAME, kind=SpanKind.CLIENT, attributes={"llm.request.type": "image_generation"}
|
|
350
|
+
) as span:
|
|
351
|
+
set_request_attributes(span, kwargs, "image_generation")
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
start_time = time.time()
|
|
355
|
+
response = wrapped(*args, **kwargs)
|
|
356
|
+
end_time = time.time()
|
|
357
|
+
|
|
358
|
+
response_dict = model_as_dict(response)
|
|
359
|
+
set_response_attributes(span, response_dict, "image_generation")
|
|
360
|
+
|
|
361
|
+
span.set_attribute("llm.response.duration", end_time - start_time)
|
|
362
|
+
span.set_status(Status(StatusCode.OK))
|
|
363
|
+
|
|
364
|
+
return response
|
|
365
|
+
except Exception as e:
|
|
366
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
367
|
+
raise
|
|
368
|
+
|
|
369
|
+
return wrapper
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def aimage_generation_wrapper(tracer: Tracer) -> Callable[..., Awaitable[Any]]:
|
|
373
|
+
"""Async wrapper for LiteLLM aimage_generation function"""
|
|
374
|
+
|
|
375
|
+
async def wrapper(
|
|
376
|
+
wrapped: Callable[..., Awaitable[Any]], instance: Any, args: Tuple[Any, ...], kwargs: Dict[str, Any]
|
|
377
|
+
) -> Any:
|
|
378
|
+
if should_suppress_instrumentation():
|
|
379
|
+
return await wrapped(*args, **kwargs)
|
|
380
|
+
|
|
381
|
+
# Image generation is never streaming, always use start_as_current_span
|
|
382
|
+
with tracer.start_as_current_span(
|
|
383
|
+
IMAGE_GENERATION_SPAN_NAME, kind=SpanKind.CLIENT, attributes={"llm.request.type": "image_generation"}
|
|
384
|
+
) as span:
|
|
385
|
+
set_request_attributes(span, kwargs, "image_generation")
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
start_time = time.time()
|
|
389
|
+
response = await wrapped(*args, **kwargs)
|
|
390
|
+
end_time = time.time()
|
|
391
|
+
|
|
392
|
+
response_dict = model_as_dict(response)
|
|
393
|
+
set_response_attributes(span, response_dict, "image_generation")
|
|
394
|
+
|
|
395
|
+
span.set_attribute("llm.response.duration", end_time - start_time)
|
|
396
|
+
span.set_status(Status(StatusCode.OK))
|
|
397
|
+
|
|
398
|
+
return response
|
|
399
|
+
except Exception as e:
|
|
400
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
401
|
+
raise
|
|
402
|
+
|
|
403
|
+
return wrapper
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class StreamingWrapper(ObjectProxy): # type: ignore[misc]
|
|
407
|
+
"""Wrapper for streaming responses"""
|
|
408
|
+
|
|
409
|
+
def __init__(self, span: Span, response: Iterator[Any], start_time: float, request_kwargs: Dict[str, Any]) -> None:
|
|
410
|
+
super().__init__(response)
|
|
411
|
+
self._span = span
|
|
412
|
+
self._start_time = start_time
|
|
413
|
+
self._request_kwargs = request_kwargs
|
|
414
|
+
self._complete_response: Dict[str, Any] = {"choices": [], "model": ""}
|
|
415
|
+
self._content_parts: list[str] = []
|
|
416
|
+
|
|
417
|
+
def __iter__(self) -> Iterator[Any]:
|
|
418
|
+
return self
|
|
419
|
+
|
|
420
|
+
def __next__(self) -> Any:
|
|
421
|
+
try:
|
|
422
|
+
chunk = self.__wrapped__.__next__()
|
|
423
|
+
self._process_chunk(chunk)
|
|
424
|
+
return chunk
|
|
425
|
+
except StopIteration:
|
|
426
|
+
self._finalize_span()
|
|
427
|
+
raise
|
|
428
|
+
|
|
429
|
+
def _process_chunk(self, chunk: Any) -> None:
|
|
430
|
+
"""Process streaming chunk"""
|
|
431
|
+
chunk_dict = model_as_dict(chunk)
|
|
432
|
+
|
|
433
|
+
# Accumulate response data
|
|
434
|
+
if chunk_dict.get("model"):
|
|
435
|
+
self._complete_response["model"] = chunk_dict["model"]
|
|
436
|
+
|
|
437
|
+
# Accumulate usage information from chunks
|
|
438
|
+
if chunk_dict.get("usage"):
|
|
439
|
+
self._complete_response["usage"] = chunk_dict["usage"]
|
|
440
|
+
|
|
441
|
+
# Collect content from delta
|
|
442
|
+
choices = chunk_dict.get("choices", [])
|
|
443
|
+
for choice in choices:
|
|
444
|
+
delta = choice.get("delta", {})
|
|
445
|
+
if delta.get("content"):
|
|
446
|
+
self._content_parts.append(delta["content"])
|
|
447
|
+
|
|
448
|
+
# Collect finish_reason from choices
|
|
449
|
+
if choice.get("finish_reason"):
|
|
450
|
+
if "choices" not in self._complete_response:
|
|
451
|
+
self._complete_response["choices"] = []
|
|
452
|
+
# Ensure we have enough choice entries
|
|
453
|
+
while len(self._complete_response["choices"]) <= len(choices) - 1:
|
|
454
|
+
self._complete_response["choices"].append(
|
|
455
|
+
{"message": {"role": "assistant", "content": ""}, "finish_reason": None}
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
choice_index = choice.get("index", 0)
|
|
459
|
+
if choice_index < len(self._complete_response["choices"]):
|
|
460
|
+
self._complete_response["choices"][choice_index]["finish_reason"] = choice["finish_reason"]
|
|
461
|
+
|
|
462
|
+
# Add chunk event
|
|
463
|
+
self._span.add_event("llm.content.completion.chunk")
|
|
464
|
+
|
|
465
|
+
def _finalize_span(self) -> None:
|
|
466
|
+
"""Finalize span when streaming is complete"""
|
|
467
|
+
end_time = time.time()
|
|
468
|
+
duration = end_time - self._start_time
|
|
469
|
+
|
|
470
|
+
# Set accumulated content
|
|
471
|
+
if self._content_parts:
|
|
472
|
+
full_content = "".join(self._content_parts)
|
|
473
|
+
self._span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.0.content", full_content)
|
|
474
|
+
self._span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.0.role", "assistant")
|
|
475
|
+
|
|
476
|
+
set_response_attributes(self._span, self._complete_response, "chat")
|
|
477
|
+
self._span.set_attribute("llm.response.duration", duration)
|
|
478
|
+
self._span.set_status(Status(StatusCode.OK))
|
|
479
|
+
self._span.end()
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class AsyncStreamingWrapper(ObjectProxy): # type: ignore[misc]
|
|
483
|
+
"""Async wrapper for streaming responses"""
|
|
484
|
+
|
|
485
|
+
def __init__(
|
|
486
|
+
self, span: Span, response: AsyncIterator[Any], start_time: float, request_kwargs: Dict[str, Any]
|
|
487
|
+
) -> None:
|
|
488
|
+
super().__init__(response)
|
|
489
|
+
self._span = span
|
|
490
|
+
self._start_time = start_time
|
|
491
|
+
self._request_kwargs = request_kwargs
|
|
492
|
+
self._complete_response: Dict[str, Any] = {"choices": [], "model": ""}
|
|
493
|
+
self._content_parts: list[str] = []
|
|
494
|
+
|
|
495
|
+
def __aiter__(self) -> AsyncIterator[Any]:
|
|
496
|
+
return self
|
|
497
|
+
|
|
498
|
+
async def __anext__(self) -> Any:
|
|
499
|
+
try:
|
|
500
|
+
chunk = await self.__wrapped__.__anext__()
|
|
501
|
+
self._process_chunk(chunk)
|
|
502
|
+
return chunk
|
|
503
|
+
except StopAsyncIteration:
|
|
504
|
+
self._finalize_span()
|
|
505
|
+
raise
|
|
506
|
+
|
|
507
|
+
def _process_chunk(self, chunk: Any) -> None:
|
|
508
|
+
"""Process streaming chunk"""
|
|
509
|
+
chunk_dict = model_as_dict(chunk)
|
|
510
|
+
|
|
511
|
+
# Accumulate response data
|
|
512
|
+
if chunk_dict.get("model"):
|
|
513
|
+
self._complete_response["model"] = chunk_dict["model"]
|
|
514
|
+
|
|
515
|
+
# Accumulate usage information from chunks
|
|
516
|
+
if chunk_dict.get("usage"):
|
|
517
|
+
self._complete_response["usage"] = chunk_dict["usage"]
|
|
518
|
+
|
|
519
|
+
# Collect content from delta
|
|
520
|
+
choices = chunk_dict.get("choices", [])
|
|
521
|
+
for choice in choices:
|
|
522
|
+
delta = choice.get("delta", {})
|
|
523
|
+
if delta.get("content"):
|
|
524
|
+
self._content_parts.append(delta["content"])
|
|
525
|
+
|
|
526
|
+
# Collect finish_reason from choices
|
|
527
|
+
if choice.get("finish_reason"):
|
|
528
|
+
if "choices" not in self._complete_response:
|
|
529
|
+
self._complete_response["choices"] = []
|
|
530
|
+
# Ensure we have enough choice entries
|
|
531
|
+
while len(self._complete_response["choices"]) <= len(choices) - 1:
|
|
532
|
+
self._complete_response["choices"].append(
|
|
533
|
+
{"message": {"role": "assistant", "content": ""}, "finish_reason": None}
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
choice_index = choice.get("index", 0)
|
|
537
|
+
if choice_index < len(self._complete_response["choices"]):
|
|
538
|
+
self._complete_response["choices"][choice_index]["finish_reason"] = choice["finish_reason"]
|
|
539
|
+
|
|
540
|
+
# Add chunk event
|
|
541
|
+
self._span.add_event("llm.content.completion.chunk")
|
|
542
|
+
|
|
543
|
+
def _finalize_span(self) -> None:
|
|
544
|
+
"""Finalize span when streaming is complete"""
|
|
545
|
+
end_time = time.time()
|
|
546
|
+
duration = end_time - self._start_time
|
|
547
|
+
|
|
548
|
+
# Set accumulated content
|
|
549
|
+
if self._content_parts:
|
|
550
|
+
full_content = "".join(self._content_parts)
|
|
551
|
+
self._span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.0.content", full_content)
|
|
552
|
+
self._span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.0.role", "assistant")
|
|
553
|
+
|
|
554
|
+
set_response_attributes(self._span, self._complete_response, "chat")
|
|
555
|
+
self._span.set_attribute("llm.response.duration", duration)
|
|
556
|
+
self._span.set_status(Status(StatusCode.OK))
|
|
557
|
+
self._span.end()
|
netra/processors/__init__.py
CHANGED
|
@@ -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
|
|
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.
|
|
1
|
+
__version__ = "0.1.31"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: netra-sdk
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
netra/__init__.py,sha256=
|
|
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
|
|
7
|
-
netra/decorators.py,sha256=
|
|
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
|
|
11
11
|
netra/input_scanner.py,sha256=At6N9gNY8cR0O6S8x3K6swWBV3P1a_9O-XBNM_pcKz4,5348
|
|
12
|
-
netra/instrumentation/__init__.py,sha256=
|
|
12
|
+
netra/instrumentation/__init__.py,sha256=HdG3n5TxPRUNlOxsqjlvwDmBcnm3UtYx1OecLhnLeQM,41578
|
|
13
13
|
netra/instrumentation/aiohttp/__init__.py,sha256=M1kuF0R3gKY5rlbhEC1AR13UWHelmfokluL2yFysKWc,14398
|
|
14
14
|
netra/instrumentation/aiohttp/version.py,sha256=Zy-0Aukx-HS_Mo3NKPWg-hlUoWKDzS0w58gLoVtJec8,24
|
|
15
15
|
netra/instrumentation/cohere/__init__.py,sha256=3XwmCAZwZiMkHdNN3YvcBOLsNCx80ymbU31TyMzv1IY,17685
|
|
@@ -22,7 +22,10 @@ netra/instrumentation/google_genai/utils.py,sha256=2OeSN5jUaMKF4x5zWiW65R1LB_a44
|
|
|
22
22
|
netra/instrumentation/google_genai/version.py,sha256=Hww1duZrC8kYK7ThBSQVyz0HNOb0ys_o8Pln-wVQ1hI,23
|
|
23
23
|
netra/instrumentation/httpx/__init__.py,sha256=w1su_eQP_w5ZJHq0Lf-4miF5zM4OOW0ItmRp0wi85Ew,19388
|
|
24
24
|
netra/instrumentation/httpx/version.py,sha256=ZRQKbgDaGz_yuLk-cUKuk6ZBKCSRKZC8nQd041NRNXk,23
|
|
25
|
-
netra/instrumentation/instruments.py,sha256=
|
|
25
|
+
netra/instrumentation/instruments.py,sha256=O6MI_BO-5EBkVqI-dr5eqhYnk8mP5QEpI0RWJ7Fe3FQ,4349
|
|
26
|
+
netra/instrumentation/litellm/__init__.py,sha256=H9FsdEq-CL39zbl_dLm8D43-D1vAjoNqFTBpbmZsVXs,6740
|
|
27
|
+
netra/instrumentation/litellm/version.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
28
|
+
netra/instrumentation/litellm/wrappers.py,sha256=H_UG0et6PUmj6CQagvNzbs_WodNTMruzzGOHhedmTko,22840
|
|
26
29
|
netra/instrumentation/mistralai/__init__.py,sha256=RE0b-rS6iXdoynJMFKHL9s97eYo5HghrJa013fR4ZhI,18910
|
|
27
30
|
netra/instrumentation/mistralai/config.py,sha256=XCyo3mk30qkvqyCqeTrKwROahu0gcOEwmbDLOo53J5k,121
|
|
28
31
|
netra/instrumentation/mistralai/utils.py,sha256=nhdIer5gJFxuGwg8FCT222hggDHeMQDhJctnDSwLqcc,894
|
|
@@ -37,14 +40,15 @@ netra/instrumentation/pydantic_ai/wrappers.py,sha256=6cfIRvELBS4d9G9TttNYcHGueNI
|
|
|
37
40
|
netra/instrumentation/weaviate/__init__.py,sha256=EOlpWxobOLHYKqo_kMct_7nu26x1hr8qkeG5_h99wtg,4330
|
|
38
41
|
netra/instrumentation/weaviate/version.py,sha256=PiCZHjonujPbnIn0KmD3Yl68hrjPRG_oKe5vJF3mmG8,24
|
|
39
42
|
netra/pii.py,sha256=Rn4SjgTJW_aw9LcbjLuMqF3fKd9b1ndlYt1CaK51Ge0,33125
|
|
40
|
-
netra/processors/__init__.py,sha256=
|
|
43
|
+
netra/processors/__init__.py,sha256=2E18QGVwVl8nEpT-EViEZqGLdVVejMTEH1wZNugDmYU,230
|
|
44
|
+
netra/processors/instrumentation_span_processor.py,sha256=Ef5FTr8O5FLHcIkBAW3ueU1nlkV2DuOi-y5iIwHzldQ,4252
|
|
41
45
|
netra/processors/session_span_processor.py,sha256=qcsBl-LnILWefsftI8NQhXDGb94OWPc8LvzhVA0JS_c,2432
|
|
42
46
|
netra/scanner.py,sha256=kyDpeZiscCPb6pjuhS-sfsVj-dviBFRepdUWh0sLoEY,11554
|
|
43
|
-
netra/session_manager.py,sha256=
|
|
44
|
-
netra/span_wrapper.py,sha256=
|
|
45
|
-
netra/tracer.py,sha256=
|
|
46
|
-
netra/version.py,sha256=
|
|
47
|
-
netra_sdk-0.1.
|
|
48
|
-
netra_sdk-0.1.
|
|
49
|
-
netra_sdk-0.1.
|
|
50
|
-
netra_sdk-0.1.
|
|
47
|
+
netra/session_manager.py,sha256=AoQa-k4dFcq7PeOD8G8DNzhLzL1JrHUW6b_y8mRyTQo,10255
|
|
48
|
+
netra/span_wrapper.py,sha256=lGuV1F4Q5I_swIoIof5myzOQCFmGFdtrpgfQt7dTTus,8105
|
|
49
|
+
netra/tracer.py,sha256=YiuijB_5DBOLVgE39Lj3thWVmUqHLcqbdFVB0HGovW0,3543
|
|
50
|
+
netra/version.py,sha256=i-fDEsQ0iAiPKXFaj9eERDqcxl3BqNnavaCEqpNxmVI,23
|
|
51
|
+
netra_sdk-0.1.31.dist-info/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
|
|
52
|
+
netra_sdk-0.1.31.dist-info/METADATA,sha256=VvltGCy_nbt-TRB91KiH_hu6YC4CceY_uQW-UYXT7NE,28196
|
|
53
|
+
netra_sdk-0.1.31.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
54
|
+
netra_sdk-0.1.31.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|