Add "instruments-any" feature: unblock multi-target instrumentations while fixing dependency conflict breakage. (#3610)

This commit is contained in:
Jeremy Voss
2025-07-21 09:24:50 -07:00
committed by GitHub
parent f20fa77ad5
commit 77f3171bd4
20 changed files with 574 additions and 90 deletions

View File

@ -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

View File

@ -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

View File

@ -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)]
) )

View File

@ -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"
] ]

View File

@ -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

View File

@ -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)

View File

@ -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),
)

View File

@ -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",
] ]

View File

@ -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.

View File

@ -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,
) )

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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: "[]"''',
)

View File

@ -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]",

View File

@ -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

View File

@ -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(

View File

@ -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
View File

@ -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" },