Skip to content

Commit 9f9f012

Browse files
committed
opentelemetry-instrumentation: add unwrapping from dotted paths strings
Make it possible to express object to unwrap as dotted module paths strings. This helps in avoiding side effects or race conditions with other instrumentations if we do importing too early. While at it add tests also for current functionality.
1 parent e4ece57 commit 9f9f012

File tree

2 files changed

+99
-1
lines changed

2 files changed

+99
-1
lines changed

opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import urllib.parse
1616
from contextlib import contextmanager
17+
from importlib import import_module
1718
from re import escape, sub
1819
from typing import Dict, Iterable, Sequence
1920

@@ -83,10 +84,27 @@ def http_status_to_status_code(
8384
def unwrap(obj, attr: str):
8485
"""Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it
8586
87+
The object containing the function to unwrap may be passed as dotted module path string.
88+
8689
Args:
87-
obj: Object that holds a reference to the wrapped function
90+
obj: Object that holds a reference to the wrapped function or dotted import path as string
8891
attr (str): Name of the wrapped function
8992
"""
93+
if isinstance(obj, str):
94+
try:
95+
module_path, class_name = obj.rsplit(".", 1)
96+
except ValueError as exc:
97+
raise ImportError(
98+
f"Cannot parse '{obj}' as dotted import path"
99+
) from exc
100+
module = import_module(module_path)
101+
try:
102+
obj = getattr(module, class_name)
103+
except AttributeError as exc:
104+
raise ImportError(
105+
f"Cannot import '{class_name}' from '{module}'"
106+
) from exc
107+
90108
func = getattr(obj, attr, None)
91109
if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"):
92110
setattr(obj, attr, func.__wrapped__)

opentelemetry-instrumentation/tests/test_utils.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import unittest
1616
from http import HTTPStatus
1717

18+
from wrapt import ObjectProxy, wrap_function_wrapper
19+
1820
from opentelemetry.context import (
1921
_SUPPRESS_HTTP_INSTRUMENTATION_KEY,
2022
_SUPPRESS_INSTRUMENTATION_KEY,
@@ -29,10 +31,19 @@
2931
is_instrumentation_enabled,
3032
suppress_http_instrumentation,
3133
suppress_instrumentation,
34+
unwrap,
3235
)
3336
from opentelemetry.trace import StatusCode
3437

3538

39+
class WrappedClass:
40+
def method(self):
41+
pass
42+
43+
def wrapper_method(self):
44+
pass
45+
46+
3647
class TestUtils(unittest.TestCase):
3748
# See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status
3849
def test_http_status_to_status_code(self):
@@ -240,3 +251,72 @@ def test_suppress_http_instrumentation_key(self):
240251
self.assertTrue(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY))
241252

242253
self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY))
254+
255+
@staticmethod
256+
def _wrap_method():
257+
return wrap_function_wrapper(
258+
WrappedClass, "method", WrappedClass.wrapper_method
259+
)
260+
261+
def test_unwrap_can_unwrap_object_attribute(self):
262+
self._wrap_method()
263+
instance = WrappedClass()
264+
self.assertTrue(isinstance(instance.method, ObjectProxy))
265+
266+
unwrap(WrappedClass, "method")
267+
self.assertFalse(isinstance(instance.method, ObjectProxy))
268+
269+
def test_unwrap_can_unwrap_object_attribute_as_string(self):
270+
self._wrap_method()
271+
instance = WrappedClass()
272+
self.assertTrue(isinstance(instance.method, ObjectProxy))
273+
274+
unwrap("tests.test_utils.WrappedClass", "method")
275+
self.assertFalse(isinstance(instance.method, ObjectProxy))
276+
277+
def test_unwrap_raises_import_error_if_path_not_well_formed(self):
278+
self._wrap_method()
279+
instance = WrappedClass()
280+
self.assertTrue(isinstance(instance.method, ObjectProxy))
281+
282+
with self.assertRaisesRegex(
283+
ImportError, "Cannot parse '' as dotted import path"
284+
):
285+
unwrap("", "method")
286+
287+
unwrap(WrappedClass, "method")
288+
self.assertFalse(isinstance(instance.method, ObjectProxy))
289+
290+
def test_unwrap_raises_import_error_if_cannot_find_module(self):
291+
self._wrap_method()
292+
instance = WrappedClass()
293+
self.assertTrue(isinstance(instance.method, ObjectProxy))
294+
295+
with self.assertRaisesRegex(ImportError, "No module named 'does'"):
296+
unwrap("does.not.exist.WrappedClass", "method")
297+
298+
unwrap(WrappedClass, "method")
299+
self.assertFalse(isinstance(instance.method, ObjectProxy))
300+
301+
def test_unwrap_raises_import_error_if_cannot_find_object(self):
302+
self._wrap_method()
303+
instance = WrappedClass()
304+
self.assertTrue(isinstance(instance.method, ObjectProxy))
305+
306+
with self.assertRaisesRegex(
307+
ImportError, "Cannot import 'NotWrappedClass' from"
308+
):
309+
unwrap("tests.test_utils.NotWrappedClass", "method")
310+
311+
unwrap(WrappedClass, "method")
312+
self.assertFalse(isinstance(instance.method, ObjectProxy))
313+
314+
def test_does_nothing_if_cannot_find_attribute(self):
315+
instance = WrappedClass()
316+
unwrap(instance, "method_not_found")
317+
318+
def test_unwrap_does_nothing_if_attribute_is_not_from_wrapt(self):
319+
instance = WrappedClass()
320+
self.assertFalse(isinstance(instance.method, ObjectProxy))
321+
unwrap(WrappedClass, "method")
322+
self.assertFalse(isinstance(instance.method, ObjectProxy))

0 commit comments

Comments
 (0)