Skip to content

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

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bf72030
Utilize instruments_either in fastapi, kafka, and psycopg2
jeremydvoss Jul 3, 2025
8e2ece4
changelog entries
jeremydvoss Jul 3, 2025
a9bd19c
Add instruments_either to bootstrap
jeremydvoss Jul 7, 2025
efd2e33
generate bootstrap code
jeremydvoss Jul 7, 2025
d38dd3c
generate readme fix
jeremydvoss Jul 7, 2025
4048c8e
instruments-any and _instruments_any
jeremydvoss Jul 9, 2025
19390ed
sync
jeremydvoss Jul 9, 2025
0890970
uv.lock
jeremydvoss Jul 11, 2025
30e6b48
uv.lock
jeremydvoss Jul 11, 2025
f6a582d
instrumentation changes
jeremydvoss Jul 2, 2025
46fe178
lint
jeremydvoss Jul 2, 2025
a5606f4
test fastapi tests
jeremydvoss Jul 2, 2025
f86b1e2
lint
jeremydvoss Jul 2, 2025
5750679
lint
jeremydvoss Jul 2, 2025
de3f120
psycopg2 tests
jeremydvoss Jul 2, 2025
7063d9f
lint. Still needs to be simplified
jeremydvoss Jul 3, 2025
003a878
lint refactor
jeremydvoss Jul 3, 2025
2704c38
clean _load, sitecustomize, dependencies
jeremydvoss Jul 3, 2025
acffa61
Minimize changes
jeremydvoss Jul 3, 2025
a60c7d8
Minimize changes
jeremydvoss Jul 3, 2025
5edb569
Minimize changes
jeremydvoss Jul 3, 2025
8d5d23c
Minimize test changes
jeremydvoss Jul 3, 2025
6bd5653
Fix fastapi tests
jeremydvoss Jul 3, 2025
61f1113
clean up
jeremydvoss Jul 3, 2025
739be1d
update generative scripts with instruments_either
jeremydvoss Jul 7, 2025
90c07b9
update docs
jeremydvoss Jul 7, 2025
ff69bba
instruments-any
jeremydvoss Jul 9, 2025
2025700
correct pkg fields vs toml fields
jeremydvoss Jul 9, 2025
92268b6
Fix docs
jeremydvoss Jul 9, 2025
c6738f3
Try adding kafka changes
jeremydvoss Jul 9, 2025
b888ae5
Revert "Try adding kafka changes"
jeremydvoss Jul 9, 2025
b51094f
Update opentelemetry-instrumentation/src/opentelemetry/instrumentatio…
jeremydvoss Jul 10, 2025
73eab06
lint
jeremydvoss Jul 10, 2025
de08183
gen
jeremydvoss Jul 11, 2025
90e9a7b
remove upload dates from uv.lock
jeremydvoss Jul 11, 2025
517e4b1
Merge branch 'instruments-either-use' into instruments-either
jeremydvoss Jul 11, 2025
1e3aba6
Fix changelog
jeremydvoss Jul 11, 2025
42803f7
Merge branch 'main' into instruments-either
jeremydvoss Jul 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 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-fastapi` Utilize instruments-any functionality. TODO MOVE TO NEW VERSION WHEN OUT
([#3612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3612))
- `opentelemetry-instrumentation-psycopg2` Utilize instruments-any functionality.
([#3612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3612))
- `opentelemetry-instrumentation-kafka-python` Utilize instruments-any functionality.
([#3612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3612))

## Version 1.35.0/0.56b0 (2025-07-11)

### Added
Expand Down
8 changes: 6 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,14 @@ Below is a checklist of things to be mindful of when implementing a new instrume
### Update supported instrumentation package versions

- 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 `_instruments` variable in instrumentation **`package.py`** file 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` 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

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.

<!-- 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:

- At the **instrumentation package directory:** Add new test-requirements.txt file with the respective package version required for testing
Expand Down
2 changes: 1 addition & 1 deletion instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | development
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | development
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 5.0.0 | Yes | migration
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.92 | Yes | migration
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.92,fastapi-slim ~= 0.92 | Yes | migration
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio >= 1.42.0 | No | development
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | Yes | migration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ dependencies = [
]

