netra-sdk 0.1.6__py3-none-any.whl → 0.1.9__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 CHANGED
@@ -8,8 +8,8 @@ from .config import Config
8
8
 
9
9
  # Instrumentor functions
10
10
  from .instrumentation import init_instrumentations
11
- from .session import Session
12
11
  from .session_manager import SessionManager
12
+ from .span_wrapper import ActionModel, SpanWrapper, UsageModel
13
13
  from .tracer import Tracer
14
14
 
15
15
  logger = logging.getLogger(__name__)
@@ -133,16 +133,16 @@ class Netra:
133
133
  SessionManager.set_custom_event(event_name, attributes)
134
134
 
135
135
  @classmethod
136
- def start_session(
136
+ def start_span(
137
137
  cls,
138
138
  name: str,
139
139
  attributes: Optional[Dict[str, str]] = None,
140
140
  module_name: str = "combat_sdk",
141
- ) -> Session:
141
+ ) -> SpanWrapper:
142
142
  """
143
143
  Start a new session.
144
144
  """
145
- return Session(name, attributes, module_name)
145
+ return SpanWrapper(name, attributes, module_name)
146
146
 
147
147
 
148
- __all__ = ["Netra"]
148
+ __all__ = ["Netra", "UsageModel", "ActionModel"]
netra/config.py CHANGED
@@ -51,7 +51,7 @@ class Config:
51
51
  if isinstance(headers, str):
52
52
  self.headers = parse_env_headers(headers)
53
53
 
54
- if self.otlp_endpoint == "https://api.dev.getcombat.ai" and not self.api_key:
54
+ if self.otlp_endpoint in ["https://api.dev.getcombat.ai", "https://api.eu.getnetra.ai"] and not self.api_key:
55
55
  print("Error: Missing Netra API key, go to https://app.dev.getcombat.ai/api-key to create one")
56
56
  print("Set the NETRA_API_KEY environment variable to the key")
57
57
  return
@@ -59,7 +59,7 @@ class Config:
59
59
  # Handle API key authentication based on OTLP endpoint
60
60
  if self.api_key and self.otlp_endpoint:
61
61
  # For Netra endpoints, use x-api-key header
62
- if "getcombat" in self.otlp_endpoint.lower():
62
+ if "getcombat" in self.otlp_endpoint.lower() or "getnetra" in self.otlp_endpoint.lower():
63
63
  if not self.headers:
64
64
  self.headers = {"x-api-key": self.api_key}
65
65
  elif "x-api-key" not in self.headers:
@@ -86,6 +86,11 @@ class Config:
86
86
  env_tc = os.getenv("NETRA_TRACE_CONTENT")
87
87
  self.trace_content = False if (env_tc is not None and env_tc.lower() in ("0", "false")) else True
88
88
 
89
+ if not self.trace_content:
90
+ os.environ["TRACELOOP_TRACE_CONTENT"] = "false"
91
+ else:
92
+ os.environ["TRACELOOP_TRACE_CONTENT"] = "true"
93
+
89
94
  # 7. Environment: param override, else env
90
95
  if environment is not None:
91
96
  self.environment = environment
@@ -37,6 +37,7 @@ def init_instrumentations(
37
37
  Instruments.QDRANT,
38
38
  Instruments.GOOGLE_GENERATIVEAI,
39
39
  Instruments.MISTRAL,
40
+ Instruments.OPENAI,
40
41
  }
41
42
  )
42
43
  if instruments is not None and traceloop_instruments is None and traceloop_block_instruments is None:
