netra-sdk 0.1.7__tar.gz → 0.1.10__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.

Files changed (49) hide show
  1. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/PKG-INFO +53 -1
  2. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/README.md +52 -0
  3. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/__init__.py +2 -2
  4. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/config.py +2 -2
  5. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/__init__.py +26 -1
  6. netra_sdk-0.1.10/netra/instrumentation/fastapi/__init__.py +363 -0
  7. netra_sdk-0.1.10/netra/instrumentation/fastapi/version.py +1 -0
  8. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/instruments.py +1 -0
  9. netra_sdk-0.1.10/netra/instrumentation/openai/__init__.py +153 -0
  10. netra_sdk-0.1.10/netra/instrumentation/openai/version.py +1 -0
  11. netra_sdk-0.1.10/netra/instrumentation/openai/wrappers.py +554 -0
  12. netra_sdk-0.1.10/netra/processors/__init__.py +3 -0
  13. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/span_wrapper.py +15 -0
  14. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/tracer.py +1 -2
  15. netra_sdk-0.1.10/netra/version.py +1 -0
  16. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/pyproject.toml +1 -1
  17. netra_sdk-0.1.7/netra/processors/__init__.py +0 -4
  18. netra_sdk-0.1.7/netra/processors/error_detection_processor.py +0 -84
  19. netra_sdk-0.1.7/netra/version.py +0 -1
  20. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/LICENCE +0 -0
  21. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/anonymizer/__init__.py +0 -0
  22. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/anonymizer/anonymizer.py +0 -0
  23. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/anonymizer/base.py +0 -0
  24. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/anonymizer/fp_anonymizer.py +0 -0
  25. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/decorators.py +0 -0
  26. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/exceptions/__init__.py +0 -0
  27. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/exceptions/injection.py +0 -0
  28. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/exceptions/pii.py +0 -0
  29. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/input_scanner.py +0 -0
  30. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/aiohttp/__init__.py +0 -0
  31. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/aiohttp/version.py +0 -0
  32. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/cohere/__init__.py +0 -0
  33. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/cohere/version.py +0 -0
  34. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/google_genai/__init__.py +0 -0
  35. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/google_genai/config.py +0 -0
  36. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/google_genai/utils.py +0 -0
  37. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/google_genai/version.py +0 -0
  38. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/httpx/__init__.py +0 -0
  39. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/httpx/version.py +0 -0
  40. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/mistralai/__init__.py +0 -0
  41. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/mistralai/config.py +0 -0
  42. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/mistralai/utils.py +0 -0
  43. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/mistralai/version.py +0 -0
  44. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/weaviate/__init__.py +0 -0
  45. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/instrumentation/weaviate/version.py +0 -0
  46. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/pii.py +0 -0
  47. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/processors/session_span_processor.py +0 -0
  48. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/scanner.py +0 -0
  49. {netra_sdk-0.1.7 → netra_sdk-0.1.10}/netra/session_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: netra-sdk
3
- Version: 0.1.7
3
+ Version: 0.1.10
4
4
  Summary: A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments.
5
5
  License: Apache-2.0
6
6
  Keywords: netra,tracing,observability,sdk,ai,llm,vector,database
@@ -440,6 +440,58 @@ with Netra.start_span("image_generation") as span:
440
440
 
441
441
  # Get the current active open telemetry span
442
442
  current_span = span.get_current_span()
