Skip to content

Commit 8800091

Browse files
committed
starlette: use wrapt for patching instead of class replacement
1 parent 71bfc95 commit 8800091

File tree

3 files changed

+83
-65
lines changed

3 files changed

+83
-65
lines changed

instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies = [
3131
"opentelemetry-instrumentation-asgi == 0.55b0.dev",
3232
"opentelemetry-semantic-conventions == 0.55b0.dev",
3333
"opentelemetry-util-http == 0.55b0.dev",
34+
"wrapt >= 1.0.0, < 2.0.0"
3435
]
3536

3637
[project.optional-dependencies]

instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py

Lines changed: 79 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,12 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
176176

177177
from __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

181182
from starlette import applications
182183
from starlette.routing import Match
184+
from wrapt import wrap_function_wrapper
183185

184186
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
185187
from opentelemetry.instrumentation.asgi.types import (
@@ -190,6 +192,7 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
190192
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
191193
from opentelemetry.instrumentation.starlette.package import _instruments
192194
from opentelemetry.instrumentation.starlette.version import __version__
195+
from opentelemetry.instrumentation.utils import unwrap
193196
from opentelemetry.metrics import MeterProvider, get_meter
194197
from opentelemetry.semconv.trace import SpanAttributes
195198
from 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

338355
def _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:

instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -572,15 +572,15 @@ def test_instrumentation(self):
572572
removing as expected.
573573
"""
574574
instrumentor = otel_starlette.StarletteInstrumentor()
575-
original = applications.Starlette
575+
original = applications.Starlette.__init__
576576
instrumentor.instrument()
577577
try:
578-
instrumented = applications.Starlette
578+
instrumented = applications.Starlette.__init__
579579
self.assertIsNot(original, instrumented)
580580
finally:
581581
instrumentor.uninstrument()
582582

583-
should_be_original = applications.Starlette
583+
should_be_original = applications.Starlette.__init__
584584
self.assertIs(original, should_be_original)
585585

586586

0 commit comments

Comments
 (0)