@@ -82,6 +83,10 @@ def init_instrumentations(
82
83
  if CustomInstruments.MISTRALAI in netra_custom_instruments:
83
84
  init_mistral_instrumentor()
84
85
 
86
+ # Initialize OpenAI instrumentation.
87
+ if CustomInstruments.OPENAI in netra_custom_instruments:
88
+ init_openai_instrumentation()
89
+
85
90
  # Initialize aio_pika instrumentation.
86
91
  if CustomInstruments.AIO_PIKA in netra_custom_instruments:
87
92
  init_aio_pika_instrumentation()
@@ -282,7 +287,7 @@ def init_fastapi_instrumentation() -> bool:
282
287
  """
283
288
  try:
284
289
  if is_package_installed("fastapi"):
285
- from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
290
+ from netra.instrumentation.fastapi import FastAPIInstrumentor
286
291
 
287
292
  instrumentor = FastAPIInstrumentor()
288
293
  if not instrumentor.is_instrumented_by_opentelemetry:
@@ -416,6 +421,26 @@ def init_mistral_instrumentor() -> bool:
416
421
  return False
417
422
 
418
423
 
424
+ def init_openai_instrumentation() -> bool:
425
+ """Initialize OpenAI instrumentation.
426
+
427
+ Returns:
428
+ bool: True if initialization was successful, False otherwise.
429
+ """
430
+ try:
431
+ if is_package_installed("openai"):
432
+ from netra.instrumentation.openai import NetraOpenAIInstrumentor
433
+
434
+ instrumentor = NetraOpenAIInstrumentor()
435
+ if not instrumentor.is_instrumented_by_opentelemetry:
436
+ instrumentor.instrument()
437
+ return True
438
+ except Exception as e:
439
+ logging.error(f"Error initializing OpenAI instrumentor: {e}")
440
+ Telemetry().log_exception(e)
441
+ return False
442
+
443
+
419
444
  def init_aio_pika_instrumentation() -> bool:
420
445
  """Initialize aio_pika instrumentation."""
421
446
  try:
@@ -0,0 +1,363 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import logging
5
+ import types
6
+ from typing import Any, Collection, Dict, Iterable, Optional, Union
7
+
8
+ import fastapi
9
+ import httpx
10
+ from fastapi import HTTPException
11
+ from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
12
+ from opentelemetry.instrumentation.asgi.types import (
13
+ ClientRequestHook,
14
+ ClientResponseHook,
15
+ ServerRequestHook,
16
+ )
17
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
18
+ from opentelemetry.metrics import MeterProvider, get_meter
19
+ from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE
20
+ from opentelemetry.trace import Span, Status, StatusCode, TracerProvider, get_tracer
21
+ from opentelemetry.util.http import (
22
+ get_excluded_urls,
23
+ parse_excluded_urls,
24
+ sanitize_method,
25
+ )
26
+ from starlette.applications import Starlette
27
+ from starlette.middleware.errors import ServerErrorMiddleware
28
+ from starlette.routing import Match
29
+ from starlette.types import ASGIApp
30
+
31
+ _excluded_urls_from_env = get_excluded_urls("FASTAPI")
32
+ _logger = logging.getLogger(__name__)
33
+
34
+
35
+ class SpanAttributeMonitor:
36
+ """Monitor to track span attribute changes and detect HTTP status codes."""
37
+
38
+ def __init__(self, span: Span, error_status_codes: set[int], error_messages: dict[Union[int, range], str]):
39
+ self.span = span
40
+ self.error_status_codes = error_status_codes
41
+ self.error_messages = error_messages
42
+ self.original_set_attribute = span.set_attribute
43
+ self.span.set_attribute = self._monitored_set_attribute
44
+
45
+ def _monitored_set_attribute(self, key: str, value: Any) -> None:
46
+ """Monitor set_attribute calls to detect HTTP status codes."""
47
+ # Call the original set_attribute method
48
+ self.original_set_attribute(key, value)
49
+
50
+ # Check if this is an HTTP status code attribute
51
+ if key == "http.status_code" and isinstance(value, int):
52
+ if value in self.error_status_codes:
53
+ self._record_error_for_span(value)
54
+
55
+ def _record_error_for_span(self, status_code: int) -> None:
56
+ """Record an HTTPException for the given span based on the status code."""
57
+ if not self.span or not self.span.is_recording():
58
+ return
59
+
60
+ # Get custom error message if available
61
+ error_message = self._get_error_message(status_code)
62
+
63
+ # Create and record the HTTPException
64
+ exception = HTTPException(status_code=status_code, detail=error_message)
65
+ self.span.record_exception(exception)
66
+
67
+ # Set span status to error for 5xx errors
68
+ if httpx.codes.is_error(status_code):
69
+ self.span.set_status(Status(StatusCode.ERROR, error_message))
70
+
71
+ _logger.debug(f"Recorded HTTPException for HTTP {status_code}: {error_message}")
72
+
73
+ def _get_error_message(self, status_code: int) -> str:
74
+ """Get the appropriate error message for a status code."""
75
+ # Check for exact status code match
76
+ if status_code in self.error_messages:
77
+ return self.error_messages[status_code]
78
+
79
+ # Check for range matches
80
+ for key, message in self.error_messages.items():
81
+ if isinstance(key, range) and status_code in key:
82
+ return message
83
+
84
+ # Default messages based on status code ranges
85
+ if 400 <= status_code < 500:
86
+ return f"Client Error: HTTP {status_code}"
87
+ elif 500 <= status_code < 600:
88
+ return f"Server Error: HTTP {status_code}"
89
+ else:
90
+ return f"HTTP {status_code} Error"
91
+
92
+
93
+ class StatusCodeMonitoringMiddleware:
94
+ """Middleware that monitors spans for HTTP status codes and records exceptions."""
95
+
96
+ def __init__(
97
+ self,
98
+ app: ASGIApp,
99
+ error_status_codes: Optional[Iterable[int]] = None,
100
+ error_messages: Optional[Dict[Union[int, range], str]] = None,
101
+ ):
102
+ self.app = app
103
+ self.error_status_codes = set(error_status_codes or range(400, 600))
104
+ self.error_messages = error_messages or {}
105
+
106
+ async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
107
+ if scope["type"] not in ("http", "websocket"):
108
+ await self.app(scope, receive, send)
109
+ return
110
+
111
+ # Get the current span
112
+ from opentelemetry.trace import get_current_span
113
+
114
+ span = get_current_span()
115
+
116
+ # Set up span monitoring if we have a valid span
117
+ monitor = None
118
+ if span and span.is_recording():
119
+ monitor = SpanAttributeMonitor(span, self.error_status_codes, self.error_messages)
120
+
121
+ try:
122
+ await self.app(scope, receive, send)
123
+ finally:
124
+ # Restore original set_attribute method
125
+ if monitor:
126
+ span.set_attribute = monitor.original_set_attribute
127
+
128
+
129
+ class FastAPIInstrumentor(BaseInstrumentor): # type: ignore[misc]
130
+ """An instrumentor for FastAPI that records HTTPExceptions for HTTP error status codes.
131
+
132
+ This instrumentor follows the OpenTelemetry FastAPI instrumentation pattern while adding
133
+ the capability to monitor http.status_code attributes and automatically record HTTPExceptions
134
+ for error status codes.
135
+ """
136
+
137
+ _original_fastapi: Optional[type[fastapi.FastAPI]] = None
138
+
139
+ @staticmethod
140
+ def instrument_app(
141
+ app: fastapi.FastAPI,
142
+ server_request_hook: Optional[ServerRequestHook] = None,
143
+ client_request_hook: Optional[ClientRequestHook] = None,
144
+ client_response_hook: Optional[ClientResponseHook] = None,
145
+ tracer_provider: Optional[TracerProvider] = None,
146
+ meter_provider: Optional[MeterProvider] = None,
147
+ excluded_urls: Optional[str] = None,
148
+ http_capture_headers_server_request: Optional[list[str]] = None,
149
+ http_capture_headers_server_response: Optional[list[str]] = None,
150
+ http_capture_headers_sanitize_fields: Optional[list[str]] = None,
151
+ exclude_spans: Optional[list[str]] = None,
152
+ error_status_codes: Optional[Iterable[int]] = None,
153
+ error_messages: Optional[Dict[Union[int, range], str]] = None,
154
+ ) -> None:
155
+ """Instrument an uninstrumented FastAPI application with status code monitoring.
156
+
157
+ Args:
158
+ app: The fastapi ASGI application callable to forward requests to.
159
+ server_request_hook: Optional callback which is called with the server span and ASGI
160
+ scope object for every incoming request.
161
+ client_request_hook: Optional callback which is called with the internal span, and ASGI
162
+ scope and event which are sent as dictionaries for when the method receive is called.
163
+ client_response_hook: Optional callback which is called with the internal span, and ASGI
164
+ scope and event which are sent as dictionaries for when the method send is called.
165
+ tracer_provider: The optional tracer provider to use. If omitted
166
+ the current globally configured one is used.
167
+ meter_provider: The optional meter provider to use. If omitted
168
+ the current globally configured one is used.
169
+ excluded_urls: Optional comma delimited string of regexes to match URLs that should not be traced.
170
+ http_capture_headers_server_request: Optional list of HTTP headers to capture from the request.
171
+ http_capture_headers_server_response: Optional list of HTTP headers to capture from the response.
172
+ http_capture_headers_sanitize_fields: Optional list of HTTP headers to sanitize.
173
+ exclude_spans: Optionally exclude HTTP spans from the trace.
174
+ error_status_codes: Optional iterable of status codes to consider as errors. Defaults to range(400, 600).
175
+ error_messages: Optional dictionary mapping status codes or ranges to custom error messages.
176
+ """
177
+ if not hasattr(app, "_is_instrumented_by_opentelemetry"):
178
+ app._is_instrumented_by_opentelemetry = False
179
+
180
+ if not getattr(app, "_is_instrumented_by_opentelemetry", False):
181
+ if excluded_urls is None:
182
+ excluded_urls = _excluded_urls_from_env
183
+ else:
184
+ excluded_urls = parse_excluded_urls(excluded_urls)
185
+
186
+ tracer = get_tracer(__name__, "1.0.0", tracer_provider)
187
+ meter = get_meter(__name__, "1.0.0", meter_provider)
188
+
189
+ # Instead of using `app.add_middleware` we monkey patch `build_middleware_stack` to insert our middleware
190
+ # as the outermost middleware.
191
+ # This follows the OpenTelemetry FastAPI instrumentation pattern.
192
+ def build_middleware_stack(self: Starlette) -> ASGIApp:
193
+ inner_server_error_middleware: ASGIApp = self._original_build_middleware_stack()
194
+
195
+ # Add our status code monitoring middleware
196
+ status_code_middleware = StatusCodeMonitoringMiddleware(
197
+ inner_server_error_middleware,
198
+ error_status_codes=error_status_codes,
199
+ error_messages=error_messages,
200
+ )
201
+
202
+ # Add OpenTelemetry middleware
203
+ otel_middleware = OpenTelemetryMiddleware(
204
+ status_code_middleware,
205
+ excluded_urls=excluded_urls,
206
+ default_span_details=_get_default_span_details,
207
+ server_request_hook=server_request_hook,
208
+ client_request_hook=client_request_hook,
209
+ client_response_hook=client_response_hook,
210
+ tracer=tracer,
211
+ meter=meter,
212
+ http_capture_headers_server_request=http_capture_headers_server_request,
213
+ http_capture_headers_server_response=http_capture_headers_server_response,
214
+ http_capture_headers_sanitize_fields=http_capture_headers_sanitize_fields,
215
+ exclude_spans=exclude_spans,
216
+ )
217
+
218
+ # Wrap in an outer layer of ServerErrorMiddleware
219
+ if isinstance(inner_server_error_middleware, ServerErrorMiddleware):
220
+ outer_server_error_middleware = ServerErrorMiddleware(
221
+ app=otel_middleware,
222
+ )
223
+ else:
224
+ outer_server_error_middleware = ServerErrorMiddleware(app=otel_middleware)
225
+ return outer_server_error_middleware
226
+
227
+ app._original_build_middleware_stack = app.build_middleware_stack
228
+ app.build_middleware_stack = types.MethodType(
229
+ functools.wraps(app.build_middleware_stack)(build_middleware_stack),
230
+ app,
231
+ )
232
+
233
+ app._is_instrumented_by_opentelemetry = True
234
+ if app not in _InstrumentedFastAPI._instrumented_fastapi_apps:
235
+ _InstrumentedFastAPI._instrumented_fastapi_apps.add(app)
236
+ else:
237
+ _logger.warning("Attempting to instrument FastAPI app while already instrumented")
238
+
239
+ @staticmethod
240
+ def uninstrument_app(app: fastapi.FastAPI) -> None:
241
+ """Remove instrumentation from a FastAPI app."""
242
+ original_build_middleware_stack = getattr(app, "_original_build_middleware_stack", None)
243
+ if original_build_middleware_stack:
244
+ app.build_middleware_stack = original_build_middleware_stack
245
+ del app._original_build_middleware_stack
246
+ app.middleware_stack = app.build_middleware_stack()
247
+ app._is_instrumented_by_opentelemetry = False
248
+
249
+ def instrumentation_dependencies(self) -> Collection[str]:
250
+ return ["fastapi"]
251
+
252
+ def _instrument(self, **kwargs: Any) -> None:
253
+ """Instrument all FastAPI applications with status code monitoring."""
254
+ self._original_fastapi = fastapi.FastAPI
255
+ _InstrumentedFastAPI._tracer_provider = kwargs.get("tracer_provider")
256
+ _InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider")
257
+ _InstrumentedFastAPI._excluded_urls = kwargs.get("excluded_urls")
258
+ _InstrumentedFastAPI._server_request_hook = kwargs.get("server_request_hook")
259
+ _InstrumentedFastAPI._client_request_hook = kwargs.get("client_request_hook")
260
+ _InstrumentedFastAPI._client_response_hook = kwargs.get("client_response_hook")
261
+ _InstrumentedFastAPI._http_capture_headers_server_request = kwargs.get("http_capture_headers_server_request")
262
+ _InstrumentedFastAPI._http_capture_headers_server_response = kwargs.get("http_capture_headers_server_response")
263
+ _InstrumentedFastAPI._http_capture_headers_sanitize_fields = kwargs.get("http_capture_headers_sanitize_fields")
264
+ _InstrumentedFastAPI._exclude_spans = kwargs.get("exclude_spans")
265
+ _InstrumentedFastAPI._error_status_codes = kwargs.get("error_status_codes")
266
+ _InstrumentedFastAPI._error_messages = kwargs.get("error_messages")
267
+ fastapi.FastAPI = _InstrumentedFastAPI
268
+
269
+ def _uninstrument(self, **kwargs: Any) -> None:
270
+ """Remove instrumentation from all FastAPI applications."""
271
+ for instance in _InstrumentedFastAPI._instrumented_fastapi_apps:
272
+ self.uninstrument_app(instance)
273
+ _InstrumentedFastAPI._instrumented_fastapi_apps.clear()
274
+ fastapi.FastAPI = self._original_fastapi
275
+
276
+
277
+ class _InstrumentedFastAPI(fastapi.FastAPI): # type: ignore[misc]
278
+ """FastAPI class with automatic status code monitoring instrumentation."""
279
+
280
+ _tracer_provider: Optional[TracerProvider] = None
281
+ _meter_provider: Optional[MeterProvider] = None
282
+ _excluded_urls: Optional[str] = None
283
+ _server_request_hook: Optional[ServerRequestHook] = None
284
+ _client_request_hook: Optional[ClientRequestHook] = None
285
+ _client_response_hook: Optional[ClientResponseHook] = None
286
+ _http_capture_headers_server_request: Optional[list[str]] = None
287
+ _http_capture_headers_server_response: Optional[list[str]] = None
288
+ _http_capture_headers_sanitize_fields: Optional[list[str]] = None
289
+ _exclude_spans: Optional[list[str]] = None
290
+ _error_status_codes: Optional[Iterable[int]] = None
291
+ _error_messages: Optional[Dict[Union[int, range], str]] = None
292
+
293
+ _instrumented_fastapi_apps: set[fastapi.FastAPI] = set()
294
+
295
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
296
+ super().__init__(*args, **kwargs)
297
+ FastAPIInstrumentor.instrument_app(
298
+ self,
299
+ server_request_hook=self._server_request_hook,
300
+ client_request_hook=self._client_request_hook,
301
+ client_response_hook=self._client_response_hook,
302
+ tracer_provider=self._tracer_provider,
303
+ meter_provider=self._meter_provider,
304
+ excluded_urls=self._excluded_urls,
305
+ http_capture_headers_server_request=self._http_capture_headers_server_request,
306
+ http_capture_headers_server_response=self._http_capture_headers_server_response,
307
+ http_capture_headers_sanitize_fields=self._http_capture_headers_sanitize_fields,
308
+ exclude_spans=self._exclude_spans,
309
+ error_status_codes=self._error_status_codes,
310
+ error_messages=self._error_messages,
311
+ )
312
+ _InstrumentedFastAPI._instrumented_fastapi_apps.add(self)
313
+
314
+ def __del__(self) -> None:
315
+ if self in _InstrumentedFastAPI._instrumented_fastapi_apps:
316
+ _InstrumentedFastAPI._instrumented_fastapi_apps.remove(self)
317
+
318
+
319
+ def _get_route_details(scope: dict[str, Any]) -> Optional[str]:
320
+ """
321
+ Function to retrieve Starlette route from scope.
322
+
323
+ Args:
324
+ scope: A Starlette scope
325
+ Returns:
326
+ A string containing the route or None
327
+ """
328
+ app = scope["app"]
329
+ route = None
330
+
331
+ for starlette_route in app.routes:
332
+ match, _ = starlette_route.matches(scope)
333
+ if match == Match.FULL:
334
+ route = starlette_route.path
335
+ break
336
+ if match == Match.PARTIAL:
337
+ route = starlette_route.path
338
+ return route
339
+
340
+
341
+ def _get_default_span_details(scope: dict[str, Any]) -> tuple[str, dict[str, Any]]:
342
+ """
343
+ Callback to retrieve span name and attributes from scope.
344
+
345
+ Args:
346
+ scope: A Starlette scope
347
+ Returns:
348
+ A tuple of span name and attributes
349
+ """
350
+ route = _get_route_details(scope)
351
+ method = sanitize_method(scope.get("method", "").strip())
352
+ attributes: dict[str, Any] = {}
353
+ if method == "_OTHER":
354
+ method = "HTTP"
355
+ if route:
356
+ attributes[HTTP_ROUTE] = route
357
+ if method and route: # http
358
+ span_name = f"{method} {route}"
359
+ elif route: # websocket
360
+ span_name = route
361
+ else: # fallback
362
+ span_name = method
363
+ return span_name, attributes
@@ -0,0 +1 @@
1
+ __version__ = "0.116.1"
@@ -9,6 +9,7 @@ class CustomInstruments(Enum):
9
9
  COHEREAI = "cohere_ai"
10
10
  HTTPX = "httpx"
11
11
  MISTRALAI = "mistral_ai"
12
+ OPENAI = "openai"
12
13
  QDRANTDB = "qdrant_db"
13
14
  WEAVIATEDB = "weaviate_db"
14
15
  GOOGLE_GENERATIVEAI = "google_genai"
@@ -0,0 +1,153 @@
1
+ import json
2
+ import logging
3
+ import time
4
+ from typing import Any, Collection, Dict, Optional
5
+
6
+ from opentelemetry import context as context_api
7
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
8
+ from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
9
+ from opentelemetry.trace import SpanKind, Tracer, get_tracer
10
+ from opentelemetry.trace.status import Status, StatusCode
11
+ from wrapt import wrap_function_wrapper
12
+
13
+ from netra.instrumentation.openai.version import __version__
14
+ from netra.instrumentation.openai.wrappers import (
15
+ achat_wrapper,
16
+ acompletion_wrapper,
17
+ aembeddings_wrapper,
18
+ aresponses_wrapper,
19
+ chat_wrapper,
20
+ completion_wrapper,
21
+ embeddings_wrapper,
22
+ responses_wrapper,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _instruments = ("openai >= 1.0.0",)
28
+
29
+
30
+ class NetraOpenAIInstrumentor(BaseInstrumentor): # type: ignore[misc]
31
+ """
32
+ Custom OpenAI instrumentor for Netra SDK with enhanced support for:
33
+ - responses.create method
34
+ - Proper streaming/non-streaming span handling
35
+ - Integration with Netra tracing
36
+ """
37
+
38
+ def instrumentation_dependencies(self) -> Collection[str]:
39
+ return _instruments
40
+
41
+ def _instrument(self, **kwargs): # type: ignore[no-untyped-def]
42
+ """Instrument OpenAI client methods"""
43
+ tracer_provider = kwargs.get("tracer_provider")
44
+ tracer = get_tracer(__name__, __version__, tracer_provider)
45
+
46
+ # Chat completions
47
+ wrap_function_wrapper(
48
+ "openai.resources.chat.completions",
49
+ "Completions.create",
50
+ chat_wrapper(tracer),
51
+ )
52
+
53
+ wrap_function_wrapper(
54
+ "openai.resources.chat.completions",
55
+ "AsyncCompletions.create",
56
+ achat_wrapper(tracer),
57
+ )
58
+
59
+ # Traditional completions
60
+ wrap_function_wrapper(
61
+ "openai.resources.completions",
62
+ "Completions.create",
63
+ completion_wrapper(tracer),
64
+ )
65
+
66
+ wrap_function_wrapper(
67
+ "openai.resources.completions",
68
+ "AsyncCompletions.create",
69
+ acompletion_wrapper(tracer),
70
+ )
71
+
72
+ # Embeddings
73
+ wrap_function_wrapper(
74
+ "openai.resources.embeddings",
75
+ "Embeddings.create",
76
+ embeddings_wrapper(tracer),
77
+ )
78
+
79
+ wrap_function_wrapper(
80
+ "openai.resources.embeddings",
81
+ "AsyncEmbeddings.create",
82
+ aembeddings_wrapper(tracer),
83
+ )
84
+
85
+ # New responses.create method
86
+ try:
87
+ wrap_function_wrapper(
88
+ "openai.resources.responses",
89
+ "Responses.create",
90
+ responses_wrapper(tracer),
91
+ )
92
+
93
+ wrap_function_wrapper(
94
+ "openai.resources.responses",
95
+ "AsyncResponses.create",
96
+ aresponses_wrapper(tracer),
97
+ )
98
+ except (AttributeError, ModuleNotFoundError):
99
+ logger.debug("responses.create method not available in this OpenAI version")
100
+
101
+ # Beta APIs
102
+ try:
103
+ wrap_function_wrapper(
104
+ "openai.resources.beta.chat.completions",
105
+ "Completions.parse",
106
+ chat_wrapper(tracer),
107
+ )
108
+
109
+ wrap_function_wrapper(
110
+ "openai.resources.beta.chat.completions",
111
+ "AsyncCompletions.parse",
112
+ achat_wrapper(tracer),
113
+ )
114
+ except (AttributeError, ModuleNotFoundError):
115
+ logger.debug("Beta chat completions not available in this OpenAI version")
116
+
117
+ def _uninstrument(self, **kwargs): # type: ignore[no-untyped-def]
118
+ """Uninstrument OpenAI client methods"""
119
+ # Chat completions
120
+ unwrap("openai.resources.chat.completions", "Completions.create")
121
+ unwrap("openai.resources.chat.completions", "AsyncCompletions.create")
122
+
123
+ # Traditional completions
124
+ unwrap("openai.resources.completions", "Completions.create")
125
+ unwrap("openai.resources.completions", "AsyncCompletions.create")
126
+
127
+ # Embeddings
128
+ unwrap("openai.resources.embeddings", "Embeddings.create")
129
+ unwrap("openai.resources.embeddings", "AsyncEmbeddings.create")
130
+
131
+ # New responses.create method
132
+ try:
133
+ unwrap("openai.resources.responses", "Responses.create")
134
+ unwrap("openai.resources.responses", "AsyncResponses.create")
135
+ except (AttributeError, ModuleNotFoundError):
136
+ pass
137
+
138
+ # Beta APIs
139
+ try:
140
+ unwrap("openai.resources.beta.chat.completions", "Completions.parse")
141
+ unwrap("openai.resources.beta.chat.completions", "AsyncCompletions.parse")
142
+ except (AttributeError, ModuleNotFoundError):
143
+ pass
144
+
145
+
146
+ def is_streaming_response(response: Any) -> bool:
147
+ """Check if response is a streaming response"""
148
+ return hasattr(response, "__iter__") and not isinstance(response, (str, bytes, dict))
149
+
150
+
151
+ def should_suppress_instrumentation() -> bool:
152
+ """Check if instrumentation should be suppressed"""
153
+ return context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) is True
@@ -0,0 +1 @@
1
+ __version__ = "1.95.1"