Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
### Features Added
- - Update OpenTelemetry dependencies to latest versions, bump `langchain-core` minimum version to address S360, and support the new `httpx2` entry point exposed by `opentelemetry-instrumentation-httpx`.
([#254](https://github.com/microsoft/opentelemetry-distro-python/pull/254))
- Add independent dependency checks for the `httpx` and `httpx2` instrumentation entry points.
Comment thread
rads-1996 marked this conversation as resolved.
([#259](https://github.com/microsoft/opentelemetry-distro-python/pull/259))

# 1.3.8 (2026-08-20)
### Features Added
Expand Down
14 changes: 12 additions & 2 deletions src/microsoft/opentelemetry/_distro.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
GenAIMainAgentLogRecordProcessor,
GenAIMainAgentSpanProcessor,
)
from microsoft.opentelemetry._instrumentation import get_dist_dependency_conflicts
from microsoft.opentelemetry._instrumentation import get_dist_dependency_conflicts, get_dependency_conflicts
from microsoft.opentelemetry._otlp import is_otlp_enabled
from microsoft.opentelemetry._sdkstats._state import (
SdkStatsFeature,
Expand Down Expand Up @@ -840,7 +840,17 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None:
if lib_name in ["agent_framework", "langchain"]:
merged_kwargs[ENABLE_SENSITIVE_DATA_ARG] = enable_sensitive_data
instrumentor: Any = entry_point.load()
instrumentor().instrument(skip_dep_check=True, **merged_kwargs)
instrumentor_instance = instrumentor()
if lib_name in ("httpx", "httpx2"):
conflict = get_dependency_conflicts(instrumentor_instance.instrumentation_dependencies())
if conflict:
_logger.debug(
"Skipping instrumentation %s: %s",
entry_point.name,
conflict,
)
continue
instrumentor_instance.instrument(skip_dep_check=True, **merged_kwargs)
set_sdkstats_instrumentation_by_name(lib_name)
except Exception as ex: # pylint: disable=broad-except
_logger.warning(
Expand Down
95 changes: 92 additions & 3 deletions tests/test_instrumentation_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,9 +478,7 @@ def test_httpx2_can_be_disabled_independently(self):
return_value=[httpx_entry_point, httpx2_entry_point],
),
):
_setup_instrumentations(
{"instrumentation_options": {"httpx2": {"enabled": False}}}
)
_setup_instrumentations({"instrumentation_options": {"httpx2": {"enabled": False}}})

httpx_instrumentor.instrument.assert_called_once()
httpx2_instrumentor.instrument.assert_not_called()
Expand Down Expand Up @@ -512,6 +510,97 @@ def test_httpx2_failure_does_not_affect_httpx(self, set_sdkstats):
httpx2_instrumentor.instrument.assert_called_once()
set_sdkstats.assert_called_once_with("httpx")

@patch("microsoft.opentelemetry._distro.set_sdkstats_instrumentation_by_name")
def test_httpx_dependency_conflict_skips_only_affected_entry_point(self, set_sdkstats):
for conflicting_lib in ("httpx", "httpx2"):
with self.subTest(conflicting_lib=conflicting_lib):
set_sdkstats.reset_mock()
instrumentors = {
"httpx": MagicMock(name="httpx_instrumentor"),
"httpx2": MagicMock(name="httpx2_instrumentor"),
}
entry_points_by_name = []
for lib_name, instrumentor in instrumentors.items():
instrumentor.instrumentation_dependencies.return_value = [f"{lib_name}>=1"]
entry_point = MagicMock(name=f"{lib_name}_entry_point")
entry_point.name = lib_name
entry_point.load.return_value = lambda instrumentor=instrumentor: instrumentor
entry_points_by_name.append(entry_point)

dependency_conflicts = [
object() if conflicting_lib == "httpx" else None,
object() if conflicting_lib == "httpx2" else None,
]

with (
patch("microsoft.opentelemetry._distro.get_dist_dependency_conflicts", return_value=None),
patch(
"microsoft.opentelemetry._distro.get_dependency_conflicts",
side_effect=dependency_conflicts,
),
patch(
"microsoft.opentelemetry._distro.entry_points",
return_value=entry_points_by_name,
),
):
_setup_instrumentations({})

sibling_lib = "httpx2" if conflicting_lib == "httpx" else "httpx"
instrumentors[conflicting_lib].instrument.assert_not_called()
instrumentors[sibling_lib].instrument.assert_called_once_with(skip_dep_check=True)
set_sdkstats.assert_called_once_with(sibling_lib)

@patch("microsoft.opentelemetry._distro.get_dependency_conflicts", return_value=None)
def test_httpx_entry_points_instrument_when_dependencies_are_available(self, get_conflicts):
httpx_instrumentor = MagicMock()
httpx_instrumentor.instrumentation_dependencies.return_value = ["httpx>=1"]
httpx2_instrumentor = MagicMock()
httpx2_instrumentor.instrumentation_dependencies.return_value = ["httpx-ws>=1"]

httpx_entry_point = MagicMock(name="httpx_entry_point")
httpx_entry_point.name = "httpx"
httpx_entry_point.load.return_value = lambda: httpx_instrumentor

httpx2_entry_point = MagicMock(name="httpx2_entry_point")
httpx2_entry_point.name = "httpx2"
httpx2_entry_point.load.return_value = lambda: httpx2_instrumentor

with (
patch("microsoft.opentelemetry._distro.get_dist_dependency_conflicts", return_value=None),
patch(
"microsoft.opentelemetry._distro.entry_points",
return_value=[httpx_entry_point, httpx2_entry_point],
),
):
_setup_instrumentations({})

self.assertEqual(
get_conflicts.call_args_list,
[unittest.mock.call(["httpx>=1"]), unittest.mock.call(["httpx-ws>=1"])],
)
httpx_instrumentor.instrument.assert_called_once_with(skip_dep_check=True)
httpx2_instrumentor.instrument.assert_called_once_with(skip_dep_check=True)

@patch("microsoft.opentelemetry._distro.get_dependency_conflicts")
def test_non_httpx_entry_point_does_not_run_instrumentor_dependency_check(self, get_conflicts):
requests_instrumentor = MagicMock()
requests_entry_point = MagicMock(name="requests_entry_point")
requests_entry_point.name = "requests"
requests_entry_point.load.return_value = lambda: requests_instrumentor

with (
patch("microsoft.opentelemetry._distro.get_dist_dependency_conflicts", return_value=None),
patch(
"microsoft.opentelemetry._distro.entry_points",
return_value=[requests_entry_point],
),
):
_setup_instrumentations({})

requests_instrumentor.instrumentation_dependencies.assert_not_called()
get_conflicts.assert_not_called()
requests_instrumentor.instrument.assert_called_once_with(skip_dep_check=True)

def test_kwargs_not_forwarded_to_disabled_lib(self):
"""A disabled library should not have instrument() called at all."""
instrumentor_instance = MagicMock()
Expand Down