@@ -176,10 +176,12 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
176
176
177
177
from __future__ import annotations
178
178
179
- from typing import TYPE_CHECKING , Any , Collection , cast
179
+ from functools import partial
180
+ from typing import TYPE_CHECKING , Any , Collection
180
181
181
182
from starlette import applications
182
183
from starlette .routing import Match
184
+ from wrapt import wrap_function_wrapper
183
185
184
186
from opentelemetry .instrumentation .asgi import OpenTelemetryMiddleware
185
187
from opentelemetry .instrumentation .asgi .types import (
@@ -190,6 +192,7 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
190
192
from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
191
193
from opentelemetry .instrumentation .starlette .package import _instruments
192
194
from opentelemetry .instrumentation .starlette .version import __version__
195
+ from opentelemetry .instrumentation .utils import unwrap
193
196
from opentelemetry .metrics import MeterProvider , get_meter
194
197
from opentelemetry .semconv .trace import SpanAttributes
195
198
from opentelemetry .trace import TracerProvider , get_tracer
@@ -215,7 +218,7 @@ class StarletteInstrumentor(BaseInstrumentor):
215
218
See `BaseInstrumentor`.
216
219
"""
217
220
218
- _original_starlette = None
221
+ _instrumented_starlette_apps : set [ applications . Starlette ] = set ()
219
222
220
223
@staticmethod
221
224
def instrument_app (
@@ -240,22 +243,14 @@ def instrument_app(
240
243
schema_url = "https://opentelemetry.io/schemas/1.11.0" ,
241
244
)
242
245
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 ,
253
253
)
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 )
259
254
260
255
@staticmethod
261
256
def uninstrument_app (app : applications .Starlette ):
@@ -271,68 +266,90 @@ def instrumentation_dependencies(self) -> Collection[str]:
271
266
return _instruments
272
267
273
268
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" )
304
274
305
- def __init__ (self , * args : Any , ** kwargs : Any ):
306
- super ().__init__ (* args , ** kwargs )
307
275
tracer = get_tracer (
308
276
__name__ ,
309
277
__version__ ,
310
- _InstrumentedStarlette . _tracer_provider ,
278
+ tracer_provider ,
311
279
schema_url = "https://opentelemetry.io/schemas/1.11.0" ,
312
280
)
313
281
meter = get_meter (
314
282
__name__ ,
315
283
__version__ ,
316
- _InstrumentedStarlette . _meter_provider ,
284
+ meter_provider ,
317
285
schema_url = "https://opentelemetry.io/schemas/1.11.0" ,
318
286
)
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 (
320
341
OpenTelemetryMiddleware ,
321
342
excluded_urls = _excluded_urls ,
322
343
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 ,
327
347
tracer = tracer ,
328
348
meter = meter ,
329
349
)
330
- self ._is_instrumented_by_opentelemetry = True
350
+ app ._is_instrumented_by_opentelemetry = True
331
351
# 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 )
336
353
337
354
338
355
def _get_route_details (scope : dict [str , Any ]) -> str | None :
@@ -349,7 +366,7 @@ def _get_route_details(scope: dict[str, Any]) -> str | None:
349
366
Returns:
350
367
The path to the route if found, otherwise None.
351
368
"""
352
- app = cast ( applications . Starlette , scope ["app" ])
369
+ app = scope ["app" ]
353
370
route : str | None = None
354
371
355
372
for starlette_route in app .routes :
0 commit comments