@@ -176,10 +176,12 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
176176
177177from __future__ import annotations
178178
179- from typing import TYPE_CHECKING , Any , Collection , cast
179+ from functools import partial
180+ from typing import TYPE_CHECKING , Any , Collection
180181
181182from starlette import applications
182183from starlette .routing import Match
184+ from wrapt import wrap_function_wrapper
183185
184186from opentelemetry .instrumentation .asgi import OpenTelemetryMiddleware
185187from opentelemetry .instrumentation .asgi .types import (
@@ -190,6 +192,7 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
190192from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
191193from opentelemetry .instrumentation .starlette .package import _instruments
192194from opentelemetry .instrumentation .starlette .version import __version__
195+ from opentelemetry .instrumentation .utils import unwrap
193196from opentelemetry .metrics import MeterProvider , get_meter
194197from opentelemetry .semconv .trace import SpanAttributes
195198from opentelemetry .trace import TracerProvider , get_tracer
@@ -215,7 +218,7 @@ class StarletteInstrumentor(BaseInstrumentor):
215218 See `BaseInstrumentor`.
216219 """
217220
218- _original_starlette = None
221+ _instrumented_starlette_apps : set [ applications . Starlette ] = set ()
219222
220223 @staticmethod
221224 def instrument_app (
@@ -240,22 +243,14 @@ def instrument_app(
240243 schema_url = "https://opentelemetry.io/schemas/1.11.0" ,
241244 )
242245 if not getattr (app , "is_instrumented_by_opentelemetry" , False ):
243- app .add_middleware (
244- OpenTelemetryMiddleware ,
245- excluded_urls = _excluded_urls ,
246- default_span_details = _get_default_span_details ,
247- server_request_hook = server_request_hook ,
248- client_request_hook = client_request_hook ,
249- client_response_hook = client_response_hook ,
250- # Pass in tracer/meter to get __name__and __version__ of starlette instrumentation
251- tracer = tracer ,
252- meter = meter ,
246+ StarletteInstrumentor ._add_instrumentation_middleware (
247+ app ,
248+ tracer ,
249+ meter ,
250+ server_request_hook ,
251+ client_request_hook ,
252+ client_response_hook ,
253253 )
254- app .is_instrumented_by_opentelemetry = True
255-
256- # adding apps to set for uninstrumenting
257- if app not in _InstrumentedStarlette ._instrumented_starlette_apps :
258- _InstrumentedStarlette ._instrumented_starlette_apps .add (app )
259254
260255 @staticmethod
261256 def uninstrument_app (app : applications .Starlette ):
@@ -271,68 +266,90 @@ def instrumentation_dependencies(self) -> Collection[str]:
271266 return _instruments
272267
273268 def _instrument (self , ** kwargs : Unpack [InstrumentKwargs ]):
274- self ._original_starlette = applications .Starlette
275- _InstrumentedStarlette ._tracer_provider = kwargs .get ("tracer_provider" )
276- _InstrumentedStarlette ._server_request_hook = kwargs .get (
277- "server_request_hook"
278- )
279- _InstrumentedStarlette ._client_request_hook = kwargs .get (
280- "client_request_hook"
281- )
282- _InstrumentedStarlette ._client_response_hook = kwargs .get (
283- "client_response_hook"
284- )
285- _InstrumentedStarlette ._meter_provider = kwargs .get ("meter_provider" )
286-
287- applications .Starlette = _InstrumentedStarlette
288-
289- def _uninstrument (self , ** kwargs : Any ):
290- """uninstrumenting all created apps by user"""
291- for instance in _InstrumentedStarlette ._instrumented_starlette_apps :
292- self .uninstrument_app (instance )
293- _InstrumentedStarlette ._instrumented_starlette_apps .clear ()
294- applications .Starlette = self ._original_starlette
295-
296-
297- class _InstrumentedStarlette (applications .Starlette ):
298- _tracer_provider : TracerProvider | None = None
299- _meter_provider : MeterProvider | None = None
300- _server_request_hook : ServerRequestHook = None
301- _client_request_hook : ClientRequestHook = None
302- _client_response_hook : ClientResponseHook = None
303- _instrumented_starlette_apps : set [applications .Starlette ] = set ()
269+ tracer_provider = kwargs .get ("tracer_provider" )
270+ server_request_hook = kwargs .get ("server_request_hook" )
271+ client_request_hook = kwargs .get ("client_request_hook" )
272+ client_response_hook = kwargs .get ("client_response_hook" )
273+ meter_provider = kwargs .get ("meter_provider" )
304274
305- def __init__ (self , * args : Any , ** kwargs : Any ):
306- super ().__init__ (* args , ** kwargs )
307275 tracer = get_tracer (
308276 __name__ ,
309277 __version__ ,
310- _InstrumentedStarlette . _tracer_provider ,
278+ tracer_provider ,
311279 schema_url = "https://opentelemetry.io/schemas/1.11.0" ,
312280 )
313281 meter = get_meter (
314282 __name__ ,
315283 __version__ ,
316- _InstrumentedStarlette . _meter_provider ,
284+ meter_provider ,
317285 schema_url = "https://opentelemetry.io/schemas/1.11.0" ,
318286 )
319- self .add_middleware (
287+
288+ def instrumented_init (
289+ wrapped ,
290+ instance ,
291+ args ,
292+ kwargs ,
293+ tracer ,
294+ meter ,
295+ server_request_hook ,
296+ client_request_hook ,
297+ client_response_hook ,
298+ ):
299+ result = wrapped (* args , ** kwargs )
300+ StarletteInstrumentor ._add_instrumentation_middleware (
301+ instance ,
302+ tracer ,
303+ meter ,
304+ server_request_hook ,
305+ client_request_hook ,
306+ client_response_hook ,
307+ )
308+
309+ return result
310+
311+ # Wrap Starlette's __init__ method to add instrumentation
312+ wrap_function_wrapper (
313+ applications .Starlette ,
314+ "__init__" ,
315+ partial (
316+ instrumented_init ,
317+ tracer = tracer ,
318+ meter = meter ,
319+ server_request_hook = server_request_hook ,
320+ client_request_hook = client_request_hook ,
321+ client_response_hook = client_response_hook ,
322+ ),
323+ )
324+
325+ def _uninstrument (self , ** kwargs : Any ):
326+ for app in list (StarletteInstrumentor ._instrumented_starlette_apps ):
327+ self .uninstrument_app (app )
328+
329+ unwrap (applications .Starlette , "__init__" )
330+
331+ @staticmethod
332+ def _add_instrumentation_middleware (
333+ app : applications .Starlette ,
334+ tracer ,
335+ meter ,
336+ server_request_hook ,
337+ client_request_hook ,
338+ client_response_hook ,
339+ ):
340+ app .add_middleware (
320341 OpenTelemetryMiddleware ,
321342 excluded_urls = _excluded_urls ,
322343 default_span_details = _get_default_span_details ,
323- server_request_hook = _InstrumentedStarlette ._server_request_hook ,
324- client_request_hook = _InstrumentedStarlette ._client_request_hook ,
325- client_response_hook = _InstrumentedStarlette ._client_response_hook ,
326- # Pass in tracer/meter to get __name__and __version__ of starlette instrumentation
344+ server_request_hook = server_request_hook ,
345+ client_request_hook = client_request_hook ,
346+ client_response_hook = client_response_hook ,
327347 tracer = tracer ,
328348 meter = meter ,
329349 )
330- self ._is_instrumented_by_opentelemetry = True
350+ app ._is_instrumented_by_opentelemetry = True
331351 # adding apps to set for uninstrumenting
332- _InstrumentedStarlette ._instrumented_starlette_apps .add (self )
333-
334- def __del__ (self ):
335- _InstrumentedStarlette ._instrumented_starlette_apps .remove (self )
352+ StarletteInstrumentor ._instrumented_starlette_apps .add (app )
336353
337354
338355def _get_route_details (scope : dict [str , Any ]) -> str | None :
@@ -349,7 +366,7 @@ def _get_route_details(scope: dict[str, Any]) -> str | None:
349366 Returns:
350367 The path to the route if found, otherwise None.
351368 """
352- app = cast ( applications . Starlette , scope ["app" ])
369+ app = scope ["app" ]
353370 route : str | None = None
354371
355372 for starlette_route in app .routes :
0 commit comments