mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-31 06:03:21 +08:00
Add "instruments-any" feature: unblock multi-target instrumentations while fixing dependency conflict breakage. (#3610)
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@ -11,6 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `opentelemetry-instrumentation`: Fix dependency conflict detection when instrumented packages are not installed by moving check back to before instrumentors are loaded. Add "instruments-any" feature for instrumentations that target multiple packages.
|
||||||
|
([#3610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610))
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `opentelemetry-instrumentation-psycopg2` Utilize instruments-any functionality.
|
||||||
|
([#3610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610))
|
||||||
|
- `opentelemetry-instrumentation-kafka-python` Utilize instruments-any functionality.
|
||||||
|
([#3610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610))
|
||||||
|
|
||||||
## Version 1.35.0/0.56b0 (2025-07-11)
|
## Version 1.35.0/0.56b0 (2025-07-11)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -325,10 +325,25 @@ Below is a checklist of things to be mindful of when implementing a new instrume
|
|||||||
### Update supported instrumentation package versions
|
### Update supported instrumentation package versions
|
||||||
|
|
||||||
- Navigate to the **instrumentation package directory:**
|
- Navigate to the **instrumentation package directory:**
|
||||||
- Update **`pyproject.toml`** file by modifying _instruments_ entry in the `[project.optional-dependencies]` section with the new version constraint
|
- Update **`pyproject.toml`** file by modifying `instruments` or `instruments-any` entry in the `[project.optional-dependencies]` section with the new version constraint
|
||||||
- Update `_instruments` variable in instrumentation **`package.py`** file with the new version constraint
|
- Update `_instruments` or `_instruments_any` variable in instrumentation **`package.py`** file with the new version constraint
|
||||||
- At the **root of the project directory**, run `tox -e generate` to regenerate necessary files
|
- At the **root of the project directory**, run `tox -e generate` to regenerate necessary files
|
||||||
|
|
||||||
|
Please note that `instruments-any` is an optional field that can be used instead of or in addition to `instruments`. While `instruments` is a list of dependencies, _all_ of which are expected by the instrumentation, `instruments-any` is a list _any_ of which but not all are expected. For example, the following entry requires both `util` and `common` plus either `foo` or `bar` to be present for the instrumentation to occur:
|
||||||
|
```
|
||||||
|
[project.optional-dependencies]
|
||||||
|
instruments = [
|
||||||
|
"util ~= 1.0"
|
||||||
|
"common ~= 2.0"
|
||||||
|
]
|
||||||
|
instruments-any = [
|
||||||
|
"foo ~= 3.0"
|
||||||
|
"bar ~= 4.0"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- See https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610 for details on instruments-any -->
|
||||||
|
|
||||||
If you're adding support for a new version of the instrumentation package, follow these additional steps:
|
If you're adding support for a new version of the instrumentation package, follow these additional steps:
|
||||||
|
|
||||||
- At the **instrumentation package directory:** Add new test-requirements.txt file with the respective package version required for testing
|
- At the **instrumentation package directory:** Add new test-requirements.txt file with the respective package version required for testing
|
||||||
|
@ -40,7 +40,6 @@ from opentelemetry.instrumentation.auto_instrumentation._load import (
|
|||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.dependencies import (
|
from opentelemetry.instrumentation.dependencies import (
|
||||||
DependencyConflict,
|
DependencyConflict,
|
||||||
DependencyConflictError,
|
|
||||||
)
|
)
|
||||||
from opentelemetry.sdk.metrics.export import (
|
from opentelemetry.sdk.metrics.export import (
|
||||||
HistogramDataPoint,
|
HistogramDataPoint,
|
||||||
@ -1102,40 +1101,34 @@ class TestAutoInstrumentation(TestBaseAutoFastAPI):
|
|||||||
[self._instrumentation_loaded_successfully_call()]
|
[self._instrumentation_loaded_successfully_call()]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
|
||||||
|
)
|
||||||
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
||||||
def test_instruments_with_old_fastapi_installed(self, mock_logger): # pylint: disable=no-self-use
|
def test_instruments_with_old_fastapi_installed(
|
||||||
|
self, mock_logger, mock_dep
|
||||||
|
): # pylint: disable=no-self-use
|
||||||
dependency_conflict = DependencyConflict("0.58", "0.57")
|
dependency_conflict = DependencyConflict("0.58", "0.57")
|
||||||
mock_distro = Mock()
|
mock_distro = Mock()
|
||||||
mock_distro.load_instrumentor.side_effect = DependencyConflictError(
|
mock_dep.return_value = dependency_conflict
|
||||||
dependency_conflict
|
|
||||||
)
|
|
||||||
_load_instrumentors(mock_distro)
|
_load_instrumentors(mock_distro)
|
||||||
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
|
mock_distro.load_instrumentor.assert_not_called()
|
||||||
(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(
|
mock_logger.debug.assert_has_calls(
|
||||||
[self._instrumentation_failed_to_load_call(dependency_conflict)]
|
[self._instrumentation_failed_to_load_call(dependency_conflict)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
|
||||||
|
)
|
||||||
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
||||||
def test_instruments_without_fastapi_installed(self, mock_logger): # pylint: disable=no-self-use
|
def test_instruments_without_fastapi_installed(
|
||||||
|
self, mock_logger, mock_dep
|
||||||
|
): # pylint: disable=no-self-use
|
||||||
dependency_conflict = DependencyConflict("0.58", None)
|
dependency_conflict = DependencyConflict("0.58", None)
|
||||||
mock_distro = Mock()
|
mock_distro = Mock()
|
||||||
mock_distro.load_instrumentor.side_effect = DependencyConflictError(
|
mock_dep.return_value = dependency_conflict
|
||||||
dependency_conflict
|
|
||||||
)
|
|
||||||
_load_instrumentors(mock_distro)
|
_load_instrumentors(mock_distro)
|
||||||
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
|
mock_distro.load_instrumentor.assert_not_called()
|
||||||
(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(
|
mock_logger.debug.assert_has_calls(
|
||||||
[self._instrumentation_failed_to_load_call(dependency_conflict)]
|
[self._instrumentation_failed_to_load_call(dependency_conflict)]
|
||||||
)
|
)
|
||||||
|
@ -31,7 +31,8 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
instruments = [
|
instruments = []
|
||||||
|
instruments-any = [
|
||||||
"kafka-python >= 2.0, < 3.0",
|
"kafka-python >= 2.0, < 3.0",
|
||||||
"kafka-python-ng >= 2.0, < 3.0"
|
"kafka-python-ng >= 2.0, < 3.0"
|
||||||
]
|
]
|
||||||
|
@ -91,7 +91,7 @@ from wrapt import wrap_function_wrapper
|
|||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||||
from opentelemetry.instrumentation.kafka.package import (
|
from opentelemetry.instrumentation.kafka.package import (
|
||||||
_instruments,
|
_instruments_any,
|
||||||
_instruments_kafka_python,
|
_instruments_kafka_python,
|
||||||
_instruments_kafka_python_ng,
|
_instruments_kafka_python_ng,
|
||||||
)
|
)
|
||||||
@ -123,7 +123,7 @@ class KafkaInstrumentor(BaseInstrumentor):
|
|||||||
except PackageNotFoundError:
|
except PackageNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return _instruments
|
return _instruments_any
|
||||||
|
|
||||||
def _instrument(self, **kwargs):
|
def _instrument(self, **kwargs):
|
||||||
"""Instruments the kafka module
|
"""Instruments the kafka module
|
||||||
|
@ -16,4 +16,5 @@
|
|||||||
_instruments_kafka_python = "kafka-python >= 2.0, < 3.0"
|
_instruments_kafka_python = "kafka-python >= 2.0, < 3.0"
|
||||||
_instruments_kafka_python_ng = "kafka-python-ng >= 2.0, < 3.0"
|
_instruments_kafka_python_ng = "kafka-python-ng >= 2.0, < 3.0"
|
||||||
|
|
||||||
_instruments = (_instruments_kafka_python, _instruments_kafka_python_ng)
|
_instruments = ()
|
||||||
|
_instruments_any = (_instruments_kafka_python, _instruments_kafka_python_ng)
|
||||||
|
@ -20,7 +20,6 @@ from wrapt import BoundFunctionWrapper
|
|||||||
|
|
||||||
from opentelemetry.instrumentation.kafka import KafkaInstrumentor
|
from opentelemetry.instrumentation.kafka import KafkaInstrumentor
|
||||||
from opentelemetry.instrumentation.kafka.package import (
|
from opentelemetry.instrumentation.kafka.package import (
|
||||||
_instruments,
|
|
||||||
_instruments_kafka_python,
|
_instruments_kafka_python,
|
||||||
_instruments_kafka_python_ng,
|
_instruments_kafka_python_ng,
|
||||||
)
|
)
|
||||||
@ -134,4 +133,7 @@ class TestKafka(TestCase):
|
|||||||
call("kafka-python"),
|
call("kafka-python"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.assertEqual(package_to_instrument, _instruments)
|
self.assertEqual(
|
||||||
|
package_to_instrument,
|
||||||
|
(_instruments_kafka_python, _instruments_kafka_python_ng),
|
||||||
|
)
|
||||||
|
@ -31,7 +31,8 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
instruments = [
|
instruments = []
|
||||||
|
instruments-any = [
|
||||||
"psycopg2 >= 2.7.3.1",
|
"psycopg2 >= 2.7.3.1",
|
||||||
"psycopg2-binary >= 2.7.3.1",
|
"psycopg2-binary >= 2.7.3.1",
|
||||||
]
|
]
|
||||||
|
@ -151,7 +151,7 @@ from psycopg2.sql import Composed # pylint: disable=no-name-in-module
|
|||||||
from opentelemetry.instrumentation import dbapi
|
from opentelemetry.instrumentation import dbapi
|
||||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||||
from opentelemetry.instrumentation.psycopg2.package import (
|
from opentelemetry.instrumentation.psycopg2.package import (
|
||||||
_instruments,
|
_instruments_any,
|
||||||
_instruments_psycopg2,
|
_instruments_psycopg2,
|
||||||
_instruments_psycopg2_binary,
|
_instruments_psycopg2_binary,
|
||||||
)
|
)
|
||||||
@ -187,7 +187,7 @@ class Psycopg2Instrumentor(BaseInstrumentor):
|
|||||||
except PackageNotFoundError:
|
except PackageNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return _instruments
|
return _instruments_any
|
||||||
|
|
||||||
def _instrument(self, **kwargs):
|
def _instrument(self, **kwargs):
|
||||||
"""Integrate with PostgreSQL Psycopg library.
|
"""Integrate with PostgreSQL Psycopg library.
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
_instruments_psycopg2 = "psycopg2 >= 2.7.3.1"
|
_instruments_psycopg2 = "psycopg2 >= 2.7.3.1"
|
||||||
_instruments_psycopg2_binary = "psycopg2-binary >= 2.7.3.1"
|
_instruments_psycopg2_binary = "psycopg2-binary >= 2.7.3.1"
|
||||||
|
|
||||||
_instruments = (
|
_instruments = ()
|
||||||
|
_instruments_any = (
|
||||||
_instruments_psycopg2,
|
_instruments_psycopg2,
|
||||||
_instruments_psycopg2_binary,
|
_instruments_psycopg2_binary,
|
||||||
)
|
)
|
||||||
|
@ -20,7 +20,6 @@ from opentelemetry.instrumentation.auto_instrumentation._load import (
|
|||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
|
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
|
||||||
from opentelemetry.instrumentation.psycopg2.package import (
|
from opentelemetry.instrumentation.psycopg2.package import (
|
||||||
_instruments,
|
|
||||||
_instruments_psycopg2,
|
_instruments_psycopg2,
|
||||||
_instruments_psycopg2_binary,
|
_instruments_psycopg2_binary,
|
||||||
)
|
)
|
||||||
@ -130,19 +129,29 @@ class TestPsycopg2InstrumentationDependencies(TestCase):
|
|||||||
call("psycopg2-binary"),
|
call("psycopg2-binary"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.assertEqual(package_to_instrument, _instruments)
|
self.assertEqual(
|
||||||
|
package_to_instrument,
|
||||||
|
(
|
||||||
|
_instruments_psycopg2,
|
||||||
|
_instruments_psycopg2_binary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# This test is to verify that the auto instrumentation path
|
# This test is to verify that the auto instrumentation path
|
||||||
# will auto instrument psycopg2 or psycopg2-binary is installed.
|
# will auto instrument psycopg2 or psycopg2-binary is installed.
|
||||||
# Note there is only one test here but it is run twice in tox
|
# Note there is only one test here but it is run twice in tox
|
||||||
# once with the psycopg2 package installed and once with
|
# once with the psycopg2 package installed and once with
|
||||||
# psycopg2-binary installed.
|
# psycopg2-binary installed.
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
|
||||||
|
)
|
||||||
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
||||||
def test_instruments_with_psycopg2_installed(self, mock_logger):
|
def test_instruments_with_psycopg2_installed(self, mock_logger, mock_dep):
|
||||||
def _instrumentation_loaded_successfully_call():
|
def _instrumentation_loaded_successfully_call():
|
||||||
return call("Instrumented %s", "psycopg2")
|
return call("Instrumented %s", "psycopg2")
|
||||||
|
|
||||||
mock_distro = Mock()
|
mock_distro = Mock()
|
||||||
|
mock_dep.return_value = None
|
||||||
mock_distro.load_instrumentor.return_value = None
|
mock_distro.load_instrumentor.return_value = None
|
||||||
_load_instrumentors(mock_distro)
|
_load_instrumentors(mock_distro)
|
||||||
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
|
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
|
||||||
|
@ -12,10 +12,14 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from functools import cached_property
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
from opentelemetry.instrumentation.dependencies import DependencyConflictError
|
from opentelemetry.instrumentation.dependencies import (
|
||||||
|
DependencyConflictError,
|
||||||
|
get_dist_dependency_conflicts,
|
||||||
|
)
|
||||||
from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro
|
from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro
|
||||||
from opentelemetry.instrumentation.environment_variables import (
|
from opentelemetry.instrumentation.environment_variables import (
|
||||||
OTEL_PYTHON_CONFIGURATOR,
|
OTEL_PYTHON_CONFIGURATOR,
|
||||||
@ -23,11 +27,36 @@ from opentelemetry.instrumentation.environment_variables import (
|
|||||||
OTEL_PYTHON_DISTRO,
|
OTEL_PYTHON_DISTRO,
|
||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.version import __version__
|
from opentelemetry.instrumentation.version import __version__
|
||||||
from opentelemetry.util._importlib_metadata import entry_points
|
from opentelemetry.util._importlib_metadata import (
|
||||||
|
EntryPoint,
|
||||||
|
distributions,
|
||||||
|
entry_points,
|
||||||
|
)
|
||||||
|
|
||||||
_logger = getLogger(__name__)
|
_logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class _EntryPointDistFinder:
|
||||||
|
@cached_property
|
||||||
|
def _mapping(self):
|
||||||
|
return {
|
||||||
|
self._key_for(ep): dist
|
||||||
|
for dist in distributions()
|
||||||
|
for ep in dist.entry_points
|
||||||
|
}
|
||||||
|
|
||||||
|
def dist_for(self, entry_point: EntryPoint):
|
||||||
|
dist = getattr(entry_point, "dist", None)
|
||||||
|
if dist:
|
||||||
|
return dist
|
||||||
|
|
||||||
|
return self._mapping.get(self._key_for(entry_point))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _key_for(entry_point: EntryPoint):
|
||||||
|
return f"{entry_point.group}:{entry_point.name}:{entry_point.value}"
|
||||||
|
|
||||||
|
|
||||||
def _load_distro() -> BaseDistro:
|
def _load_distro() -> BaseDistro:
|
||||||
distro_name = environ.get(OTEL_PYTHON_DISTRO, None)
|
distro_name = environ.get(OTEL_PYTHON_DISTRO, None)
|
||||||
for entry_point in entry_points(group="opentelemetry_distro"):
|
for entry_point in entry_points(group="opentelemetry_distro"):
|
||||||
@ -55,6 +84,7 @@ def _load_distro() -> BaseDistro:
|
|||||||
|
|
||||||
def _load_instrumentors(distro):
|
def _load_instrumentors(distro):
|
||||||
package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, [])
|
package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, [])
|
||||||
|
entry_point_finder = _EntryPointDistFinder()
|
||||||
if isinstance(package_to_exclude, str):
|
if isinstance(package_to_exclude, str):
|
||||||
package_to_exclude = package_to_exclude.split(",")
|
package_to_exclude = package_to_exclude.split(",")
|
||||||
# to handle users entering "requests , flask" or "requests, flask" with spaces
|
# to handle users entering "requests , flask" or "requests, flask" with spaces
|
||||||
@ -71,11 +101,24 @@ def _load_instrumentors(distro):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
distro.load_instrumentor(
|
entry_point_dist = entry_point_finder.dist_for(entry_point)
|
||||||
entry_point, raise_exception_on_conflict=True
|
conflict = get_dist_dependency_conflicts(entry_point_dist)
|
||||||
)
|
if conflict:
|
||||||
|
_logger.debug(
|
||||||
|
"Skipping instrumentation %s: %s",
|
||||||
|
entry_point.name,
|
||||||
|
conflict,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# tell instrumentation to not run dep checks again as we already did it above
|
||||||
|
distro.load_instrumentor(entry_point, skip_dep_check=True)
|
||||||
_logger.debug("Instrumented %s", entry_point.name)
|
_logger.debug("Instrumented %s", entry_point.name)
|
||||||
except DependencyConflictError as exc:
|
except DependencyConflictError as exc:
|
||||||
|
# Dependency conflicts are generally caught from get_dist_dependency_conflicts
|
||||||
|
# returning a DependencyConflict. Keeping this error handling in case custom
|
||||||
|
# distro and instrumentor behavior raises a DependencyConflictError later.
|
||||||
|
# See https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610
|
||||||
_logger.debug(
|
_logger.debug(
|
||||||
"Skipping instrumentation %s: %s",
|
"Skipping instrumentation %s: %s",
|
||||||
entry_point.name,
|
entry_point.name,
|
||||||
|
@ -20,6 +20,7 @@ from typing import Collection
|
|||||||
from packaging.requirements import InvalidRequirement, Requirement
|
from packaging.requirements import InvalidRequirement, Requirement
|
||||||
|
|
||||||
from opentelemetry.util._importlib_metadata import (
|
from opentelemetry.util._importlib_metadata import (
|
||||||
|
Distribution,
|
||||||
PackageNotFoundError,
|
PackageNotFoundError,
|
||||||
version,
|
version,
|
||||||
)
|
)
|
||||||
@ -28,14 +29,44 @@ logger = getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DependencyConflict:
|
class DependencyConflict:
|
||||||
|
"""Represents a dependency conflict in OpenTelemetry instrumentation.
|
||||||
|
|
||||||
|
This class is used to track conflicts between required dependencies and the
|
||||||
|
actual installed packages. It supports two scenarios:
|
||||||
|
|
||||||
|
1. Standard conflicts where all dependencies are required
|
||||||
|
2. Either/or conflicts where only one of a set of dependencies is required
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
required: The required dependency specification that conflicts with what's installed.
|
||||||
|
found: The actual dependency that was found installed (if any).
|
||||||
|
required_any: Collection of dependency specifications where any one would satisfy
|
||||||
|
the requirement (for either/or scenarios).
|
||||||
|
found_any: Collection of actual dependencies found for either/or scenarios.
|
||||||
|
"""
|
||||||
|
|
||||||
required: str | None = None
|
required: str | None = None
|
||||||
found: str | None = None
|
found: str | None = None
|
||||||
|
# The following fields are used when an instrumentation requires any of a set of dependencies rather than all.
|
||||||
|
required_any: Collection[str] = None
|
||||||
|
found_any: Collection[str] = None
|
||||||
|
|
||||||
def __init__(self, required: str | None, found: str | None = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
required: str | None = None,
|
||||||
|
found: str | None = None,
|
||||||
|
required_any: Collection[str] = None,
|
||||||
|
found_any: Collection[str] = None,
|
||||||
|
):
|
||||||
self.required = required
|
self.required = required
|
||||||
self.found = found
|
self.found = found
|
||||||
|
# The following fields are used when an instrumentation requires any of a set of dependencies rather than all.
|
||||||
|
self.required_any = required_any
|
||||||
|
self.found_any = found_any
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if not self.required and (self.required_any or self.found_any):
|
||||||
|
return f'DependencyConflict: requested any of the following: "{self.required_any}" but found: "{self.found_any}"'
|
||||||
return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"'
|
return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"'
|
||||||
|
|
||||||
|
|
||||||
@ -49,8 +80,39 @@ class DependencyConflictError(Exception):
|
|||||||
return str(self.conflict)
|
return str(self.conflict)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dist_dependency_conflicts(
|
||||||
|
dist: Distribution,
|
||||||
|
) -> DependencyConflict | None:
|
||||||
|
instrumentation_deps = []
|
||||||
|
instrumentation_any_deps = []
|
||||||
|
extra = "extra"
|
||||||
|
instruments = "instruments"
|
||||||
|
instruments_marker = {extra: instruments}
|
||||||
|
instruments_any = "instruments-any"
|
||||||
|
instruments_any_marker = {extra: instruments_any}
|
||||||
|
if dist.requires:
|
||||||
|
for dep in dist.requires:
|
||||||
|
if extra not in dep:
|
||||||
|
continue
|
||||||
|
if instruments not in dep and instruments_any not in dep:
|
||||||
|
continue
|
||||||
|
|
||||||
|
req = Requirement(dep)
|
||||||
|
if req.marker.evaluate(instruments_marker): # type: ignore
|
||||||
|
instrumentation_deps.append(req) # type: ignore
|
||||||
|
if req.marker.evaluate(instruments_any_marker): # type: ignore
|
||||||
|
instrumentation_any_deps.append(req) # type: ignore
|
||||||
|
return get_dependency_conflicts(
|
||||||
|
instrumentation_deps, instrumentation_any_deps
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def get_dependency_conflicts(
|
def get_dependency_conflicts(
|
||||||
deps: Collection[str | Requirement],
|
deps: Collection[
|
||||||
|
str | Requirement
|
||||||
|
], # Dependencies all of which are required
|
||||||
|
deps_any: Collection[str | Requirement]
|
||||||
|
| None = None, # Dependencies any of which are required
|
||||||
) -> DependencyConflict | None:
|
) -> DependencyConflict | None:
|
||||||
for dep in deps:
|
for dep in deps:
|
||||||
if isinstance(dep, Requirement):
|
if isinstance(dep, Requirement):
|
||||||
@ -73,4 +135,53 @@ def get_dependency_conflicts(
|
|||||||
|
|
||||||
if not req.specifier.contains(dist_version):
|
if not req.specifier.contains(dist_version):
|
||||||
return DependencyConflict(dep, f"{req.name} {dist_version}")
|
return DependencyConflict(dep, f"{req.name} {dist_version}")
|
||||||
|
|
||||||
|
# If all the dependencies in "instruments" are present, check "instruments-any" for conflicts.
|
||||||
|
if deps_any:
|
||||||
|
return _get_dependency_conflicts_any(deps_any)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# This is a helper functions designed to ease reading and meet linting requirements.
|
||||||
|
def _get_dependency_conflicts_any(
|
||||||
|
deps_any: Collection[str | Requirement],
|
||||||
|
) -> DependencyConflict | None:
|
||||||
|
if not deps_any:
|
||||||
|
return None
|
||||||
|
is_dependency_conflict = True
|
||||||
|
required_any: Collection[str] = []
|
||||||
|
found_any: Collection[str] = []
|
||||||
|
for dep in deps_any:
|
||||||
|
if isinstance(dep, Requirement):
|
||||||
|
req = dep
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
req = Requirement(dep)
|
||||||
|
except InvalidRequirement as exc:
|
||||||
|
logger.warning(
|
||||||
|
'error parsing dependency, reporting as a conflict: "%s" - %s',
|
||||||
|
dep,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
return DependencyConflict(dep)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dist_version = version(req.name)
|
||||||
|
except PackageNotFoundError:
|
||||||
|
required_any.append(str(dep))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if req.specifier.contains(dist_version):
|
||||||
|
# Since only one of the instrumentation_any dependencies is required, there is no dependency conflict.
|
||||||
|
is_dependency_conflict = False
|
||||||
|
break
|
||||||
|
# If the version does not match, add it to the list of unfulfilled requirement options.
|
||||||
|
required_any.append(str(dep))
|
||||||
|
found_any.append(f"{req.name} {dist_version}")
|
||||||
|
|
||||||
|
if is_dependency_conflict:
|
||||||
|
return DependencyConflict(
|
||||||
|
required_any=required_any,
|
||||||
|
found_any=found_any,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
@ -19,7 +19,6 @@ from unittest.mock import Mock, call, patch
|
|||||||
from opentelemetry.instrumentation.auto_instrumentation import _load
|
from opentelemetry.instrumentation.auto_instrumentation import _load
|
||||||
from opentelemetry.instrumentation.dependencies import (
|
from opentelemetry.instrumentation.dependencies import (
|
||||||
DependencyConflict,
|
DependencyConflict,
|
||||||
DependencyConflictError,
|
|
||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.environment_variables import (
|
from opentelemetry.instrumentation.environment_variables import (
|
||||||
OTEL_PYTHON_CONFIGURATOR,
|
OTEL_PYTHON_CONFIGURATOR,
|
||||||
@ -27,6 +26,7 @@ from opentelemetry.instrumentation.environment_variables import (
|
|||||||
OTEL_PYTHON_DISTRO,
|
OTEL_PYTHON_DISTRO,
|
||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.version import __version__
|
from opentelemetry.instrumentation.version import __version__
|
||||||
|
from opentelemetry.util._importlib_metadata import EntryPoint, entry_points
|
||||||
|
|
||||||
|
|
||||||
class TestLoad(TestCase):
|
class TestLoad(TestCase):
|
||||||
@ -213,10 +213,13 @@ class TestLoad(TestCase):
|
|||||||
"os.environ",
|
"os.environ",
|
||||||
{OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: " instr1 , instr3 "},
|
{OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: " instr1 , instr3 "},
|
||||||
)
|
)
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
|
||||||
|
)
|
||||||
@patch(
|
@patch(
|
||||||
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
||||||
)
|
)
|
||||||
def test_load_instrumentors(self, iter_mock):
|
def test_load_instrumentors(self, iter_mock, mock_dep):
|
||||||
# Mock opentelemetry_pre_instrument entry points
|
# Mock opentelemetry_pre_instrument entry points
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
pre_ep_mock1 = Mock()
|
pre_ep_mock1 = Mock()
|
||||||
@ -261,6 +264,8 @@ class TestLoad(TestCase):
|
|||||||
(ep_mock1, ep_mock2, ep_mock3, ep_mock4),
|
(ep_mock1, ep_mock2, ep_mock3, ep_mock4),
|
||||||
(post_ep_mock1, post_ep_mock2),
|
(post_ep_mock1, post_ep_mock2),
|
||||||
]
|
]
|
||||||
|
# No dependency conflict
|
||||||
|
mock_dep.return_value = None
|
||||||
_load._load_instrumentors(distro_mock)
|
_load._load_instrumentors(distro_mock)
|
||||||
# All opentelemetry_pre_instrument entry points should be loaded
|
# All opentelemetry_pre_instrument entry points should be loaded
|
||||||
pre_mock1.assert_called_once()
|
pre_mock1.assert_called_once()
|
||||||
@ -269,8 +274,8 @@ class TestLoad(TestCase):
|
|||||||
# Only non-disabled instrumentations should be loaded
|
# Only non-disabled instrumentations should be loaded
|
||||||
distro_mock.load_instrumentor.assert_has_calls(
|
distro_mock.load_instrumentor.assert_has_calls(
|
||||||
[
|
[
|
||||||
call(ep_mock2, raise_exception_on_conflict=True),
|
call(ep_mock2, skip_dep_check=True),
|
||||||
call(ep_mock4, raise_exception_on_conflict=True),
|
call(ep_mock4, skip_dep_check=True),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.assertEqual(distro_mock.load_instrumentor.call_count, 2)
|
self.assertEqual(distro_mock.load_instrumentor.call_count, 2)
|
||||||
@ -282,56 +287,71 @@ class TestLoad(TestCase):
|
|||||||
"os.environ",
|
"os.environ",
|
||||||
{OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: " instr1 , instr3 "},
|
{OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: " instr1 , instr3 "},
|
||||||
)
|
)
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
|
||||||
|
)
|
||||||
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
||||||
@patch(
|
@patch(
|
||||||
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
||||||
)
|
)
|
||||||
def test_load_instrumentors_dep_conflict(self, iter_mock, mock_logger): # pylint: disable=no-self-use
|
def test_load_instrumentors_dep_conflict(
|
||||||
|
self, iter_mock, mock_logger, mock_dep
|
||||||
|
): # pylint: disable=no-self-use
|
||||||
ep_mock1 = Mock()
|
ep_mock1 = Mock()
|
||||||
ep_mock1.name = "instr1"
|
ep_mock1.name = "instr1" # disabled
|
||||||
|
|
||||||
ep_mock2 = Mock()
|
ep_mock2 = Mock()
|
||||||
ep_mock2.name = "instr2"
|
ep_mock2.name = "instr2"
|
||||||
|
|
||||||
ep_mock3 = Mock()
|
ep_mock3 = Mock()
|
||||||
ep_mock3.name = "instr3"
|
ep_mock3.name = "instr3" # disabled
|
||||||
|
|
||||||
ep_mock4 = Mock()
|
ep_mock4 = Mock()
|
||||||
ep_mock4.name = "instr4"
|
ep_mock4.name = "instr4" # dependency conflict
|
||||||
|
|
||||||
dependency_conflict = DependencyConflict("1.2.3", None)
|
dependency_conflict = DependencyConflict("1.2.3", None)
|
||||||
|
|
||||||
distro_mock = Mock()
|
distro_mock = Mock()
|
||||||
distro_mock.load_instrumentor.side_effect = [
|
|
||||||
None,
|
|
||||||
DependencyConflictError(dependency_conflict),
|
|
||||||
]
|
|
||||||
|
|
||||||
# If a dependency conflict is raised, that instrumentation should not be loaded, but others still should.
|
|
||||||
# In this case, ep_mock4 will fail to load and ep_mock2 will succeed. (ep_mock1 and ep_mock3 are disabled)
|
|
||||||
iter_mock.return_value = (ep_mock1, ep_mock2, ep_mock3, ep_mock4)
|
iter_mock.return_value = (ep_mock1, ep_mock2, ep_mock3, ep_mock4)
|
||||||
|
print((ep_mock1, ep_mock2, ep_mock3, ep_mock4))
|
||||||
|
# If a dependency conflict is raised, that instrumentation should not be loaded, but others still should.
|
||||||
|
# In this case, ep_mock4 will not be loaded and ep_mock2 will succeed. (ep_mock1 and ep_mock3 are disabled)
|
||||||
|
mock_dep.side_effect = [None, dependency_conflict]
|
||||||
_load._load_instrumentors(distro_mock)
|
_load._load_instrumentors(distro_mock)
|
||||||
distro_mock.load_instrumentor.assert_has_calls(
|
distro_mock.load_instrumentor.assert_has_calls(
|
||||||
[
|
[
|
||||||
call(ep_mock2, raise_exception_on_conflict=True),
|
call(ep_mock2, skip_dep_check=True),
|
||||||
call(ep_mock4, raise_exception_on_conflict=True),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert distro_mock.load_instrumentor.call_count == 2
|
distro_mock.load_instrumentor.assert_called_once()
|
||||||
mock_logger.debug.assert_has_calls(
|
mock_logger.debug.assert_has_calls(
|
||||||
[
|
[
|
||||||
|
call(
|
||||||
|
"Instrumentation skipped for library %s",
|
||||||
|
ep_mock1.name,
|
||||||
|
),
|
||||||
|
call("Instrumented %s", ep_mock2.name),
|
||||||
|
call(
|
||||||
|
"Instrumentation skipped for library %s",
|
||||||
|
ep_mock3.name,
|
||||||
|
),
|
||||||
self._instrumentation_failed_to_load_call(
|
self._instrumentation_failed_to_load_call(
|
||||||
ep_mock4.name, dependency_conflict
|
ep_mock4.name,
|
||||||
)
|
dependency_conflict,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
|
||||||
|
)
|
||||||
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
||||||
@patch(
|
@patch(
|
||||||
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
||||||
)
|
)
|
||||||
def test_load_instrumentors_import_error_does_not_stop_everything(
|
def test_load_instrumentors_import_error_does_not_stop_everything(
|
||||||
self, iter_mock, mock_logger
|
self, iter_mock, mock_logger, mock_dep
|
||||||
):
|
):
|
||||||
ep_mock1 = Mock(name="instr1")
|
ep_mock1 = Mock(name="instr1")
|
||||||
ep_mock2 = Mock(name="instr2")
|
ep_mock2 = Mock(name="instr2")
|
||||||
@ -345,13 +365,14 @@ class TestLoad(TestCase):
|
|||||||
(ep_mock1, ep_mock2),
|
(ep_mock1, ep_mock2),
|
||||||
(),
|
(),
|
||||||
]
|
]
|
||||||
|
mock_dep.return_value = None
|
||||||
|
|
||||||
_load._load_instrumentors(distro_mock)
|
_load._load_instrumentors(distro_mock)
|
||||||
|
|
||||||
distro_mock.load_instrumentor.assert_has_calls(
|
distro_mock.load_instrumentor.assert_has_calls(
|
||||||
[
|
[
|
||||||
call(ep_mock1, raise_exception_on_conflict=True),
|
call(ep_mock1, skip_dep_check=True),
|
||||||
call(ep_mock2, raise_exception_on_conflict=True),
|
call(ep_mock2, skip_dep_check=True),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.assertEqual(distro_mock.load_instrumentor.call_count, 2)
|
self.assertEqual(distro_mock.load_instrumentor.call_count, 2)
|
||||||
@ -362,10 +383,13 @@ class TestLoad(TestCase):
|
|||||||
|
|
||||||
mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name)
|
mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
|
||||||
|
)
|
||||||
@patch(
|
@patch(
|
||||||
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
||||||
)
|
)
|
||||||
def test_load_instrumentors_raises_exception(self, iter_mock):
|
def test_load_instrumentors_raises_exception(self, iter_mock, mock_dep):
|
||||||
ep_mock1 = Mock(name="instr1")
|
ep_mock1 = Mock(name="instr1")
|
||||||
ep_mock2 = Mock(name="instr2")
|
ep_mock2 = Mock(name="instr2")
|
||||||
|
|
||||||
@ -378,23 +402,27 @@ class TestLoad(TestCase):
|
|||||||
(ep_mock1, ep_mock2),
|
(ep_mock1, ep_mock2),
|
||||||
(),
|
(),
|
||||||
]
|
]
|
||||||
|
mock_dep.return_value = None
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
_load._load_instrumentors(distro_mock)
|
_load._load_instrumentors(distro_mock)
|
||||||
|
|
||||||
distro_mock.load_instrumentor.assert_has_calls(
|
distro_mock.load_instrumentor.assert_has_calls(
|
||||||
[
|
[
|
||||||
call(ep_mock1, raise_exception_on_conflict=True),
|
call(ep_mock1, skip_dep_check=True),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.assertEqual(distro_mock.load_instrumentor.call_count, 1)
|
self.assertEqual(distro_mock.load_instrumentor.call_count, 1)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
|
||||||
|
)
|
||||||
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
|
||||||
@patch(
|
@patch(
|
||||||
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
|
||||||
)
|
)
|
||||||
def test_load_instrumentors_module_not_found_error(
|
def test_load_instrumentors_module_not_found_error(
|
||||||
self, iter_mock, mock_logger
|
self, iter_mock, mock_logger, mock_dep
|
||||||
):
|
):
|
||||||
ep_mock1 = Mock()
|
ep_mock1 = Mock()
|
||||||
ep_mock1.name = "instr1"
|
ep_mock1.name = "instr1"
|
||||||
@ -404,6 +432,8 @@ class TestLoad(TestCase):
|
|||||||
|
|
||||||
distro_mock = Mock()
|
distro_mock = Mock()
|
||||||
|
|
||||||
|
mock_dep.return_value = None
|
||||||
|
|
||||||
distro_mock.load_instrumentor.side_effect = [
|
distro_mock.load_instrumentor.side_effect = [
|
||||||
ModuleNotFoundError("No module named 'fake_module'"),
|
ModuleNotFoundError("No module named 'fake_module'"),
|
||||||
None,
|
None,
|
||||||
@ -415,8 +445,8 @@ class TestLoad(TestCase):
|
|||||||
|
|
||||||
distro_mock.load_instrumentor.assert_has_calls(
|
distro_mock.load_instrumentor.assert_has_calls(
|
||||||
[
|
[
|
||||||
call(ep_mock1, raise_exception_on_conflict=True),
|
call(ep_mock1, skip_dep_check=True),
|
||||||
call(ep_mock2, raise_exception_on_conflict=True),
|
call(ep_mock2, skip_dep_check=True),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.assertEqual(distro_mock.load_instrumentor.call_count, 2)
|
self.assertEqual(distro_mock.load_instrumentor.call_count, 2)
|
||||||
@ -434,3 +464,31 @@ class TestLoad(TestCase):
|
|||||||
_load._load_instrumentors(distro_mock)
|
_load._load_instrumentors(distro_mock)
|
||||||
# this has no specific assert because it is run for every instrumentation
|
# this has no specific assert because it is run for every instrumentation
|
||||||
self.assertTrue(distro_mock)
|
self.assertTrue(distro_mock)
|
||||||
|
|
||||||
|
def test_entry_point_dist_finder(self):
|
||||||
|
entry_point_finder = _load._EntryPointDistFinder()
|
||||||
|
self.assertTrue(entry_point_finder._mapping)
|
||||||
|
entry_point = list(
|
||||||
|
entry_points(group="opentelemetry_environment_variables")
|
||||||
|
)[0]
|
||||||
|
self.assertTrue(entry_point)
|
||||||
|
self.assertTrue(entry_point.dist)
|
||||||
|
|
||||||
|
# this will not hit cache
|
||||||
|
entry_point_dist = entry_point_finder.dist_for(entry_point)
|
||||||
|
self.assertTrue(entry_point_dist)
|
||||||
|
# dist are not comparable so we are sure we are not hitting the cache
|
||||||
|
self.assertEqual(entry_point.dist, entry_point_dist)
|
||||||
|
|
||||||
|
# this will hit cache
|
||||||
|
entry_point_without_dist = EntryPoint(
|
||||||
|
name=entry_point.name,
|
||||||
|
group=entry_point.group,
|
||||||
|
value=entry_point.value,
|
||||||
|
)
|
||||||
|
self.assertIsNone(entry_point_without_dist.dist)
|
||||||
|
new_entry_point_dist = entry_point_finder.dist_for(
|
||||||
|
entry_point_without_dist
|
||||||
|
)
|
||||||
|
# dist are not comparable, being truthy is enough
|
||||||
|
self.assertTrue(new_entry_point_dist)
|
||||||
|
@ -14,14 +14,21 @@
|
|||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.requirements import Requirement
|
from packaging.requirements import Requirement
|
||||||
|
|
||||||
from opentelemetry.instrumentation.dependencies import (
|
from opentelemetry.instrumentation.dependencies import (
|
||||||
DependencyConflict,
|
DependencyConflict,
|
||||||
get_dependency_conflicts,
|
get_dependency_conflicts,
|
||||||
|
get_dist_dependency_conflicts,
|
||||||
)
|
)
|
||||||
from opentelemetry.test.test_base import TestBase
|
from opentelemetry.test.test_base import TestBase
|
||||||
|
from opentelemetry.util._importlib_metadata import (
|
||||||
|
Distribution,
|
||||||
|
PackageNotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDependencyConflicts(TestBase):
|
class TestDependencyConflicts(TestBase):
|
||||||
@ -62,3 +69,211 @@ class TestDependencyConflicts(TestBase):
|
|||||||
str(conflict),
|
str(conflict),
|
||||||
f'DependencyConflict: requested: "pytest == 5000" but found: "pytest {pytest.__version__}"',
|
f'DependencyConflict: requested: "pytest == 5000" but found: "pytest {pytest.__version__}"',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_dist_dependency_conflicts(self):
|
||||||
|
class MockDistribution(Distribution):
|
||||||
|
def locate_file(self, path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_text(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires(self):
|
||||||
|
return ['test-pkg ~= 1.0; extra == "instruments"']
|
||||||
|
|
||||||
|
dist = MockDistribution()
|
||||||
|
|
||||||
|
conflict = get_dist_dependency_conflicts(dist)
|
||||||
|
self.assertTrue(conflict is not None)
|
||||||
|
self.assertTrue(isinstance(conflict, DependencyConflict))
|
||||||
|
self.assertEqual(
|
||||||
|
str(conflict),
|
||||||
|
'DependencyConflict: requested: "test-pkg~=1.0; extra == "instruments"" but found: "None"',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_dist_dependency_conflicts_requires_none(self):
|
||||||
|
class MockDistribution(Distribution):
|
||||||
|
def locate_file(self, path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_text(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires(self):
|
||||||
|
# TODO: make another test for returning something with a blank list for both and and or
|
||||||
|
return None
|
||||||
|
|
||||||
|
dist = MockDistribution()
|
||||||
|
conflict = get_dist_dependency_conflicts(dist)
|
||||||
|
self.assertTrue(conflict is None)
|
||||||
|
|
||||||
|
@patch("opentelemetry.instrumentation.dependencies.version")
|
||||||
|
def test_get_dist_dependency_conflicts_any(self, version_mock):
|
||||||
|
class MockDistribution(Distribution):
|
||||||
|
def locate_file(self, path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_text(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires(self):
|
||||||
|
return [
|
||||||
|
'foo ~= 1.0; extra == "instruments-any"',
|
||||||
|
'bar ~= 1.0; extra == "instruments-any"',
|
||||||
|
]
|
||||||
|
|
||||||
|
dist = MockDistribution()
|
||||||
|
|
||||||
|
def version_side_effect(package_name):
|
||||||
|
if package_name == "foo":
|
||||||
|
raise PackageNotFoundError("foo not found")
|
||||||
|
if package_name == "bar":
|
||||||
|
return "1.0.0"
|
||||||
|
raise PackageNotFoundError(f"{package_name} not found")
|
||||||
|
|
||||||
|
version_mock.side_effect = version_side_effect
|
||||||
|
conflict = get_dist_dependency_conflicts(dist)
|
||||||
|
self.assertIsNone(conflict)
|
||||||
|
|
||||||
|
@patch("opentelemetry.instrumentation.dependencies.version")
|
||||||
|
def test_get_dist_dependency_conflicts_neither(self, version_mock):
|
||||||
|
class MockDistribution(Distribution):
|
||||||
|
def locate_file(self, path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_text(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires(self):
|
||||||
|
return [
|
||||||
|
'foo ~= 1.0; extra == "instruments-any"',
|
||||||
|
'bar ~= 1.0; extra == "instruments-any"',
|
||||||
|
]
|
||||||
|
|
||||||
|
dist = MockDistribution()
|
||||||
|
# version_mock.side_effect = lambda x: "1.0.0" if x == "foo" else "2.0.0"
|
||||||
|
# version_mock("foo").return_value = "2.0.0"
|
||||||
|
version_mock.side_effect = PackageNotFoundError("not found")
|
||||||
|
conflict = get_dist_dependency_conflicts(dist)
|
||||||
|
self.assertTrue(conflict is not None)
|
||||||
|
self.assertTrue(isinstance(conflict, DependencyConflict))
|
||||||
|
self.assertEqual(
|
||||||
|
str(conflict),
|
||||||
|
'''DependencyConflict: requested any of the following: "['foo~=1.0; extra == "instruments-any"', 'bar~=1.0; extra == "instruments-any"']" but found: "[]"''',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tests when both "and" and "either" dependencies are specified and both pass.
|
||||||
|
@patch("opentelemetry.instrumentation.dependencies.version")
|
||||||
|
def test_get_dist_dependency_conflicts_any_and(self, version_mock):
|
||||||
|
class MockDistribution(Distribution):
|
||||||
|
def locate_file(self, path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_text(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires(self):
|
||||||
|
# This indicates the instrumentation requires (foo and (bar or baz)))
|
||||||
|
return [
|
||||||
|
'foo ~= 1.0; extra == "instruments"',
|
||||||
|
'bar ~= 2.0; extra == "instruments-any"',
|
||||||
|
'baz ~= 3.0; extra == "instruments-any"',
|
||||||
|
]
|
||||||
|
|
||||||
|
dist = MockDistribution()
|
||||||
|
|
||||||
|
def version_side_effect(package_name):
|
||||||
|
if package_name == "foo":
|
||||||
|
return "1.2.0"
|
||||||
|
if package_name == "bar":
|
||||||
|
raise PackageNotFoundError("bar not found")
|
||||||
|
if package_name == "baz":
|
||||||
|
return "3.7.0"
|
||||||
|
raise PackageNotFoundError(f"{package_name} not found")
|
||||||
|
|
||||||
|
version_mock.side_effect = version_side_effect
|
||||||
|
conflict = get_dist_dependency_conflicts(dist)
|
||||||
|
self.assertIsNone(conflict)
|
||||||
|
|
||||||
|
# Tests when both "and" and "either" dependencies are specified but the "and" dependencies fail to resolve.
|
||||||
|
@patch("opentelemetry.instrumentation.dependencies.version")
|
||||||
|
def test_get_dist_dependency_conflicts_any_and_failed(self, version_mock):
|
||||||
|
class MockDistribution(Distribution):
|
||||||
|
def locate_file(self, path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_text(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires(self):
|
||||||
|
# This indicates the instrumentation requires (foo and (bar or baz)))
|
||||||
|
return [
|
||||||
|
'foo ~= 1.0; extra == "instruments"',
|
||||||
|
'bar ~= 2.0; extra == "instruments-any"',
|
||||||
|
'baz ~= 3.0; extra == "instruments-any"',
|
||||||
|
]
|
||||||
|
|
||||||
|
dist = MockDistribution()
|
||||||
|
|
||||||
|
def version_side_effect(package_name):
|
||||||
|
if package_name == "foo":
|
||||||
|
raise PackageNotFoundError("foo not found")
|
||||||
|
if package_name == "bar":
|
||||||
|
raise PackageNotFoundError("bar not found")
|
||||||
|
if package_name == "baz":
|
||||||
|
return "3.7.0"
|
||||||
|
raise PackageNotFoundError(f"{package_name} not found")
|
||||||
|
|
||||||
|
version_mock.side_effect = version_side_effect
|
||||||
|
conflict = get_dist_dependency_conflicts(dist)
|
||||||
|
self.assertTrue(conflict is not None)
|
||||||
|
self.assertTrue(isinstance(conflict, DependencyConflict))
|
||||||
|
self.assertEqual(
|
||||||
|
str(conflict),
|
||||||
|
'DependencyConflict: requested: "foo~=1.0; extra == "instruments"" but found: "None"',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tests when both "and" and "either" dependencies are specified but the "either" dependencies fail to resolve.
|
||||||
|
@patch("opentelemetry.instrumentation.dependencies.version")
|
||||||
|
def test_get_dist_dependency_conflicts_and_any_failed(self, version_mock):
|
||||||
|
class MockDistribution(Distribution):
|
||||||
|
def locate_file(self, path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_text(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires(self):
|
||||||
|
# This indicates the instrumentation requires (foo and (bar or baz)))
|
||||||
|
return [
|
||||||
|
'foo ~= 1.0; extra == "instruments"',
|
||||||
|
'bar ~= 2.0; extra == "instruments-any"',
|
||||||
|
'baz ~= 3.0; extra == "instruments-any"',
|
||||||
|
]
|
||||||
|
|
||||||
|
dist = MockDistribution()
|
||||||
|
|
||||||
|
def version_side_effect(package_name):
|
||||||
|
if package_name == "foo":
|
||||||
|
return "1.7.0"
|
||||||
|
if package_name == "bar":
|
||||||
|
raise PackageNotFoundError("bar not found")
|
||||||
|
if package_name == "baz":
|
||||||
|
raise PackageNotFoundError("baz not found")
|
||||||
|
raise PackageNotFoundError(f"{package_name} not found")
|
||||||
|
|
||||||
|
version_mock.side_effect = version_side_effect
|
||||||
|
conflict = get_dist_dependency_conflicts(dist)
|
||||||
|
self.assertTrue(conflict is not None)
|
||||||
|
self.assertTrue(isinstance(conflict, DependencyConflict))
|
||||||
|
self.assertEqual(
|
||||||
|
str(conflict),
|
||||||
|
'''DependencyConflict: requested any of the following: "['bar~=2.0; extra == "instruments-any"', 'baz~=3.0; extra == "instruments-any"']" but found: "[]"''',
|
||||||
|
)
|
||||||
|
@ -37,12 +37,14 @@ dependencies = [
|
|||||||
"opentelemetry-instrumentation-httpx[instruments]",
|
"opentelemetry-instrumentation-httpx[instruments]",
|
||||||
"opentelemetry-instrumentation-jinja2[instruments]",
|
"opentelemetry-instrumentation-jinja2[instruments]",
|
||||||
"opentelemetry-instrumentation-kafka-python[instruments]",
|
"opentelemetry-instrumentation-kafka-python[instruments]",
|
||||||
|
"opentelemetry-instrumentation-kafka-python[instruments-any]",
|
||||||
"opentelemetry-instrumentation-logging",
|
"opentelemetry-instrumentation-logging",
|
||||||
"opentelemetry-instrumentation-mysql[instruments]",
|
"opentelemetry-instrumentation-mysql[instruments]",
|
||||||
"opentelemetry-instrumentation-mysqlclient[instruments]",
|
"opentelemetry-instrumentation-mysqlclient[instruments]",
|
||||||
"opentelemetry-instrumentation-pika[instruments]",
|
"opentelemetry-instrumentation-pika[instruments]",
|
||||||
"opentelemetry-instrumentation-psycopg[instruments]",
|
"opentelemetry-instrumentation-psycopg[instruments]",
|
||||||
"opentelemetry-instrumentation-psycopg2[instruments]",
|
"opentelemetry-instrumentation-psycopg2[instruments]",
|
||||||
|
"opentelemetry-instrumentation-psycopg2[instruments-any]",
|
||||||
"opentelemetry-instrumentation-pymemcache[instruments]",
|
"opentelemetry-instrumentation-pymemcache[instruments]",
|
||||||
"opentelemetry-instrumentation-pymongo[instruments]",
|
"opentelemetry-instrumentation-pymongo[instruments]",
|
||||||
"opentelemetry-instrumentation-pymysql[instruments]",
|
"opentelemetry-instrumentation-pymysql[instruments]",
|
||||||
|
@ -84,7 +84,7 @@ def main():
|
|||||||
pkg_name = pkg.get("name")
|
pkg_name = pkg.get("name")
|
||||||
if pkg_name in packages_to_exclude:
|
if pkg_name in packages_to_exclude:
|
||||||
continue
|
continue
|
||||||
if not pkg["instruments"]:
|
if not pkg["instruments"] and not pkg["instruments-any"]:
|
||||||
default_instrumentations.elts.append(ast.Str(pkg["requirement"]))
|
default_instrumentations.elts.append(ast.Str(pkg["requirement"]))
|
||||||
for target_pkg in pkg["instruments"]:
|
for target_pkg in pkg["instruments"]:
|
||||||
libraries.elts.append(
|
libraries.elts.append(
|
||||||
@ -93,6 +93,14 @@ def main():
|
|||||||
values=[ast.Str(target_pkg), ast.Str(pkg["requirement"])],
|
values=[ast.Str(target_pkg), ast.Str(pkg["requirement"])],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# instruments-any is an optional field that can be used instead of or in addition to _instruments. While _instruments is a list of dependencies, all of which are expected by the instrumentation, instruments-any is a list any of which but not all are expected.
|
||||||
|
for target_pkg in pkg["instruments-any"]:
|
||||||
|
libraries.elts.append(
|
||||||
|
ast.Dict(
|
||||||
|
keys=[ast.Str("library"), ast.Str("instrumentation")],
|
||||||
|
values=[ast.Str(target_pkg), ast.Str(pkg["requirement"])],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
tree = ast.parse(_source_tmpl)
|
tree = ast.parse(_source_tmpl)
|
||||||
tree.body[0].value = libraries
|
tree.body[0].value = libraries
|
||||||
|
@ -58,11 +58,16 @@ def main(base_instrumentation_path):
|
|||||||
with open(version_filename, encoding="utf-8") as fh:
|
with open(version_filename, encoding="utf-8") as fh:
|
||||||
exec(fh.read(), pkg_info)
|
exec(fh.read(), pkg_info)
|
||||||
|
|
||||||
instruments = pkg_info["_instruments"]
|
instruments_and = pkg_info.get("_instruments", ())
|
||||||
|
# _instruments_any is an optional field that can be used instead of or in addition to _instruments. While _instruments is a list of dependencies, all of which are expected by the instrumentation, _instruments_any is a list any of which but not all are expected.
|
||||||
|
instruments_any = pkg_info.get("_instruments_any", ())
|
||||||
supports_metrics = pkg_info.get("_supports_metrics")
|
supports_metrics = pkg_info.get("_supports_metrics")
|
||||||
semconv_status = pkg_info.get("_semconv_status")
|
semconv_status = pkg_info.get("_semconv_status")
|
||||||
if not instruments:
|
instruments_all = ()
|
||||||
instruments = (name,)
|
if not instruments_and and not instruments_any:
|
||||||
|
instruments_all = (name,)
|
||||||
|
else:
|
||||||
|
instruments_all = tuple(instruments_and + instruments_any)
|
||||||
|
|
||||||
if not semconv_status:
|
if not semconv_status:
|
||||||
semconv_status = "development"
|
semconv_status = "development"
|
||||||
@ -70,7 +75,7 @@ def main(base_instrumentation_path):
|
|||||||
metric_column = "Yes" if supports_metrics else "No"
|
metric_column = "Yes" if supports_metrics else "No"
|
||||||
|
|
||||||
table.append(
|
table.append(
|
||||||
f"| [{instrumentation}](./{instrumentation}) | {','.join(instruments)} | {metric_column} | {semconv_status}"
|
f"| [{instrumentation}](./{instrumentation}) | {','.join(instruments_all)} | {metric_column} | {semconv_status}"
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(
|
with open(
|
||||||
|
@ -60,12 +60,17 @@ def get_instrumentation_packages(
|
|||||||
with open(pyproject_toml_path, "rb") as file:
|
with open(pyproject_toml_path, "rb") as file:
|
||||||
pyproject_toml = tomli.load(file)
|
pyproject_toml = tomli.load(file)
|
||||||
|
|
||||||
|
optional_dependencies = pyproject_toml["project"][
|
||||||
|
"optional-dependencies"
|
||||||
|
]
|
||||||
|
instruments = optional_dependencies.get("instruments", [])
|
||||||
|
# instruments-any is an optional field that can be used instead of or in addition to instruments. While instruments is a list of dependencies, all of which are expected by the instrumentation, instruments-any is a list any of which but not all are expected.
|
||||||
|
instruments_any = optional_dependencies.get("instruments-any", [])
|
||||||
instrumentation = {
|
instrumentation = {
|
||||||
"name": pyproject_toml["project"]["name"],
|
"name": pyproject_toml["project"]["name"],
|
||||||
"version": version.strip(),
|
"version": version.strip(),
|
||||||
"instruments": pyproject_toml["project"]["optional-dependencies"][
|
"instruments": instruments,
|
||||||
"instruments"
|
"instruments-any": instruments_any,
|
||||||
],
|
|
||||||
}
|
}
|
||||||
if instrumentation["name"] in independent_packages:
|
if instrumentation["name"] in independent_packages:
|
||||||
specifier = independent_packages[instrumentation["name"]]
|
specifier = independent_packages[instrumentation["name"]]
|
||||||
|
22
uv.lock
generated
22
uv.lock
generated
@ -2872,20 +2872,20 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
instruments = [
|
instruments-any = [
|
||||||
{ name = "kafka-python" },
|
{ name = "kafka-python" },
|
||||||
{ name = "kafka-python-ng" },
|
{ name = "kafka-python-ng" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "kafka-python", marker = "extra == 'instruments'", specifier = ">=2.0,<3.0" },
|
{ name = "kafka-python", marker = "extra == 'instruments-any'", specifier = ">=2.0,<3.0" },
|
||||||
{ name = "kafka-python-ng", marker = "extra == 'instruments'", specifier = ">=2.0,<3.0" },
|
{ name = "kafka-python-ng", marker = "extra == 'instruments-any'", specifier = ">=2.0,<3.0" },
|
||||||
{ name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" },
|
{ name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" },
|
||||||
{ name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" },
|
{ name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" },
|
||||||
{ name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" },
|
{ name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" },
|
||||||
]
|
]
|
||||||
provides-extras = ["instruments"]
|
provides-extras = ["instruments", "instruments-any"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-instrumentation-logging"
|
name = "opentelemetry-instrumentation-logging"
|
||||||
@ -3029,7 +3029,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
instruments = [
|
instruments-any = [
|
||||||
{ name = "psycopg2" },
|
{ name = "psycopg2" },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
]
|
]
|
||||||
@ -3039,10 +3039,10 @@ requires-dist = [
|
|||||||
{ name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" },
|
{ name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" },
|
||||||
{ name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" },
|
{ name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" },
|
||||||
{ name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" },
|
{ name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" },
|
||||||
{ name = "psycopg2", marker = "extra == 'instruments'", specifier = ">=2.7.3.1" },
|
{ name = "psycopg2", marker = "extra == 'instruments-any'", specifier = ">=2.7.3.1" },
|
||||||
{ name = "psycopg2-binary", marker = "extra == 'instruments'", specifier = ">=2.7.3.1" },
|
{ name = "psycopg2-binary", marker = "extra == 'instruments-any'", specifier = ">=2.7.3.1" },
|
||||||
]
|
]
|
||||||
provides-extras = ["instruments"]
|
provides-extras = ["instruments", "instruments-any"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-instrumentation-pymemcache"
|
name = "opentelemetry-instrumentation-pymemcache"
|
||||||
@ -3548,14 +3548,14 @@ dependencies = [
|
|||||||
{ name = "opentelemetry-instrumentation-grpc", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-grpc", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-httpx", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-httpx", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-jinja2", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-jinja2", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-kafka-python", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-kafka-python", extra = ["instruments-any"] },
|
||||||
{ name = "opentelemetry-instrumentation-logging" },
|
{ name = "opentelemetry-instrumentation-logging" },
|
||||||
{ name = "opentelemetry-instrumentation-mysql", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-mysql", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-mysqlclient", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-mysqlclient", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-openai-v2", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-openai-v2", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-pika", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-pika", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-psycopg", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-psycopg", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-psycopg2", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-psycopg2", extra = ["instruments-any"] },
|
||||||
{ name = "opentelemetry-instrumentation-pymemcache", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-pymemcache", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-pymongo", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-pymongo", extra = ["instruments"] },
|
||||||
{ name = "opentelemetry-instrumentation-pymysql", extra = ["instruments"] },
|
{ name = "opentelemetry-instrumentation-pymysql", extra = ["instruments"] },
|
||||||
@ -3613,6 +3613,7 @@ requires-dist = [
|
|||||||
{ name = "opentelemetry-instrumentation-httpx", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-httpx" },
|
{ name = "opentelemetry-instrumentation-httpx", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-httpx" },
|
||||||
{ name = "opentelemetry-instrumentation-jinja2", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-jinja2" },
|
{ name = "opentelemetry-instrumentation-jinja2", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-jinja2" },
|
||||||
{ name = "opentelemetry-instrumentation-kafka-python", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-kafka-python" },
|
{ name = "opentelemetry-instrumentation-kafka-python", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-kafka-python" },
|
||||||
|
{ name = "opentelemetry-instrumentation-kafka-python", extras = ["instruments-any"], editable = "instrumentation/opentelemetry-instrumentation-kafka-python" },
|
||||||
{ name = "opentelemetry-instrumentation-logging", editable = "instrumentation/opentelemetry-instrumentation-logging" },
|
{ name = "opentelemetry-instrumentation-logging", editable = "instrumentation/opentelemetry-instrumentation-logging" },
|
||||||
{ name = "opentelemetry-instrumentation-mysql", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-mysql" },
|
{ name = "opentelemetry-instrumentation-mysql", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-mysql" },
|
||||||
{ name = "opentelemetry-instrumentation-mysqlclient", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-mysqlclient" },
|
{ name = "opentelemetry-instrumentation-mysqlclient", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-mysqlclient" },
|
||||||
@ -3620,6 +3621,7 @@ requires-dist = [
|
|||||||
{ name = "opentelemetry-instrumentation-pika", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pika" },
|
{ name = "opentelemetry-instrumentation-pika", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pika" },
|
||||||
{ name = "opentelemetry-instrumentation-psycopg", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-psycopg" },
|
{ name = "opentelemetry-instrumentation-psycopg", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-psycopg" },
|
||||||
{ name = "opentelemetry-instrumentation-psycopg2", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-psycopg2" },
|
{ name = "opentelemetry-instrumentation-psycopg2", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-psycopg2" },
|
||||||
|
{ name = "opentelemetry-instrumentation-psycopg2", extras = ["instruments-any"], editable = "instrumentation/opentelemetry-instrumentation-psycopg2" },
|
||||||
{ name = "opentelemetry-instrumentation-pymemcache", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymemcache" },
|
{ name = "opentelemetry-instrumentation-pymemcache", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymemcache" },
|
||||||
{ name = "opentelemetry-instrumentation-pymongo", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymongo" },
|
{ name = "opentelemetry-instrumentation-pymongo", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymongo" },
|
||||||
{ name = "opentelemetry-instrumentation-pymysql", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymysql" },
|
{ name = "opentelemetry-instrumentation-pymysql", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymysql" },
|
||||||
|
Reference in New Issue
Block a user