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.

Files changed (42) hide show
  1. netra/__init__.py +148 -0
  2. netra/anonymizer/__init__.py +7 -0
  3. netra/anonymizer/anonymizer.py +79 -0
  4. netra/anonymizer/base.py +159 -0
  5. netra/anonymizer/fp_anonymizer.py +182 -0
  6. netra/config.py +111 -0
  7. netra/decorators.py +167 -0
  8. netra/exceptions/__init__.py +6 -0
  9. netra/exceptions/injection.py +33 -0
  10. netra/exceptions/pii.py +46 -0
  11. netra/input_scanner.py +142 -0
  12. netra/instrumentation/__init__.py +257 -0
  13. netra/instrumentation/aiohttp/__init__.py +378 -0
  14. netra/instrumentation/aiohttp/version.py +1 -0
  15. netra/instrumentation/cohere/__init__.py +446 -0
  16. netra/instrumentation/cohere/version.py +1 -0
  17. netra/instrumentation/google_genai/__init__.py +506 -0
  18. netra/instrumentation/google_genai/config.py +5 -0
  19. netra/instrumentation/google_genai/utils.py +31 -0
  20. netra/instrumentation/google_genai/version.py +1 -0
  21. netra/instrumentation/httpx/__init__.py +545 -0
  22. netra/instrumentation/httpx/version.py +1 -0
  23. netra/instrumentation/instruments.py +78 -0
  24. netra/instrumentation/mistralai/__init__.py +545 -0
  25. netra/instrumentation/mistralai/config.py +5 -0
  26. netra/instrumentation/mistralai/utils.py +30 -0
  27. netra/instrumentation/mistralai/version.py +1 -0
  28. netra/instrumentation/weaviate/__init__.py +121 -0
  29. netra/instrumentation/weaviate/version.py +1 -0
  30. netra/pii.py +757 -0
  31. netra/processors/__init__.py +4 -0
  32. netra/processors/session_span_processor.py +55 -0
  33. netra/processors/span_aggregation_processor.py +365 -0
  34. netra/scanner.py +104 -0
  35. netra/session.py +185 -0
  36. netra/session_manager.py +96 -0
  37. netra/tracer.py +99 -0
  38. netra/version.py +1 -0
  39. netra_sdk-0.1.0.dist-info/LICENCE +201 -0
  40. netra_sdk-0.1.0.dist-info/METADATA +573 -0
  41. netra_sdk-0.1.0.dist-info/RECORD +42 -0
  42. netra_sdk-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,506 @@
