Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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))
- `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))
- `opentelemetry-instrumentation` Add support for string based dotted module paths in unwrap
([#2919](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2919))

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import urllib.parse
from contextlib import contextmanager
from importlib import import_module
from re import escape, sub
from typing import Dict, Iterable, Sequence

Expand Down Expand Up @@ -83,10 +84,27 @@ def http_status_to_status_code(
def unwrap(obj, attr: str):
xrmx marked this conversation as resolved.
Show resolved Hide resolved
"""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:
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
"""
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)
if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"):
setattr(obj, attr, func.__wrapped__)
Expand Down
83 changes: 83 additions & 0 deletions opentelemetry-instrumentation/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import unittest
from http import HTTPStatus

from wrapt import ObjectProxy, wrap_function_wrapper

from opentelemetry.context import (
_SUPPRESS_HTTP_INSTRUMENTATION_KEY,
_SUPPRESS_INSTRUMENTATION_KEY,
Expand All @@ -29,10 +31,19 @@
is_instrumentation_enabled,
suppress_http_instrumentation,
suppress_instrumentation,
unwrap,
)
from opentelemetry.trace import StatusCode


class WrappedClass:
def method(self):
pass

def wrapper_method(self):
pass


class TestUtils(unittest.TestCase):
# 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):
Expand Down Expand Up @@ -240,3 +251,75 @@ def test_suppress_http_instrumentation_key(self):
self.assertTrue(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))
Loading