netra-sdk 0.1.45__tar.gz → 0.1.47__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of netra-sdk might be problematic. Click here for more details.
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/PKG-INFO +1 -1
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/__init__.py +14 -10
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/config.py +1 -1
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/decorators.py +40 -8
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/processors/instrumentation_span_processor.py +0 -3
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/session_manager.py +12 -5
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/span_wrapper.py +22 -1
- netra_sdk-0.1.47/netra/tracer.py +134 -0
- netra_sdk-0.1.47/netra/version.py +1 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/pyproject.toml +1 -1
- netra_sdk-0.1.45/netra/tracer.py +0 -121
- netra_sdk-0.1.45/netra/version.py +0 -1
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/LICENCE +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/README.md +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/anonymizer/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/anonymizer/anonymizer.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/anonymizer/base.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/anonymizer/fp_anonymizer.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/exceptions/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/exceptions/injection.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/exceptions/pii.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/exporters/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/exporters/filtering_span_exporter.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/input_scanner.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/aiohttp/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/aiohttp/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/cohere/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/cohere/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/fastapi/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/fastapi/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/google_genai/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/google_genai/config.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/google_genai/utils.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/google_genai/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/httpx/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/httpx/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/instruments.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/litellm/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/litellm/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/litellm/wrappers.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/mistralai/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/mistralai/config.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/mistralai/utils.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/mistralai/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/openai/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/openai/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/openai/wrappers.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/pydantic_ai/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/pydantic_ai/utils.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/pydantic_ai/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/pydantic_ai/wrappers.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/weaviate/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/instrumentation/weaviate/version.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/pii.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/processors/__init__.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/processors/local_filtering_span_processor.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/processors/scrubbing_span_processor.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/processors/session_span_processor.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/scanner.py +0 -0
- {netra_sdk-0.1.45 → netra_sdk-0.1.47}/netra/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netra-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.47
|
|
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-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENCE
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import atexit
|
|
2
2
|
import logging
|
|
3
3
|
import threading
|
|
4
|
-
from typing import Any, Dict, List, Optional, Set
|
|
4
|
+
from typing import Any, Dict, List, Literal, Optional, Set
|
|
5
5
|
|
|
6
6
|
from opentelemetry import context as context_api
|
|
7
7
|
from opentelemetry import trace
|
|
@@ -14,7 +14,7 @@ from .config import Config
|
|
|
14
14
|
# Instrumentor functions
|
|
15
15
|
from .instrumentation import init_instrumentations
|
|
16
16
|
from .session_manager import ConversationType, SessionManager
|
|
17
|
-
from .span_wrapper import ActionModel, SpanWrapper, UsageModel
|
|
17
|
+
from .span_wrapper import ActionModel, SpanType, SpanWrapper, UsageModel
|
|
18
18
|
from .tracer import Tracer
|
|
19
19
|
|
|
20
20
|
# Package-level logger. Attach NullHandler by default so library does not emit logs
|
|
@@ -183,11 +183,12 @@ class Netra:
|
|
|
183
183
|
session_id: Session identifier
|
|
184
184
|
"""
|
|
185
185
|
if not isinstance(session_id, str):
|
|
186
|
-
|
|
186
|
+
logger.error(f"set_session_id: session_id must be a string, got {type(session_id)}")
|
|
187
|
+
return
|
|
187
188
|
if session_id:
|
|
188
189
|
SessionManager.set_session_context("session_id", session_id)
|
|
189
190
|
else:
|
|
190
|
-
logger.warning("Session ID must be provided for setting session_id.")
|
|
191
|
+
logger.warning("set_session_id: Session ID must be provided for setting session_id.")
|
|
191
192
|
|
|
192
193
|
@classmethod
|
|
193
194
|
def set_user_id(cls, user_id: str) -> None:
|
|
@@ -198,11 +199,12 @@ class Netra:
|
|
|
198
199
|
user_id: User identifier
|
|
199
200
|
"""
|
|
200
201
|
if not isinstance(user_id, str):
|
|
201
|
-
|
|
202
|
+
logger.error(f"set_user_id: user_id must be a string, got {type(user_id)}")
|
|
203
|
+
return
|
|
202
204
|
if user_id:
|
|
203
205
|
SessionManager.set_session_context("user_id", user_id)
|
|
204
206
|
else:
|
|
205
|
-
logger.warning("User ID must be provided for setting user_id.")
|
|
207
|
+
logger.warning("set_user_id: User ID must be provided for setting user_id.")
|
|
206
208
|
|
|
207
209
|
@classmethod
|
|
208
210
|
def set_tenant_id(cls, tenant_id: str) -> None:
|
|
@@ -213,11 +215,12 @@ class Netra:
|
|
|
213
215
|
user_account_id: User account identifier
|
|
214
216
|
"""
|
|
215
217
|
if not isinstance(tenant_id, str):
|
|
216
|
-
|
|
218
|
+
logger.error(f"set_tenant_id: tenant_id must be a string, got {type(tenant_id)}")
|
|
219
|
+
return
|
|
217
220
|
if tenant_id:
|
|
218
221
|
SessionManager.set_session_context("tenant_id", tenant_id)
|
|
219
222
|
else:
|
|
220
|
-
logger.warning("Tenant ID must be provided for setting tenant_id.")
|
|
223
|
+
logger.warning("set_tenant_id: Tenant ID must be provided for setting tenant_id.")
|
|
221
224
|
|
|
222
225
|
@classmethod
|
|
223
226
|
def set_custom_attributes(cls, key: str, value: Any) -> None:
|
|
@@ -263,11 +266,12 @@ class Netra:
|
|
|
263
266
|
name: str,
|
|
264
267
|
attributes: Optional[Dict[str, str]] = None,
|
|
265
268
|
module_name: str = "combat_sdk",
|
|
269
|
+
as_type: Optional[SpanType] = SpanType.SPAN,
|
|
266
270
|
) -> SpanWrapper:
|
|
267
271
|
"""
|
|
268
272
|
Start a new session.
|
|
269
273
|
"""
|
|
270
|
-
return SpanWrapper(name, attributes, module_name)
|
|
274
|
+
return SpanWrapper(name, attributes, module_name, as_type=as_type)
|
|
271
275
|
|
|
272
276
|
|
|
273
|
-
__all__ = ["Netra", "UsageModel", "ActionModel"]
|
|
277
|
+
__all__ = ["Netra", "UsageModel", "ActionModel", "SpanType"]
|
|
@@ -30,7 +30,7 @@ class Config:
|
|
|
30
30
|
# Maximum length for any attribute value (strings and bytes). Processors should honor this.
|
|
31
31
|
ATTRIBUTE_MAX_LEN = 2000
|
|
32
32
|
# Maximum length specifically for conversation entry content (strings or JSON when serialized)
|
|
33
|
-
CONVERSATION_CONTENT_MAX_LEN =
|
|
33
|
+
CONVERSATION_CONTENT_MAX_LEN = 2000
|
|
34
34
|
|
|
35
35
|
def __init__(
|
|
36
36
|
self,
|
|
@@ -33,6 +33,7 @@ from opentelemetry import trace
|
|
|
33
33
|
|
|
34
34
|
from .config import Config
|
|
35
35
|
from .session_manager import SessionManager
|
|
36
|
+
from .span_wrapper import SpanType
|
|
36
37
|
|
|
37
38
|
logger = logging.getLogger(__name__)
|
|
38
39
|
|
|
@@ -262,7 +263,12 @@ def _wrap_streaming_response_with_span(
|
|
|
262
263
|
return resp
|
|
263
264
|
|
|
264
265
|
|
|
265
|
-
def _create_function_wrapper(
|
|
266
|
+
def _create_function_wrapper(
|
|
267
|
+
func: Callable[P, R],
|
|
268
|
+
entity_type: str,
|
|
269
|
+
name: Optional[str] = None,
|
|
270
|
+
as_type: Optional[SpanType] = SpanType.SPAN,
|
|
271
|
+
) -> Callable[P, R]:
|
|
266
272
|
module_name = func.__name__
|
|
267
273
|
is_async = inspect.iscoroutinefunction(func)
|
|
268
274
|
span_name = name if name is not None else func.__name__
|
|
@@ -276,6 +282,15 @@ def _create_function_wrapper(func: Callable[P, R], entity_type: str, name: Optio
|
|
|
276
282
|
|
|
277
283
|
tracer = trace.get_tracer(module_name)
|
|
278
284
|
span = tracer.start_span(span_name)
|
|
285
|
+
# Set span type if provided
|
|
286
|
+
|
|
287
|
+
if not isinstance(as_type, SpanType):
|
|
288
|
+
logger.error("Invalid span type: %s", as_type)
|
|
289
|
+
return
|
|
290
|
+
try:
|
|
291
|
+
span.set_attribute("netra.span.type", as_type.value)
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
279
294
|
# Register and activate span
|
|
280
295
|
try:
|
|
281
296
|
SessionManager.register_span(span_name, span)
|
|
@@ -327,6 +342,15 @@ def _create_function_wrapper(func: Callable[P, R], entity_type: str, name: Optio
|
|
|
327
342
|
|
|
328
343
|
tracer = trace.get_tracer(module_name)
|
|
329
344
|
span = tracer.start_span(span_name)
|
|
345
|
+
# Set span type if provided
|
|
346
|
+
if as_type is not None:
|
|
347
|
+
if not isinstance(as_type, SpanType):
|
|
348
|
+
logger.error("Invalid span type: %s", as_type)
|
|
349
|
+
return
|
|
350
|
+
try:
|
|
351
|
+
span.set_attribute("netra.span.type", as_type.value)
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
330
354
|
# Register and activate span
|
|
331
355
|
try:
|
|
332
356
|
SessionManager.register_span(span_name, span)
|
|
@@ -370,7 +394,12 @@ def _create_function_wrapper(func: Callable[P, R], entity_type: str, name: Optio
|
|
|
370
394
|
return cast(Callable[P, R], sync_wrapper)
|
|
371
395
|
|
|
372
396
|
|
|
373
|
-
def _wrap_class_methods(
|
|
397
|
+
def _wrap_class_methods(
|
|
398
|
+
cls: C,
|
|
399
|
+
entity_type: str,
|
|
400
|
+
name: Optional[str] = None,
|
|
401
|
+
as_type: Optional[SpanType] = SpanType.SPAN,
|
|
402
|
+
) -> C:
|
|
374
403
|
class_name = name if name is not None else cls.__name__
|
|
375
404
|
for attr_name in cls.__dict__:
|
|
376
405
|
attr = getattr(cls, attr_name)
|
|
@@ -378,7 +407,7 @@ def _wrap_class_methods(cls: C, entity_type: str, name: Optional[str] = None) ->
|
|
|
378
407
|
continue
|
|
379
408
|
if callable(attr) and inspect.isfunction(attr):
|
|
380
409
|
method_span_name = f"{class_name}.{attr_name}"
|
|
381
|
-
wrapped_method = _create_function_wrapper(attr, entity_type, method_span_name)
|
|
410
|
+
wrapped_method = _create_function_wrapper(attr, entity_type, method_span_name, as_type=as_type)
|
|
382
411
|
setattr(cls, attr_name, wrapped_method)
|
|
383
412
|
return cls
|
|
384
413
|
|
|
@@ -416,10 +445,10 @@ def task(
|
|
|
416
445
|
) -> Union[Callable[P, R], C, Callable[[Callable[P, R]], Callable[P, R]]]:
|
|
417
446
|
def decorator(obj: Union[Callable[P, R], C]) -> Union[Callable[P, R], C]:
|
|
418
447
|
if inspect.isclass(obj):
|
|
419
|
-
return _wrap_class_methods(cast(C, obj), "task", name)
|
|
448
|
+
return _wrap_class_methods(cast(C, obj), "task", name, as_type=SpanType.TOOL)
|
|
420
449
|
else:
|
|
421
450
|
# When obj is a function, it should be type Callable[P, R]
|
|
422
|
-
return _create_function_wrapper(cast(Callable[P, R], obj), "task", name)
|
|
451
|
+
return _create_function_wrapper(cast(Callable[P, R], obj), "task", name, as_type=SpanType.TOOL)
|
|
423
452
|
|
|
424
453
|
if target is not None:
|
|
425
454
|
return decorator(target)
|
|
@@ -427,14 +456,17 @@ def task(
|
|
|
427
456
|
|
|
428
457
|
|
|
429
458
|
def span(
|
|
430
|
-
target: Union[Callable[P, R], C, None] = None,
|
|
459
|
+
target: Union[Callable[P, R], C, None] = None,
|
|
460
|
+
*,
|
|
461
|
+
name: Optional[str] = None,
|
|
462
|
+
as_type: Optional[SpanType] = SpanType.SPAN,
|
|
431
463
|
) -> Union[Callable[P, R], C, Callable[[Callable[P, R]], Callable[P, R]]]:
|
|
432
464
|
def decorator(obj: Union[Callable[P, R], C]) -> Union[Callable[P, R], C]:
|
|
433
465
|
if inspect.isclass(obj):
|
|
434
|
-
return _wrap_class_methods(cast(C, obj), "span", name)
|
|
466
|
+
return _wrap_class_methods(cast(C, obj), "span", name, as_type=as_type)
|
|
435
467
|
else:
|
|
436
468
|
# When obj is a function, it should be type Callable[P, R]
|
|
437
|
-
return _create_function_wrapper(cast(Callable[P, R], obj), "span", name)
|
|
469
|
+
return _create_function_wrapper(cast(Callable[P, R], obj), "span", name, as_type=as_type)
|
|
438
470
|
|
|
439
471
|
if target is not None:
|
|
440
472
|
return decorator(target)
|
|
@@ -74,9 +74,6 @@ class InstrumentationSpanProcessor(SpanProcessor): # type: ignore[misc]
|
|
|
74
74
|
truncated = self._truncate_value(value)
|
|
75
75
|
# Forward to original
|
|
76
76
|
original_set_attribute(key, truncated)
|
|
77
|
-
# Special rule: if model key set, mark span as llm
|
|
78
|
-
if key == "gen_ai.request.model":
|
|
79
|
-
original_set_attribute(f"{Config.LIBRARY_NAME}.span.type", "llm")
|
|
80
77
|
except Exception:
|
|
81
78
|
# Best-effort; never break span
|
|
82
79
|
try:
|
|
@@ -275,20 +275,27 @@ class SessionManager:
|
|
|
275
275
|
|
|
276
276
|
# Hard runtime validation of input types and values
|
|
277
277
|
if not isinstance(conversation_type, ConversationType):
|
|
278
|
-
|
|
278
|
+
logger.error(
|
|
279
|
+
"add_conversation: conversation_type must be a ConversationType enum value (input, output, system)"
|
|
280
|
+
)
|
|
281
|
+
return
|
|
279
282
|
normalized_type = conversation_type.value
|
|
280
283
|
|
|
281
284
|
if not isinstance(role, str):
|
|
282
|
-
|
|
285
|
+
logger.error("add_conversation: role must be a string")
|
|
286
|
+
return
|
|
283
287
|
|
|
284
288
|
if not isinstance(content, (str, dict)):
|
|
285
|
-
|
|
289
|
+
logger.error("add_conversation: content must be a string or dict")
|
|
290
|
+
return
|
|
286
291
|
|
|
287
292
|
if not role:
|
|
288
|
-
|
|
293
|
+
logger.error("add_conversation: role must be a non-empty string")
|
|
294
|
+
return
|
|
289
295
|
|
|
290
296
|
if not content:
|
|
291
|
-
|
|
297
|
+
logger.error("add_conversation: content must not be empty")
|
|
298
|
+
return
|
|
292
299
|
|
|
293
300
|
try:
|
|
294
301
|
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
5
6
|
from typing import Any, Dict, List, Literal, Optional
|
|
6
7
|
|
|
7
8
|
from opentelemetry import baggage
|
|
@@ -49,6 +50,13 @@ class ATTRIBUTE:
|
|
|
49
50
|
ACTION = "action"
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
class SpanType(str, Enum):
|
|
54
|
+
SPAN = "SPAN"
|
|
55
|
+
GENERATION = "GENERATION"
|
|
56
|
+
TOOL = "TOOL"
|
|
57
|
+
EMBEDDING = "EMBEDDING"
|
|
58
|
+
|
|
59
|
+
|
|
52
60
|
class SpanWrapper:
|
|
53
61
|
"""
|
|
54
62
|
Context manager for tracking observability data for external API calls.
|
|
@@ -63,9 +71,16 @@ class SpanWrapper:
|
|
|
63
71
|
span.set_usage(usage_data)
|
|
64
72
|
"""
|
|
65
73
|
|
|
66
|
-
def __init__(
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
name: str,
|
|
77
|
+
attributes: Optional[Dict[str, str]] = None,
|
|
78
|
+
module_name: str = "combat_sdk",
|
|
79
|
+
as_type: Optional[SpanType] = SpanType.SPAN,
|
|
80
|
+
):
|
|
67
81
|
self.name = name
|
|
68
82
|
self.attributes = attributes or {}
|
|
83
|
+
|
|
69
84
|
self.start_time: Optional[float] = None
|
|
70
85
|
self.end_time: Optional[float] = None
|
|
71
86
|
self.status = "pending"
|
|
@@ -80,6 +95,12 @@ class SpanWrapper:
|
|
|
80
95
|
# Token for locally attached baggage (if any)
|
|
81
96
|
self._local_block_token: Optional[object] = None
|
|
82
97
|
|
|
98
|
+
if isinstance(as_type, SpanType):
|
|
99
|
+
self.attributes["netra.span.type"] = as_type.value
|
|
100
|
+
else:
|
|
101
|
+
logger.error("Invalid span type: %s", as_type)
|
|
102
|
+
return
|
|
103
|
+
|
|
83
104
|
def __enter__(self) -> "SpanWrapper":
|
|
84
105
|
"""Start the span wrapper, begin time tracking, and create OpenTelemetry span."""
|
|
85
106
|
self.start_time = time.time()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Netra OpenTelemetry tracer configuration module.
|
|
2
|
+
|
|
3
|
+
This module handles the initialization and configuration of OpenTelemetry tracing,
|
|
4
|
+
including exporter setup and span processor configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
from opentelemetry import trace
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
13
|
+
from opentelemetry.sdk import trace as sdk_trace
|
|
14
|
+
from opentelemetry.sdk.resources import DEPLOYMENT_ENVIRONMENT, SERVICE_NAME, Resource
|
|
15
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
16
|
+
from opentelemetry.sdk.trace.export import (
|
|
17
|
+
BatchSpanProcessor,
|
|
18
|
+
ConsoleSpanExporter,
|
|
19
|
+
SimpleSpanProcessor,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from netra.config import Config
|
|
23
|
+
from netra.exporters import FilteringSpanExporter
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
_provider_install_lock = threading.Lock()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Tracer:
|
|
31
|
+
"""
|
|
32
|
+
Configures Netra's OpenTelemetry tracer with OTLP exporter (or Console exporter as fallback)
|
|
33
|
+
and appropriate span processor.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, cfg: Config) -> None:
|
|
37
|
+
"""Initialize the Netra tracer with the provided configuration.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
cfg: Configuration object with tracer settings
|
|
41
|
+
"""
|
|
42
|
+
self.cfg = cfg
|
|
43
|
+
self._setup_tracer()
|
|
44
|
+
|
|
45
|
+
def _setup_tracer(self) -> None:
|
|
46
|
+
"""Set up the OpenTelemetry tracer with appropriate exporters and processors.
|
|
47
|
+
|
|
48
|
+
Creates a resource with service name and custom attributes,
|
|
49
|
+
configures the appropriate exporter (OTLP or Console fallback),
|
|
50
|
+
and sets up either a batch or simple span processor based on configuration.
|
|
51
|
+
"""
|
|
52
|
+
# Create Resource with service.name + custom attributes
|
|
53
|
+
resource_attrs: Dict[str, Any] = {
|
|
54
|
+
SERVICE_NAME: self.cfg.app_name,
|
|
55
|
+
DEPLOYMENT_ENVIRONMENT: self.cfg.environment,
|
|
56
|
+
}
|
|
57
|
+
if self.cfg.resource_attributes:
|
|
58
|
+
resource_attrs.update(self.cfg.resource_attributes)
|
|
59
|
+
resource = Resource(attributes=resource_attrs)
|
|
60
|
+
|
|
61
|
+
# Build TracerProvider
|
|
62
|
+
current_provider = trace.get_tracer_provider()
|
|
63
|
+
if isinstance(current_provider, sdk_trace.TracerProvider):
|
|
64
|
+
provider = current_provider
|
|
65
|
+
logger.info("Reusing existing TracerProvider. Possible loss of Resource attributes")
|
|
66
|
+
else:
|
|
67
|
+
provider = TracerProvider(resource=resource)
|
|
68
|
+
trace.set_tracer_provider(provider)
|
|
69
|
+
logger.info("Using Netra TracerProvider")
|
|
70
|
+
|
|
71
|
+
with _provider_install_lock:
|
|
72
|
+
if getattr(provider, "_netra_processors_installed", False):
|
|
73
|
+
logger.info("Netra processors already installed on provider; skipping setup")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if not self.cfg.otlp_endpoint:
|
|
77
|
+
logger.warning("OTLP endpoint not provided, falling back to console exporter")
|
|
78
|
+
exporter = ConsoleSpanExporter()
|
|
79
|
+
else:
|
|
80
|
+
exporter = OTLPSpanExporter(
|
|
81
|
+
endpoint=self._format_endpoint(self.cfg.otlp_endpoint),
|
|
82
|
+
headers=self.cfg.headers,
|
|
83
|
+
)
|
|
84
|
+
original_exporter = exporter
|
|
85
|
+
try:
|
|
86
|
+
patterns = getattr(self.cfg, "blocked_spans", None) or []
|
|
87
|
+
exporter = FilteringSpanExporter(exporter, patterns)
|
|
88
|
+
if patterns:
|
|
89
|
+
logger.info("Enabled FilteringSpanExporter with %d global pattern(s)", len(patterns))
|
|
90
|
+
else:
|
|
91
|
+
logger.info("Enabled FilteringSpanExporter with local-only rules")
|
|
92
|
+
except (ValueError, TypeError) as e:
|
|
93
|
+
logger.warning("Failed to enable FilteringSpanExporter: %s; using unwrapped exporter", e)
|
|
94
|
+
exporter = original_exporter
|
|
95
|
+
|
|
96
|
+
from netra.processors import (
|
|
97
|
+
InstrumentationSpanProcessor,
|
|
98
|
+
LocalFilteringSpanProcessor,
|
|
99
|
+
ScrubbingSpanProcessor,
|
|
100
|
+
SessionSpanProcessor,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
provider.add_span_processor(LocalFilteringSpanProcessor())
|
|
104
|
+
provider.add_span_processor(InstrumentationSpanProcessor())
|
|
105
|
+
provider.add_span_processor(SessionSpanProcessor())
|
|
106
|
+
|
|
107
|
+
if self.cfg.enable_scrubbing:
|
|
108
|
+
provider.add_span_processor(ScrubbingSpanProcessor()) # type: ignore[no-untyped-call]
|
|
109
|
+
|
|
110
|
+
if self.cfg.disable_batch:
|
|
111
|
+
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
112
|
+
else:
|
|
113
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
114
|
+
|
|
115
|
+
setattr(provider, "_netra_processors_installed", True)
|
|
116
|
+
|
|
117
|
+
logger.info(
|
|
118
|
+
"Netra initialized: endpoint=%s, disable_batch=%s",
|
|
119
|
+
self.cfg.otlp_endpoint,
|
|
120
|
+
self.cfg.disable_batch,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _format_endpoint(self, endpoint: str) -> str:
|
|
124
|
+
"""Format the OTLP endpoint URL to ensure it ends with '/v1/traces'.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
endpoint: Base OTLP endpoint URL
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Properly formatted endpoint URL
|
|
131
|
+
"""
|
|
132
|
+
if not endpoint.endswith("/v1/traces"):
|
|
133
|
+
return endpoint.rstrip("/") + "/v1/traces"
|
|
134
|
+
return endpoint
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.47"
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "netra-sdk"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.47"
|
|
8
8
|
description = "A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Sooraj Thomas",email = "sooraj@keyvalue.systems"}
|
netra_sdk-0.1.45/netra/tracer.py
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""Netra OpenTelemetry tracer configuration module.
|
|
2
|
-
|
|
3
|
-
This module handles the initialization and configuration of OpenTelemetry tracing,
|
|
4
|
-
including exporter setup and span processor configuration.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Any, Dict
|
|
9
|
-
|
|
10
|
-
from opentelemetry import trace
|
|
11
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
12
|
-
from opentelemetry.sdk.resources import DEPLOYMENT_ENVIRONMENT, SERVICE_NAME, Resource
|
|
13
|
-
from opentelemetry.sdk.trace import TracerProvider
|
|
14
|
-
from opentelemetry.sdk.trace.export import (
|
|
15
|
-
BatchSpanProcessor,
|
|
16
|
-
ConsoleSpanExporter,
|
|
17
|
-
SimpleSpanProcessor,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
from netra.config import Config
|
|
21
|
-
from netra.exporters import FilteringSpanExporter
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class Tracer:
|
|
27
|
-
"""
|
|
28
|
-
Configures Netra's OpenTelemetry tracer with OTLP exporter (or Console exporter as fallback)
|
|
29
|
-
and appropriate span processor.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __init__(self, cfg: Config) -> None:
|
|
33
|
-
"""Initialize the Netra tracer with the provided configuration.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
cfg: Configuration object with tracer settings
|
|
37
|
-
"""
|
|
38
|
-
self.cfg = cfg
|
|
39
|
-
self._setup_tracer()
|
|
40
|
-
|
|
41
|
-
def _setup_tracer(self) -> None:
|
|
42
|
-
"""Set up the OpenTelemetry tracer with appropriate exporters and processors.
|
|
43
|
-
|
|
44
|
-
Creates a resource with service name and custom attributes,
|
|
45
|
-
configures the appropriate exporter (OTLP or Console fallback),
|
|
46
|
-
and sets up either a batch or simple span processor based on configuration.
|
|
47
|
-
"""
|
|
48
|
-
# Create Resource with service.name + custom attributes
|
|
49
|
-
resource_attrs: Dict[str, Any] = {
|
|
50
|
-
SERVICE_NAME: self.cfg.app_name,
|
|
51
|
-
DEPLOYMENT_ENVIRONMENT: self.cfg.environment,
|
|
52
|
-
}
|
|
53
|
-
if self.cfg.resource_attributes:
|
|
54
|
-
resource_attrs.update(self.cfg.resource_attributes)
|
|
55
|
-
resource = Resource(attributes=resource_attrs)
|
|
56
|
-
|
|
57
|
-
# Build TracerProvider
|
|
58
|
-
provider = TracerProvider(resource=resource)
|
|
59
|
-
|
|
60
|
-
# Configure exporter based on configuration
|
|
61
|
-
if not self.cfg.otlp_endpoint:
|
|
62
|
-
logger.warning("OTLP endpoint not provided, falling back to console exporter")
|
|
63
|
-
exporter = ConsoleSpanExporter()
|
|
64
|
-
else:
|
|
65
|
-
exporter = OTLPSpanExporter(
|
|
66
|
-
endpoint=self._format_endpoint(self.cfg.otlp_endpoint),
|
|
67
|
-
headers=self.cfg.headers,
|
|
68
|
-
)
|
|
69
|
-
# Wrap exporter with filtering to support both global and local (baggage-based) rules
|
|
70
|
-
try:
|
|
71
|
-
patterns = getattr(self.cfg, "blocked_spans", None) or []
|
|
72
|
-
exporter = FilteringSpanExporter(exporter, patterns)
|
|
73
|
-
if patterns:
|
|
74
|
-
logger.info("Enabled FilteringSpanExporter with %d global pattern(s)", len(patterns))
|
|
75
|
-
else:
|
|
76
|
-
logger.info("Enabled FilteringSpanExporter with local-only rules")
|
|
77
|
-
except Exception as e:
|
|
78
|
-
logger.warning("Failed to enable FilteringSpanExporter: %s", e)
|
|
79
|
-
# Add span processors: first instrumentation wrapper, then session processor
|
|
80
|
-
from netra.processors import (
|
|
81
|
-
InstrumentationSpanProcessor,
|
|
82
|
-
LocalFilteringSpanProcessor,
|
|
83
|
-
ScrubbingSpanProcessor,
|
|
84
|
-
SessionSpanProcessor,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Apply local filtering propagation first so later processors and spans see attributes
|
|
88
|
-
provider.add_span_processor(LocalFilteringSpanProcessor())
|
|
89
|
-
provider.add_span_processor(InstrumentationSpanProcessor())
|
|
90
|
-
provider.add_span_processor(SessionSpanProcessor())
|
|
91
|
-
|
|
92
|
-
# Add scrubbing processor if enabled
|
|
93
|
-
if self.cfg.enable_scrubbing:
|
|
94
|
-
provider.add_span_processor(ScrubbingSpanProcessor()) # type: ignore[no-untyped-call]
|
|
95
|
-
|
|
96
|
-
# Install appropriate span processor
|
|
97
|
-
if self.cfg.disable_batch:
|
|
98
|
-
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
99
|
-
else:
|
|
100
|
-
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
101
|
-
|
|
102
|
-
# Set global tracer provider
|
|
103
|
-
trace.set_tracer_provider(provider)
|
|
104
|
-
logger.info(
|
|
105
|
-
"Netra TracerProvider initialized: endpoint=%s, disable_batch=%s",
|
|
106
|
-
self.cfg.otlp_endpoint,
|
|
107
|
-
self.cfg.disable_batch,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
def _format_endpoint(self, endpoint: str) -> str:
|
|
111
|
-
"""Format the OTLP endpoint URL to ensure it ends with '/v1/traces'.
|
|
112
|
-
|
|
113
|
-
Args:
|
|
114
|
-
endpoint: Base OTLP endpoint URL
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
Properly formatted endpoint URL
|
|
118
|
-
"""
|
|
119
|
-
if not endpoint.endswith("/v1/traces"):
|
|
120
|
-
return endpoint.rstrip("/") + "/v1/traces"
|
|
121
|
-
return endpoint
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.45"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|