[project.optional-dependencies]
instruments = [
instruments = []
instruments-any = [
"fastapi ~= 0.92",
"fastapi-slim ~= 0.92",
]

[project.entry-points.opentelemetry_instrumentor]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
# limitations under the License.


_instruments = ("fastapi ~= 0.92",)
# TODO: update this
_instruments = ()
_instruments_any = ("fastapi ~= 0.92", "fastapi-slim ~= 0.92")

_supports_metrics = True

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
)
from opentelemetry.instrumentation.dependencies import (
DependencyConflict,
DependencyConflictError,
)
from opentelemetry.sdk.metrics.export import (
HistogramDataPoint,
Expand Down Expand Up @@ -1102,40 +1101,34 @@ def test_instruments_with_fastapi_installed(self, mock_logger):
[self._instrumentation_loaded_successfully_call()]
)

@patch(
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
)
@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")
mock_distro = Mock()
mock_distro.load_instrumentor.side_effect = DependencyConflictError(
dependency_conflict
)
mock_dep.return_value = dependency_conflict
_load_instrumentors(mock_distro)
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
(ep,) = mock_distro.load_instrumentor.call_args.args
self.assertEqual(ep.name, "fastapi")
assert (
self._instrumentation_loaded_successfully_call()
not in mock_logger.debug.call_args_list
)
mock_distro.load_instrumentor.assert_not_called()
mock_logger.debug.assert_has_calls(
[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")
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)
mock_distro = Mock()
mock_distro.load_instrumentor.side_effect = DependencyConflictError(
dependency_conflict
)
mock_dep.return_value = dependency_conflict
_load_instrumentors(mock_distro)
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
(ep,) = mock_distro.load_instrumentor.call_args.args
self.assertEqual(ep.name, "fastapi")
assert (
self._instrumentation_loaded_successfully_call()
not in mock_logger.debug.call_args_list
)
mock_distro.load_instrumentor.assert_not_called()
mock_logger.debug.assert_has_calls(
[self._instrumentation_failed_to_load_call(dependency_conflict)]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ dependencies = [
]

[project.optional-dependencies]
instruments = [
instruments = []
instruments-any = [
"kafka-python >= 2.0, < 3.0",
"kafka-python-ng >= 2.0, < 3.0"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
# limitations under the License.


# TODO: where are these used?
_instruments_kafka_python = "kafka-python >= 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)
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ dependencies = [
]

[project.optional-dependencies]
instruments = [
instruments = []
instruments-any = [
"psycopg2 >= 2.7.3.1",
"psycopg2-binary >= 2.7.3.1",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
# limitations under the License.


# TODO: where are these used?
_instruments_psycopg2 = "psycopg2 >= 2.7.3.1"
_instruments_psycopg2_binary = "psycopg2-binary >= 2.7.3.1"

_instruments = (
# TODO: maybe add _instruments_any
_instruments = ()
_instruments_any = (
_instruments_psycopg2,
_instruments_psycopg2_binary,
)
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,16 @@ def _distribution(name):
# Note there is only one test here but it is run twice in tox
# once with the psycopg2 package installed and once with
# psycopg2-binary installed.
@patch(
"opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts"
)
@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():
return call("Instrumented %s", "psycopg2")

mock_distro = Mock()
mock_dep.return_value = None
mock_distro.load_instrumentor.return_value = None
_load_instrumentors(mock_distro)
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,51 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from functools import cached_property
from logging import getLogger
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.environment_variables import (
OTEL_PYTHON_CONFIGURATOR,
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
OTEL_PYTHON_DISTRO,
)
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__)


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:
distro_name = environ.get(OTEL_PYTHON_DISTRO, None)
for entry_point in entry_points(group="opentelemetry_distro"):
Expand Down Expand Up @@ -55,6 +84,7 @@ def _load_distro() -> BaseDistro:

def _load_instrumentors(distro):
package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, [])
entry_point_finder = _EntryPointDistFinder()
if isinstance(package_to_exclude, str):
package_to_exclude = package_to_exclude.split(",")
# to handle users entering "requests , flask" or "requests, flask" with spaces
Expand All @@ -71,11 +101,24 @@ def _load_instrumentors(distro):
continue

try:
distro.load_instrumentor(
entry_point, raise_exception_on_conflict=True
)
entry_point_dist = entry_point_finder.dist_for(entry_point)
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)
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(
"Skipping instrumentation %s: %s",
entry_point.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@
"library": "fastapi ~= 0.92",
"instrumentation": "opentelemetry-instrumentation-fastapi==0.57b0.dev",
},
{
"library": "fastapi-slim ~= 0.92",
"instrumentation": "opentelemetry-instrumentation-fastapi==0.57b0.dev",
},
{
"library": "flask >= 1.0",
"instrumentation": "opentelemetry-instrumentation-flask==0.57b0.dev",
Expand Down
Loading