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

Make auto instrumentation use the same dependency resolver as manual instrumentation does #3202

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3307](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3307))
- Loosen `opentelemetry-instrumentation-starlette[instruments]` specifier
([#3304](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3304))

- `opentelemetry-instrumentation` Make auto instrumentation use the same dependency resolver as manual instrumentation does
([#3202](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3202))

### Fixed

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

import unittest
from timeit import default_timer
from unittest.mock import Mock, patch
from unittest.mock import Mock, call, patch

import fastapi
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
Expand All @@ -37,6 +37,10 @@
from opentelemetry.instrumentation.auto_instrumentation._load import (
_load_instrumentors,
)
from opentelemetry.instrumentation.dependencies import (
DependencyConflict,
DependencyConflictError,
)
from opentelemetry.sdk.metrics.export import (
HistogramDataPoint,
NumberDataPoint,
Expand All @@ -54,10 +58,7 @@
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.test.globals_test import reset_trace_globals
from opentelemetry.test.test_base import TestBase
from opentelemetry.util._importlib_metadata import (
PackageNotFoundError,
entry_points,
)
from opentelemetry.util._importlib_metadata import entry_points
from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
Expand Down Expand Up @@ -1031,26 +1032,6 @@ def client_response_hook(send_span, scope, message):
)


def mock_version_with_fastapi(*args, **kwargs):
req_name = args[0]
if req_name == "fastapi":
# TODO: Value now matters
return "0.58"
raise PackageNotFoundError()


def mock_version_with_old_fastapi(*args, **kwargs):
req_name = args[0]
if req_name == "fastapi":
# TODO: Value now matters
return "0.57"
raise PackageNotFoundError()


def mock_version_without_fastapi(*args, **kwargs):
raise PackageNotFoundError()


class TestAutoInstrumentation(TestBaseAutoFastAPI):
"""Test the auto-instrumented variant
Expand All @@ -1062,31 +1043,65 @@ def test_entry_point_exists(self):
(ep,) = entry_points(group="opentelemetry_instrumentor")
self.assertEqual(ep.name, "fastapi")

@patch("opentelemetry.instrumentation.dependencies.version")
def test_instruments_with_fastapi_installed(self, mock_version):
mock_version.side_effect = mock_version_with_fastapi
@staticmethod
def _instrumentation_loaded_successfully_call():
return call("Instrumented %s", "fastapi")

@staticmethod
def _instrumentation_failed_to_load_call(dependency_conflict):
return call(
"Skipping instrumentation %s: %s", "fastapi", dependency_conflict
)

@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
def test_instruments_with_fastapi_installed(self, mock_logger):
mock_distro = Mock()
mock_distro.load_instrumentor.return_value = None
_load_instrumentors(mock_distro)
mock_version.assert_called_once_with("fastapi")
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
(ep,) = mock_distro.load_instrumentor.call_args.args
self.assertEqual(ep.name, "fastapi")
mock_logger.debug.assert_has_calls(
[self._instrumentation_loaded_successfully_call()]
)

@patch("opentelemetry.instrumentation.dependencies.version")
def test_instruments_with_old_fastapi_installed(self, mock_version): # pylint: disable=no-self-use
mock_version.side_effect = mock_version_with_old_fastapi
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
def test_instruments_with_old_fastapi_installed(self, mock_logger): # pylint: disable=no-self-use
dependency_conflict = DependencyConflict("0.58", "0.57")
mock_distro = Mock()
mock_distro.load_instrumentor.side_effect = DependencyConflictError(
dependency_conflict
)
_load_instrumentors(mock_distro)
mock_version.assert_called_once_with("fastapi")
mock_distro.load_instrumentor.assert_not_called()
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
(ep,) = mock_distro.load_instrumentor.call_args.args
self.assertEqual(ep.name, "fastapi")
assert (
self._instrumentation_loaded_successfully_call()
not in mock_logger.debug.call_args_list
)
mock_logger.debug.assert_has_calls(
[self._instrumentation_failed_to_load_call(dependency_conflict)]
)

@patch("opentelemetry.instrumentation.dependencies.version")
def test_instruments_without_fastapi_installed(self, mock_version): # pylint: disable=no-self-use
mock_version.side_effect = mock_version_without_fastapi
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
def test_instruments_without_fastapi_installed(self, mock_logger): # pylint: disable=no-self-use
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you open to write similar test to kafka since it's the instrumentation lib stated in your issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I can work on that. I am at a company conference this week but will try to get to it next week.

dependency_conflict = DependencyConflict("0.58", None)
mock_distro = Mock()
mock_distro.load_instrumentor.side_effect = DependencyConflictError(
dependency_conflict
)
_load_instrumentors(mock_distro)
mock_version.assert_called_once_with("fastapi")
mock_distro.load_instrumentor.assert_not_called()
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
(ep,) = mock_distro.load_instrumentor.call_args.args
self.assertEqual(ep.name, "fastapi")
assert (
self._instrumentation_loaded_successfully_call()
not in mock_logger.debug.call_args_list
)
mock_logger.debug.assert_has_calls(
[self._instrumentation_failed_to_load_call(dependency_conflict)]
)

def _create_app(self):
# instrumentation is handled by the instrument call
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from importlib.metadata import PackageNotFoundError
from unittest import TestCase
from unittest.mock import call, patch

from kafka import KafkaConsumer, KafkaProducer
from wrapt import BoundFunctionWrapper

from opentelemetry.instrumentation.kafka import KafkaInstrumentor
from opentelemetry.instrumentation.kafka.package import (
_instruments,
_instruments_kafka_python,
_instruments_kafka_python_ng,
)


class TestKafka(TestCase):
Expand All @@ -34,3 +41,105 @@ def test_instrument_api(self) -> None:
self.assertFalse(
isinstance(KafkaConsumer.__next__, BoundFunctionWrapper)
)

@patch("opentelemetry.instrumentation.kafka.distribution")
def test_instrumentation_dependencies_kafka_python_installed(
self, mock_distribution
) -> None:
instrumentation = KafkaInstrumentor()

def _distribution(name):
if name == "kafka-python":
return None
raise PackageNotFoundError

mock_distribution.side_effect = _distribution
package_to_instrument = instrumentation.instrumentation_dependencies()

self.assertEqual(mock_distribution.call_count, 2)
self.assertEqual(
mock_distribution.mock_calls,
[
call("kafka-python-ng"),
call("kafka-python"),
],
)
self.assertEqual(package_to_instrument, (_instruments_kafka_python,))

@patch("opentelemetry.instrumentation.kafka.distribution")
def test_instrumentation_dependencies_kafka_python_ng_installed(
self, mock_distribution
) -> None:
instrumentation = KafkaInstrumentor()

def _distribution(name):
if name == "kafka-python-ng":
return None
raise PackageNotFoundError

mock_distribution.side_effect = _distribution
package_to_instrument = instrumentation.instrumentation_dependencies()

self.assertEqual(mock_distribution.call_count, 1)
self.assertEqual(
mock_distribution.mock_calls, [call("kafka-python-ng")]
)
self.assertEqual(
package_to_instrument, (_instruments_kafka_python_ng,)
)

@patch("opentelemetry.instrumentation.kafka.distribution")
def test_instrumentation_dependencies_both_installed(
self, mock_distribution
) -> None:
instrumentation = KafkaInstrumentor()

def _distribution(name):
# Function raises PackageNotFoundError
# if name is not in the list. We will
# not raise it for both names
return None

mock_distribution.side_effect = _distribution
package_to_instrument = instrumentation.instrumentation_dependencies()

self.assertEqual(mock_distribution.call_count, 1)
self.assertEqual(
mock_distribution.mock_calls, [call("kafka-python-ng")]
)
self.assertEqual(
package_to_instrument, (_instruments_kafka_python_ng,)
)

@patch("opentelemetry.instrumentation.kafka.distribution")
def test_instrumentation_dependencies_none_installed(
self, mock_distribution
) -> None:
instrumentation = KafkaInstrumentor()

def _distribution(name):
# Function raises PackageNotFoundError
# if name is not in the list. We will
# raise it for both names to simulate
# neither being installed
raise PackageNotFoundError

mock_distribution.side_effect = _distribution
package_to_instrument = instrumentation.instrumentation_dependencies()

assert mock_distribution.call_count == 2
assert mock_distribution.mock_calls == [
call("kafka-python-ng"),
call("kafka-python"),
]
assert package_to_instrument == _instruments

self.assertEqual(mock_distribution.call_count, 2)
self.assertEqual(
mock_distribution.mock_calls,
[
call("kafka-python-ng"),
call("kafka-python"),
],
)
self.assertEqual(package_to_instrument, _instruments)
Loading