1
+ """OpenTelemetry Google GenAI API instrumentation"""
2
+
3
+ import logging
4
+ import os
5
+ import types
6
+ from typing import Any, Callable, Collection, Dict, Optional, Tuple, Union
7
+
8
+ from opentelemetry import context as context_api
9
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
10
+ from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
11
+ from opentelemetry.semconv_ai import (
12
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
13
+ LLMRequestTypeValues,
14
+ SpanAttributes,
15
+ )
16
+ from opentelemetry.trace import SpanKind, get_tracer, set_span_in_context
17
+ from opentelemetry.trace.status import Status, StatusCode
18
+ from wrapt import wrap_function_wrapper
19
+
20
+ from netra.instrumentation.google_genai.config import Config
21
+ from netra.instrumentation.google_genai.utils import dont_throw
22
+ from netra.instrumentation.google_genai.version import __version__
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _instruments = ("google-genai >= 0.1.0",)
27
+
28
+ WRAPPED_METHODS = [
29
+ {
30
+ "package": "google.genai.models",
31
+ "object": "Models",
32
+ "method": "generate_content",
33
+ "span_name": "genai.generate_content",
34
+ "is_async": False,
35
+ },
36
+ {
37
+ "package": "google.genai.models",
38
+ "object": "Models",
39
+ "method": "generate_content_stream",
40
+ "span_name": "genai.generate_content_stream",
41
+ "is_async": False,
42
+ },
43
+ {
44
+ "package": "google.genai.models",
45
+ "object": "Models",
46
+ "method": "generate_images",
47
+ "span_name": "genai.generate_images",
48
+ "is_async": False,
49
+ },
50
+ {
51
+ "package": "google.genai.models",
52
+ "object": "Models",
53
+ "method": "generate_videos",
54
+ "span_name": "genai.generate_videos",
55
+ "is_async": False,
56
+ },
57
+ {
58
+ "package": "google.genai.models",
59
+ "object": "AsyncModels",
60
+ "method": "generate_content",
61
+ "span_name": "genai.generate_content_async",
62
+ "is_async": True,
63
+ },
64
+ {
65
+ "package": "google.genai.models",
66
+ "object": "AsyncModels",
67
+ "method": "generate_content_stream",
68
+ "span_name": "genai.generate_content_stream_async",
69
+ "is_async": True,
70
+ },
71
+ {
72
+ "package": "google.genai.models",
73
+ "object": "AsyncModels",
74
+ "method": "generate_images",
75
+ "span_name": "genai.generate_images_async",
76
+ "is_async": True,
77
+ },
78
+ {
79
+ "package": "google.genai.models",
80
+ "object": "AsyncModels",
81
+ "method": "generate_videos",
82
+ "span_name": "genai.generate_videos_async",
83
+ "is_async": True,
84
+ },
85
+ ]
86
+
87
+
88
+ def should_send_prompts() -> bool:
89
+ return (os.getenv("TRACELOOP_TRACE_CONTENT") or "true").lower() == "true" or context_api.get_value(
90
+ "override_enable_content_tracing"
91
+ )
92
+
93
+
94
+ def is_streaming_response(response: Any) -> bool:
95
+ return isinstance(response, types.GeneratorType)
96
+
97
+
98
+ def is_async_streaming_response(response: Any) -> bool:
99
+ return isinstance(response, types.AsyncGeneratorType)
100
+
101
+
102
+ def _set_span_attribute(span: Any, name: str, value: Any) -> None:
103
+ if value is not None:
104
+ if value != "":
105
+ span.set_attribute(name, value)
106
+ return
107
+
108
+
109
+ def _set_input_attributes(span: Any, args: tuple[Any, ...], kwargs: dict[str, Any], llm_model: str) -> None:
110
+ if not should_send_prompts():
111
+ return
112
+
113
+ # Handle contents parameter
114
+ if "contents" in kwargs:
115
+ contents = kwargs["contents"]
116
+ if isinstance(contents, str):
117
+ # Simple string content
118
+ _set_span_attribute(
119
+ span,
120
+ f"{SpanAttributes.LLM_PROMPTS}.0.content",
121
+ contents,
122
+ )
123
+ _set_span_attribute(
124
+ span,
125
+ f"{SpanAttributes.LLM_PROMPTS}.0.role",
126
+ "user",
127
+ )
128
+ elif isinstance(contents, list):
129
+ # List of content objects
130
+ for i, content in enumerate(contents):
131
+ if hasattr(content, "parts"):
132
+ for part in content.parts:
133
+ if hasattr(part, "text"):
134
+ _set_span_attribute(
135
+ span,
136
+ f"{SpanAttributes.LLM_PROMPTS}.{i}.content",
137
+ part.text,
138
+ )
139
+ _set_span_attribute(
140
+ span,
141
+ f"{SpanAttributes.LLM_PROMPTS}.{i}.role",
142
+ getattr(content, "role", "user"),
143
+ )
144
+ elif isinstance(content, str):
145
+ _set_span_attribute(
146
+ span,
147
+ f"{SpanAttributes.LLM_PROMPTS}.{i}.content",
148
+ content,
149
+ )
150
+ _set_span_attribute(
151
+ span,
152
+ f"{SpanAttributes.LLM_PROMPTS}.{i}.role",
153
+ "user",
154
+ )
155
+ elif args and len(args) > 0:
156
+ # Handle positional arguments
157
+ prompt = ""
158
+ for arg in args:
159
+ if isinstance(arg, str):
160
+ prompt = f"{prompt}{arg}\n"
161
+ elif isinstance(arg, list):
162
+ for subarg in arg:
163
+ prompt = f"{prompt}{subarg}\n"
164
+ if prompt:
165
+ _set_span_attribute(
166
+ span,
167
+ f"{SpanAttributes.LLM_PROMPTS}.0.content",
168
+ prompt,
169
+ )
170
+ _set_span_attribute(
171
+ span,
172
+ f"{SpanAttributes.LLM_PROMPTS}.0.role",
173
+ "user",
174
+ )
175
+
176
+ # Extract model from kwargs or args
177
+ model_name = kwargs.get("model", "unknown")
178
+ if model_name != "unknown":
179
+ llm_model = model_name
180
+
181
+ _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, llm_model)
182
+
183
+ # Handle config parameter which might contain generation settings
184
+ if "config" in kwargs and kwargs["config"]:
185
+ config = kwargs["config"]
186
+ if hasattr(config, "temperature"):
187
+ _set_span_attribute(span, SpanAttributes.LLM_REQUEST_TEMPERATURE, config.temperature)
188
+ if hasattr(config, "max_output_tokens"):
189
+ _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, config.max_output_tokens)
190
+ if hasattr(config, "top_p"):
191
+ _set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, config.top_p)
192
+ if hasattr(config, "top_k"):
193
+ _set_span_attribute(span, SpanAttributes.LLM_TOP_K, config.top_k)
194
+
195
+ return
196
+
197
+
198
+ @dont_throw
199
+ def _set_response_attributes(span: Any, response: Any, llm_model: str) -> None:
200
+ _set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, llm_model)
201
+
202
+ # Handle response attributes for google.genai package
203
+ if hasattr(response, "usage_metadata"):
204
+ usage = response.usage_metadata
205
+ if hasattr(usage, "total_token_count"):
206
+ _set_span_attribute(
207
+ span,
208
+ SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
209
+ usage.total_token_count,
210
+ )
211
+ if hasattr(usage, "candidates_token_count"):
212
+ _set_span_attribute(
213
+ span,
214
+ SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
215
+ usage.candidates_token_count,
216
+ )
217
+ if hasattr(usage, "prompt_token_count"):
218
+ _set_span_attribute(
219
+ span,
220
+ SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
221
+ usage.prompt_token_count,
222
+ )
223
+
224
+ # Handle response text
225
+ if hasattr(response, "text") and response.text:
226
+ _set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.0.content", response.text)
227
+ _set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.0.role", "assistant")
228
+ elif hasattr(response, "candidates") and response.candidates:
229
+ for index, candidate in enumerate(response.candidates):
230
+ if hasattr(candidate, "content") and hasattr(candidate.content, "parts"):
231
+ for part in candidate.content.parts:
232
+ if hasattr(part, "text"):
233
+ _set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.{index}.content", part.text)
234
+ _set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.{index}.role", "assistant")
235
+
236
+ return
237
+
238
+
239
+ def _handle_request(span: Any, args: tuple[Any, ...], kwargs: dict[str, Any], llm_model: str) -> None:
240
+ if span.is_recording():
241
+ _set_input_attributes(span, args, kwargs, llm_model)
242
+
243
+
244
+ @dont_throw
245
+ def _handle_response(span: Any, response: Any, llm_model: str) -> None:
246
+ if span.is_recording():
247
+ _set_response_attributes(span, response, llm_model)
248
+ span.set_status(Status(StatusCode.OK))
249
+
250
+
251
+ def _with_tracer_wrapper(func: Callable[..., Any]) -> Callable[..., Any]:
252
+ """Helper for providing tracer for wrapper functions."""
253
+
254
+ def _with_tracer(tracer: Any, to_wrap: dict[str, Any]) -> Callable[..., Any]:
255
+ def wrapper(wrapped: Callable[..., Any], instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
256
+ return func(tracer, to_wrap, wrapped, instance, args, kwargs)
257
+
258
+ return wrapper
259
+
260
+ return _with_tracer
261
+
262
+
263
+ def _build_from_streaming_response(span: Any, response: Any, llm_model: str, context_token: Any) -> Any:
264
+ complete_response = ""
265
+ try:
266
+ for item in response:
267
+ item_to_yield = item
268
+ if hasattr(item, "text"):
269
+ complete_response += str(item.text)
270
+ yield item_to_yield
271
+
272
+ _set_response_attributes(span, complete_response, llm_model)
273
+ span.set_status(Status(StatusCode.OK))
274
+ except Exception:
275
+ span.set_status(Status(StatusCode.ERROR))
276
+ raise
277
+ finally:
278
+ span.end()
279
+ context_api.detach(context_token)
280
+
281
+
282
+ async def _abuild_from_streaming_response(span: Any, response: Any, llm_model: str, context_token: Any) -> Any:
283
+ complete_response = ""
284
+ try:
285
+ async for item in response:
286
+ item_to_yield = item
287
+ if hasattr(item, "text"):
288
+ complete_response += str(item.text)
289
+ yield item_to_yield
290
+
291
+ _set_response_attributes(span, complete_response, llm_model)
292
+ span.set_status(Status(StatusCode.OK))
293
+ except Exception:
294
+ span.set_status(Status(StatusCode.ERROR))
295
+ raise
296
+ finally:
297
+ span.end()
298
+ context_api.detach(context_token)
299
+
300
+
301
+ @_with_tracer_wrapper
302
+ async def _awrap(
303
+ tracer: Any,
304
+ to_wrap: dict[str, Any],
305
+ wrapped: Callable[..., Any],
306
+ instance: Any,
307
+ args: tuple[Any, ...],
308
+ kwargs: dict[str, Any],
309
+ ) -> Any:
310
+ """Instruments and calls every function defined in TO_WRAP."""
311
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
312
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
313
+ ):
314
+ return await wrapped(*args, **kwargs)
315
+
316
+ llm_model = kwargs.get("model", "unknown")
317
+ if llm_model != "unknown":
318
+ llm_model = llm_model.replace("models/", "")
319
+
320
+ name = to_wrap.get("span_name")
321
+ method_name = to_wrap.get("method")
322
+
323
+ if method_name == "generate_content_stream":
324
+ span = tracer.start_span(
325
+ name,
326
+ kind=SpanKind.CLIENT,
327
+ attributes={
328
+ SpanAttributes.LLM_SYSTEM: "Gemini",
329
+ SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
330
+ },
331
+ )
332
+
333
+ ctx = set_span_in_context(span)
334
+ token = context_api.attach(ctx)
335
+
336
+ try:
337
+ _handle_request(span, args, kwargs, llm_model)
338
+
339
+ response = await wrapped(*args, **kwargs)
340
+
341
+ if response:
342
+ if is_streaming_response(response):
343
+ return _build_from_streaming_response(span, response, llm_model, token)
344
+ elif is_async_streaming_response(response):
345
+ return _abuild_from_streaming_response(span, response, llm_model, token)
346
+ else:
347
+ _handle_response(span, response, llm_model)
348
+ span.end()
349
+ context_api.detach(token)
350
+ else:
351
+ span.set_status(Status(StatusCode.ERROR))
352
+ span.end()
353
+ context_api.detach(token)
354
+
355
+ return response
356
+ except Exception:
357
+ span.set_status(Status(StatusCode.ERROR))
358
+ span.end()
359
+ context_api.detach(token)
360
+ raise
361
+ else:
362
+ with tracer.start_as_current_span(
363
+ name,
364
+ kind=SpanKind.CLIENT,
365
+ attributes={
366
+ SpanAttributes.LLM_SYSTEM: "Gemini",
367
+ SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
368
+ },
369
+ ) as span:
370
+
371
+ _handle_request(span, args, kwargs, llm_model)
372
+
373
+ response = await wrapped(*args, **kwargs)
374
+ ctx = set_span_in_context(span)
375
+ token = context_api.attach(ctx)
376
+
377
+ if response:
378
+ if is_streaming_response(response):
379
+ return _build_from_streaming_response(span, response, llm_model, token)
380
+ elif is_async_streaming_response(response):
381
+ return _abuild_from_streaming_response(span, response, llm_model, token)
382
+ else:
383
+ _handle_response(span, response, llm_model)
384
+
385
+ return response
386
+
387
+
388
+ @_with_tracer_wrapper
389
+ def _wrap(
390
+ tracer: Any,
391
+ to_wrap: dict[str, Any],
392
+ wrapped: Callable[..., Any],
393
+ instance: Any,
394
+ args: tuple[Any, ...],
395
+ kwargs: dict[str, Any],
396
+ ) -> Any:
397
+ """Instruments and calls every function defined in TO_WRAP."""
398
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
399
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
400
+ ):
401
+ return wrapped(*args, **kwargs)
402
+
403
+ llm_model = kwargs.get("model", "unknown")
404
+ if llm_model != "unknown":
405
+ llm_model = llm_model.replace("models/", "")
406
+
407
+ name = to_wrap.get("span_name")
408
+ method_name = to_wrap.get("method")
409
+
410
+ if method_name == "generate_content_stream":
411
+ span = tracer.start_span(
412
+ name,
413
+ kind=SpanKind.CLIENT,
414
+ attributes={
415
+ SpanAttributes.LLM_SYSTEM: "Gemini",
416
+ SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
417
+ },
418
+ )
419
+
420
+ ctx = set_span_in_context(span)
421
+ token = context_api.attach(ctx)
422
+
423
+ try:
424
+ _handle_request(span, args, kwargs, llm_model)
425
+
426
+ response = wrapped(*args, **kwargs)
427
+
428
+ if response:
429
+ if is_streaming_response(response):
430
+ return _build_from_streaming_response(span, response, llm_model, token)
431
+ elif is_async_streaming_response(response):
432
+ return _abuild_from_streaming_response(span, response, llm_model, token)
433
+ else:
434
+ _handle_response(span, response, llm_model)
435
+ span.end()
436
+ context_api.detach(token)
437
+ else:
438
+ span.set_status(Status(StatusCode.ERROR))
439
+ span.end()
440
+ context_api.detach(token)
441
+
442
+ return response
443
+ except Exception:
444
+ span.set_status(Status(StatusCode.ERROR))
445
+ span.end()
446
+ context_api.detach(token)
447
+ raise
448
+ else:
449
+ with tracer.start_as_current_span(
450
+ name,
451
+ kind=SpanKind.CLIENT,
452
+ attributes={
453
+ SpanAttributes.LLM_SYSTEM: "Gemini",
454
+ SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
455
+ },
456
+ ) as span:
457
+
458
+ _handle_request(span, args, kwargs, llm_model)
459
+ response = wrapped(*args, **kwargs)
460
+ ctx = set_span_in_context(span)
461
+ token = context_api.attach(ctx)
462
+ if response:
463
+ if is_streaming_response(response):
464
+ return _build_from_streaming_response(span, response, llm_model, token)
465
+ elif is_async_streaming_response(response):
466
+ return _abuild_from_streaming_response(span, response, llm_model, token)
467
+ else:
468
+ _handle_response(span, response, llm_model)
469
+ return response
470
+
471
+
472
+ class GoogleGenAiInstrumentor(BaseInstrumentor): # type: ignore
473
+ """An instrumentor for Google GenAI's client library."""
474
+
475
+ def __init__(self, exception_logger: Optional[Callable[[Exception], None]]) -> None:
476
+ # Initialize the parent class
477
+ super().__init__()
478
+ # Set the exception logger in Config
479
+ if exception_logger is not None:
480
+ Config.exception_logger = exception_logger
481
+
482
+ def instrumentation_dependencies(self) -> Collection[str]:
483
+ return _instruments
484
+
485
+ def _instrument(self, **kwargs: Any) -> None:
486
+ tracer_provider = kwargs.get("tracer_provider")
487
+ tracer = get_tracer(__name__, __version__, tracer_provider)
488
+ for wrapped_method in WRAPPED_METHODS:
489
+ wrap_package = wrapped_method.get("package")
490
+ wrap_object = wrapped_method.get("object")
491
+ wrap_method = wrapped_method.get("method")
492
+
493
+ wrap_function_wrapper(
494
+ wrap_package,
495
+ f"{wrap_object}.{wrap_method}",
496
+ (_awrap(tracer, wrapped_method) if wrapped_method.get("is_async") else _wrap(tracer, wrapped_method)),
497
+ )
498
+
499
+ def _uninstrument(self, **kwargs: Any) -> None:
500
+ for wrapped_method in WRAPPED_METHODS:
501
+ wrap_package = wrapped_method.get("package")
502
+ wrap_object = wrapped_method.get("object")
503
+ unwrap(
504
+ f"{wrap_package}.{wrap_object}",
505
+ wrapped_method.get("method", ""),
506
+ )
@@ -0,0 +1,5 @@
1
+ from typing import Callable, Optional
2
+
3
+
4
+ class Config:
5
+ exception_logger: Optional[Callable[[Exception], None]] = None
@@ -0,0 +1,31 @@
1
+ import logging
2
+ import traceback
3
+ from typing import Any, Callable
4
+
5
+ from netra.instrumentation.google_genai.config import Config
6
+
7
+
8
+ def dont_throw(func: Callable[..., Any]) -> Callable[..., Any]:
9
+ """
10
+ A decorator that wraps the passed in function and logs exceptions instead of throwing them.
11
+
12
+ @param func: The function to wrap
13
+ @return: The wrapper function
14
+ """
15
+ # Obtain a logger specific to the function's module
16
+ logger = logging.getLogger(func.__module__)
17
+
18
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
19
+ try:
20
+ return func(*args, **kwargs)
21
+ except Exception as e:
22
+ logger.debug(
23
+ "OpenLLMetry failed to trace in %s, error: %s",
24
+ func.__name__,
25
+ traceback.format_exc(),
26
+ )
27
+ if Config.exception_logger:
28
+ Config.exception_logger(e)
29
+ return None
30
+
31
+ return wrapper
@@ -0,0 +1 @@
1
+ __version__ = "1.20.0"