opentelemetry-instrumentation: add unwrapping from dotted paths strings (#2919)

This commit is contained in:
Riccardo Magliocchetti
2024-10-24 22:19:29 +02:00
committed by GitHub
parent 5145a07fd1
commit 39bd7fa79a
3 changed files with 106 additions and 3 deletions

View File

@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2082](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2082)) ([#2082](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2082))
- `opentelemetry-instrumentation-redis` Add additional attributes for methods create_index and search, rename those spans - `opentelemetry-instrumentation-redis` Add additional attributes for methods create_index and search, rename those spans
([#2635](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2635)) ([#2635](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2635))
- `opentelemetry-instrumentation` Add support for string based dotted module paths in unwrap
([#2919](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2919))
### Fixed ### Fixed

View File

@ -14,8 +14,9 @@
import urllib.parse import urllib.parse
from contextlib import contextmanager from contextlib import contextmanager
from importlib import import_module
from re import escape, sub from re import escape, sub
from typing import Dict, Iterable, Sequence from typing import Dict, Iterable, Sequence, Union
from wrapt import ObjectProxy from wrapt import ObjectProxy
@ -80,13 +81,30 @@ def http_status_to_status_code(
return StatusCode.ERROR return StatusCode.ERROR
def unwrap(obj, attr: str): def unwrap(obj: Union[object, str], attr: str):
"""Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it
The object containing the function to unwrap may be passed as dotted module path string.
Args: Args:
obj: Object that holds a reference to the wrapped function obj: Object that holds a reference to the wrapped function or dotted import path as string
attr (str): Name of the wrapped function attr (str): Name of the wrapped function
""" """
if isinstance(obj, str):
try:
module_path, class_name = obj.rsplit(".", 1)
except ValueError as exc:
raise ImportError(
f"Cannot parse '{obj}' as dotted import path"
) from exc
module = import_module(module_path)
try:
obj = getattr(module, class_name)
except AttributeError as exc:
raise ImportError(
f"Cannot import '{class_name}' from '{module}'"
) from exc
func = getattr(obj, attr, None) func = getattr(obj, attr, None)
if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"):
setattr(obj, attr, func.__wrapped__) setattr(obj, attr, func.__wrapped__)

View File

@ -15,6 +15,8 @@
import unittest import unittest
from http import HTTPStatus from http import HTTPStatus
from wrapt import ObjectProxy, wrap_function_wrapper
from opentelemetry.context import ( from opentelemetry.context import (
_SUPPRESS_HTTP_INSTRUMENTATION_KEY, _SUPPRESS_HTTP_INSTRUMENTATION_KEY,
_SUPPRESS_INSTRUMENTATION_KEY, _SUPPRESS_INSTRUMENTATION_KEY,
@ -29,10 +31,19 @@ from opentelemetry.instrumentation.utils import (
is_instrumentation_enabled, is_instrumentation_enabled,
suppress_http_instrumentation, suppress_http_instrumentation,
suppress_instrumentation, suppress_instrumentation,
unwrap,
) )
from opentelemetry.trace import StatusCode from opentelemetry.trace import StatusCode
class WrappedClass:
def method(self):
pass
def wrapper_method(self):
pass
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
# See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status # See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status
def test_http_status_to_status_code(self): def test_http_status_to_status_code(self):
@ -240,3 +251,75 @@ class TestUtils(unittest.TestCase):
self.assertTrue(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) self.assertTrue(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY))
self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY))
class UnwrapTestCase(unittest.TestCase):
@staticmethod
def _wrap_method():
return wrap_function_wrapper(
WrappedClass, "method", WrappedClass.wrapper_method
)
def test_can_unwrap_object_attribute(self):
self._wrap_method()
instance = WrappedClass()
self.assertTrue(isinstance(instance.method, ObjectProxy))
unwrap(WrappedClass, "method")
self.assertFalse(isinstance(instance.method, ObjectProxy))
def test_can_unwrap_object_attribute_as_string(self):
self._wrap_method()
instance = WrappedClass()
self.assertTrue(isinstance(instance.method, ObjectProxy))
unwrap("tests.test_utils.WrappedClass", "method")
self.assertFalse(isinstance(instance.method, ObjectProxy))
def test_raises_import_error_if_path_not_well_formed(self):
self._wrap_method()
instance = WrappedClass()
self.assertTrue(isinstance(instance.method, ObjectProxy))
with self.assertRaisesRegex(
ImportError, "Cannot parse '' as dotted import path"
):
unwrap("", "method")
unwrap(WrappedClass, "method")
self.assertFalse(isinstance(instance.method, ObjectProxy))
def test_raises_import_error_if_cannot_find_module(self):
self._wrap_method()
instance = WrappedClass()
self.assertTrue(isinstance(instance.method, ObjectProxy))
with self.assertRaisesRegex(ImportError, "No module named 'does'"):
unwrap("does.not.exist.WrappedClass", "method")
unwrap(WrappedClass, "method")
self.assertFalse(isinstance(instance.method, ObjectProxy))
def test_raises_import_error_if_cannot_find_object(self):
self._wrap_method()
instance = WrappedClass()
self.assertTrue(isinstance(instance.method, ObjectProxy))
with self.assertRaisesRegex(
ImportError, "Cannot import 'NotWrappedClass' from"
):
unwrap("tests.test_utils.NotWrappedClass", "method")
unwrap(WrappedClass, "method")
self.assertFalse(isinstance(instance.method, ObjectProxy))
# pylint: disable=no-self-use
def test_does_nothing_if_cannot_find_attribute(self):
instance = WrappedClass()
unwrap(instance, "method_not_found")
def test_does_nothing_if_attribute_is_not_from_wrapt(self):
instance = WrappedClass()
self.assertFalse(isinstance(instance.method, ObjectProxy))
unwrap(WrappedClass, "method")
self.assertFalse(isinstance(instance.method, ObjectProxy))