netra-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of netra-sdk might be problematic. Click here for more details.
- netra/__init__.py +148 -0
- netra/anonymizer/__init__.py +7 -0
- netra/anonymizer/anonymizer.py +79 -0
- netra/anonymizer/base.py +159 -0
- netra/anonymizer/fp_anonymizer.py +182 -0
- netra/config.py +111 -0
- netra/decorators.py +167 -0
- netra/exceptions/__init__.py +6 -0
- netra/exceptions/injection.py +33 -0
- netra/exceptions/pii.py +46 -0
- netra/input_scanner.py +142 -0
- netra/instrumentation/__init__.py +257 -0
- netra/instrumentation/aiohttp/__init__.py +378 -0
- netra/instrumentation/aiohttp/version.py +1 -0
- netra/instrumentation/cohere/__init__.py +446 -0
- netra/instrumentation/cohere/version.py +1 -0
- netra/instrumentation/google_genai/__init__.py +506 -0
- netra/instrumentation/google_genai/config.py +5 -0
- netra/instrumentation/google_genai/utils.py +31 -0
- netra/instrumentation/google_genai/version.py +1 -0
- netra/instrumentation/httpx/__init__.py +545 -0
- netra/instrumentation/httpx/version.py +1 -0
- netra/instrumentation/instruments.py +78 -0
- netra/instrumentation/mistralai/__init__.py +545 -0
- netra/instrumentation/mistralai/config.py +5 -0
- netra/instrumentation/mistralai/utils.py +30 -0
- netra/instrumentation/mistralai/version.py +1 -0
- netra/instrumentation/weaviate/__init__.py +121 -0
- netra/instrumentation/weaviate/version.py +1 -0
- netra/pii.py +757 -0
- netra/processors/__init__.py +4 -0
- netra/processors/session_span_processor.py +55 -0
- netra/processors/span_aggregation_processor.py +365 -0
- netra/scanner.py +104 -0
- netra/session.py +185 -0
- netra/session_manager.py +96 -0
- netra/tracer.py +99 -0
- netra/version.py +1 -0
- netra_sdk-0.1.0.dist-info/LICENCE +201 -0
- netra_sdk-0.1.0.dist-info/METADATA +573 -0
- netra_sdk-0.1.0.dist-info/RECORD +42 -0
- netra_sdk-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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,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"
|