From c86dae654565aa0b318a27ba00481f98d2d571a9 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 29 Apr 2021 04:05:29 +0530 Subject: [PATCH] Load instrumentors via Distro This commit makes the following changes: - Introduces a new `load_instrumentor(EntryPoint) -> None:` with a default implementation method to the `BaseDistro` class. - The default implementation loads the insrumentor from the provided entry point and calls applies it without any arguments. (same as before) - sitecustomize now calls Distro's `load_instrumentor` method to load and activate an instrumentor instead of doing it directly. - Added a new `DefaultDistro` implementation which is used if not distro is found by entry points. --- CHANGELOG.md | 3 + .../auto_instrumentation/sitecustomize.py | 37 +++++++++---- .../opentelemetry/instrumentation/distro.py | 26 ++++++++- .../tests/test_distro.py | 55 +++++++++++++++++++ 4 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 opentelemetry-instrumentation/tests/test_distro.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d042f845b..b4e5053138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#472](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/472)) - Set the `traced_request_attrs` of FalconInstrumentor by an argument correctly. ([#473](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/473)) +- Distros can now implement `load_instrumentor(EntryPoint)` method to customize instrumentor + loading behaviour. + ([#480](https://github.com/open-telemetry/opentelemetry-python/pull/480)) ### Added - Move `opentelemetry-instrumentation` from core repository diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index 10c8faf899..e4b3e26de0 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -23,23 +23,34 @@ from opentelemetry.environment_variables import ( OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, ) +from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro logger = getLogger(__file__) -def _load_distros(): +def _load_distros() -> BaseDistro: for entry_point in iter_entry_points("opentelemetry_distro"): try: - entry_point.load()().configure() # type: ignore - logger.debug("Distribution %s configured", entry_point.name) + distro = entry_point.load()() + if not isinstance(distro, BaseDistro): + logger.debug( + "%s is not an OpenTelemetry Distro. Skipping", + entry_point.name, + ) + continue + logger.debug( + "Distribution %s will be configured", entry_point.name + ) + return distro except Exception as exc: # pylint: disable=broad-except logger.exception( "Distribution %s configuration failed", entry_point.name ) raise exc + return DefaultDistro() -def _load_instrumentors(): +def _load_instrumentors(distro): package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) if isinstance(package_to_exclude, str): package_to_exclude = package_to_exclude.split(",") @@ -47,13 +58,14 @@ def _load_instrumentors(): package_to_exclude = [x.strip() for x in package_to_exclude] for entry_point in iter_entry_points("opentelemetry_instrumentor"): + if entry_point.name in package_to_exclude: + logger.debug( + "Instrumentation skipped for library %s", entry_point.name + ) + continue + try: - if entry_point.name in package_to_exclude: - logger.debug( - "Instrumentation skipped for library %s", entry_point.name - ) - continue - entry_point.load()().instrument() # type: ignore + distro.load_instrumentor(entry_point) logger.debug("Instrumented %s", entry_point.name) except Exception as exc: # pylint: disable=broad-except logger.exception("Instrumenting of %s failed", entry_point.name) @@ -80,9 +92,10 @@ def _load_configurators(): def initialize(): try: - _load_distros() + distro = _load_distros() + distro.configure() _load_configurators() - _load_instrumentors() + _load_instrumentors(distro) except Exception: # pylint: disable=broad-except logger.exception("Failed to auto initialize opentelemetry") finally: diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py index 63cacf1f6d..b9814dcc8a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py @@ -20,6 +20,10 @@ from abc import ABC, abstractmethod from logging import getLogger +from pkg_resources import EntryPoint + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + _LOG = getLogger(__name__) @@ -43,5 +47,25 @@ def configure(self, **kwargs): """Configure the distribution""" self._configure(**kwargs) + def load_instrumentor( # pylint: disable=no-self-use + self, entry_point: EntryPoint + ): + """Takes a collection of instrumentation entry points + and activates them by instantiating and calling instrument() + on each one. + + Distros can override this method to customize the behavior by + inspecting each entry point and configuring them in special ways, + passing additional arguments, load a replacement/fork instead, + skip loading entirely, etc. + """ + instrumentor: BaseInstrumentor = entry_point.load() + instrumentor().instrument() + + +class DefaultDistro(BaseDistro): + def _configure(self, **kwargs): + pass + -__all__ = ["BaseDistro"] +__all__ = ["BaseDistro", "DefaultDistro"] diff --git a/opentelemetry-instrumentation/tests/test_distro.py b/opentelemetry-instrumentation/tests/test_distro.py new file mode 100644 index 0000000000..d955827f54 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_distro.py @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. +# type: ignore + +from unittest import TestCase + +from pkg_resources import EntryPoint + +from opentelemetry.instrumentation.distro import BaseDistro +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + + +class MockInstrumetor(BaseInstrumentor): + def _instrument(self, **kwargs): + pass + + def _uninstrument(self, **kwargs): + pass + + +class MockEntryPoint(EntryPoint): + def __init__(self, obj): # pylint: disable=super-init-not-called + self._obj = obj + + def load(self, *args, **kwargs): # pylint: disable=signature-differs + return self._obj + + +class MockDistro(BaseDistro): + def _configure(self, **kwargs): + pass + + +class TestDistro(TestCase): + def test_load_instrumentor(self): + # pylint: disable=protected-access + distro = MockDistro() + + instrumentor = MockInstrumetor() + entry_point = MockEntryPoint(MockInstrumetor) + + self.assertFalse(instrumentor._is_instrumented) + distro.load_instrumentor(entry_point) + self.assertTrue(instrumentor._is_instrumented)