443
+
444
+ # Track database operations and other actions
445
+ action = ActionModel(
446
+ action="DB",
447
+ action_type="INSERT",
448
+ affected_records=[
449
+ {"record_id": "user_123", "record_type": "user"},
450
+ {"record_id": "profile_456", "record_type": "profile"}
451
+ ],
452
+ metadata={
453
+ "table": "users",
454
+ "operation_id": "tx_789",
455
+ "duration_ms": "45"
456
+ },
457
+ success=True
458
+ )
459
+ span.set_action([action])
460
+
461
+ # Record API calls
462
+ api_action = ActionModel(
463
+ action="API",
464
+ action_type="CALL",
465
+ metadata={
466
+ "endpoint": "/api/v1/process",
467
+ "method": "POST",
468
+ "status_code": 200,
469
+ "duration_ms": "120"
470
+ },
471
+ success=True
472
+ )
473
+ span.set_action([api_action])
474
+ ```
475
+
476
+ ### Action Tracking Schema
477
+
478
+ Action tracking follows this schema:
479
+
480
+ ```python
481
+ [
482
+ {
483
+ "action": str, # Type of action (e.g., "DB", "API", "CACHE")
484
+ "action_type": str, # Action subtype (e.g., "INSERT", "SELECT", "CALL")
485
+ "affected_records": [ # Optional: List of records affected
486
+ {
487
+ "record_id": str, # ID of the affected record
488
+ "record_type": str # Type of the record
489
+ }
490
+ ],
491
+ "metadata": Dict[str, str], # Additional metadata as key-value pairs
492
+ "success": bool # Whether the action succeeded
493
+ }
494
+ ]
443
495
  ```
444
496
 
445
497
  ## 🔧 Advanced Configuration
@@ -364,6 +364,58 @@ with Netra.start_span("image_generation") as span:
364
364
 
365
365
  # Get the current active open telemetry span
366
366
  current_span = span.get_current_span()
367
+
368
+ # Track database operations and other actions
369
+ action = ActionModel(
370
+ action="DB",
371
+ action_type="INSERT",
372
+ affected_records=[
373
+ {"record_id": "user_123", "record_type": "user"},
374
+ {"record_id": "profile_456", "record_type": "profile"}
375
+ ],
376
+ metadata={
377
+ "table": "users",
378
+ "operation_id": "tx_789",
379
+ "duration_ms": "45"
380
+ },
381
+ success=True
382
+ )
383
+ span.set_action([action])
384
+
385
+ # Record API calls
386
+ api_action = ActionModel(
387
+ action="API",
388
+ action_type="CALL",
389
+ metadata={
390
+ "endpoint": "/api/v1/process",
391
+ "method": "POST",
392
+ "status_code": 200,
393
+ "duration_ms": "120"
394
+ },
395
+ success=True
396
+ )
397
+ span.set_action([api_action])
398
+ ```
399
+
400
+ ### Action Tracking Schema
401
+
402
+ Action tracking follows this schema:
403
+
404
+ ```python
405
+ [
406
+ {
407
+ "action": str, # Type of action (e.g., "DB", "API", "CACHE")
408
+ "action_type": str, # Action subtype (e.g., "INSERT", "SELECT", "CALL")
409
+ "affected_records": [ # Optional: List of records affected
410
+ {
411
+ "record_id": str, # ID of the affected record
412
+ "record_type": str # Type of the record
413
+ }
414
+ ],
415
+ "metadata": Dict[str, str], # Additional metadata as key-value pairs
416
+ "success": bool # Whether the action succeeded
417
+ }
418
+ ]
367
419
  ```
368
420
 
369
421
  ## 🔧 Advanced Configuration
@@ -9,7 +9,7 @@ from .config import Config
9
9
  # Instrumentor functions
10
10
  from .instrumentation import init_instrumentations
11
11
  from .session_manager import SessionManager
12
- from .span_wrapper import SpanWrapper, UsageModel
12
+ from .span_wrapper import ActionModel, SpanWrapper, UsageModel
13
13
  from .tracer import Tracer
14
14
 
15
15
  logger = logging.getLogger(__name__)
@@ -145,4 +145,4 @@ class Netra:
145
145
  return SpanWrapper(name, attributes, module_name)
146
146
 
147
147
 
148
- __all__ = ["Netra", "UsageModel"]
148
+ __all__ = ["Netra", "UsageModel", "ActionModel"]
@@ -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:
@@ -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"