From 3b64ce15079ba0d65ebb63b093cda0b9bcf5b503 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 1 Sep 2026 11:11:11 -0700 Subject: [PATCH 1/5] Update otel dependencies and add support for httpx2 entry point --- pyproject.toml | 30 +++++++++++++------------- src/microsoft/opentelemetry/_distro.py | 12 +++++++++++ tests/test_httpx_instrumentation.py | 6 ++++++ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9d0ac77..c97faf55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,22 +24,22 @@ dependencies = [ "azure-core<2.0.0,>=1.28.0", "azure-core-tracing-opentelemetry~=1.0.0b11", "azure-monitor-opentelemetry-exporter~=1.0.0b56", - "opentelemetry-sdk~=1.43.0", - "opentelemetry-api~=1.43.0", - "opentelemetry-exporter-otlp-proto-http~=1.43.0", - "opentelemetry-instrumentation>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-django>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-fastapi>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-flask>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-httpx>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-psycopg2>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-requests>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-urllib>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-urllib3>=0.64b0,<0.65b0", - "opentelemetry-instrumentation-logging>=0.64b0,<0.65b0", + "opentelemetry-sdk~=1.44.0", + "opentelemetry-api~=1.44.0", + "opentelemetry-exporter-otlp-proto-http~=1.44.0", + "opentelemetry-instrumentation>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-django>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-fastapi>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-flask>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-httpx>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-psycopg2>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-requests>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-urllib>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-urllib3>=0.65b0,<0.66b0", + "opentelemetry-instrumentation-logging>=0.65b0,<0.66b0", "opentelemetry-instrumentation-openai-agents-v2==0.1.0", "opentelemetry-instrumentation-openai-v2==2.3b0", - "opentelemetry-resource-detector-azure<1.0.0,>=0.1.5", + "opentelemetry-resource-detector-azure<1.0.0,>=0.2.0", "wrapt>=1.0.0,<2.0.0", "opentelemetry-util-genai==0.3b0", "aiohttp>=3.8.0", @@ -49,7 +49,7 @@ dependencies = [ [project.optional-dependencies] langchain = [ - "langchain-core>=0.2.0", + "langchain-core>=1.6.0", ] agent-framework = [ "agent-framework>=1.4.0", diff --git a/src/microsoft/opentelemetry/_distro.py b/src/microsoft/opentelemetry/_distro.py index f71a30c4..73eccbde 100644 --- a/src/microsoft/opentelemetry/_distro.py +++ b/src/microsoft/opentelemetry/_distro.py @@ -841,6 +841,8 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None: merged_kwargs[ENABLE_SENSITIVE_DATA_ARG] = enable_sensitive_data instrumentor: Any = entry_point.load() instrumentor().instrument(skip_dep_check=True, **merged_kwargs) + if lib_name == "httpx": + _setup_httpx2_instrumentation(merged_kwargs) set_sdkstats_instrumentation_by_name(lib_name) except Exception as ex: # pylint: disable=broad-except _logger.warning( @@ -850,6 +852,16 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None: ) +def _setup_httpx2_instrumentation(instrumentation_kwargs: Dict[str, Any]) -> None: + """Enable HTTPX2 when supported by the installed HTTPX instrumentor.""" + try: + import httpx2 # noqa: F401 + from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor + except ImportError: + return + HTTPX2ClientInstrumentor().instrument(skip_dep_check=True, **instrumentation_kwargs) + + def _setup_a365_openai_agents_instrumentation() -> None: """Register the A365 OpenAI Agents trace processor.""" try: diff --git a/tests/test_httpx_instrumentation.py b/tests/test_httpx_instrumentation.py index 0adf795a..b607274f 100644 --- a/tests/test_httpx_instrumentation.py +++ b/tests/test_httpx_instrumentation.py @@ -14,6 +14,7 @@ import threading import unittest from http.server import HTTPServer, BaseHTTPRequestHandler +from unittest.mock import patch import pytest @@ -30,6 +31,7 @@ _A365_DISABLED_INSTRUMENTATIONS, _SUPPORTED_INSTRUMENTED_LIBRARIES, ) +from microsoft.opentelemetry._distro import _setup_httpx2_instrumentation # noqa: E402 # pylint: enable=wrong-import-position @@ -55,6 +57,10 @@ def test_httpx_in_supported_libraries(self): def test_httpx_in_a365_disabled_list(self): self.assertIn("httpx", _A365_DISABLED_INSTRUMENTATIONS) + def test_httpx2_is_ignored_when_not_available(self): + with patch("builtins.__import__", side_effect=ImportError): + _setup_httpx2_instrumentation({}) + class TestHttpxInstrumentorLifecycle(unittest.TestCase): """Verify the upstream HTTPXClientInstrumentor can be activated and torn down.""" From 0a97f1bc9179a2568cc96eb9594d5e0d80fe3e20 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 1 Sep 2026 12:59:38 -0700 Subject: [PATCH 2/5] Bump min exporter version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c97faf55..5918c02c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ dependencies = [ "azure-core<2.0.0,>=1.28.0", "azure-core-tracing-opentelemetry~=1.0.0b11", - "azure-monitor-opentelemetry-exporter~=1.0.0b56", + "azure-monitor-opentelemetry-exporter~=1.0.0b57", "opentelemetry-sdk~=1.44.0", "opentelemetry-api~=1.44.0", "opentelemetry-exporter-otlp-proto-http~=1.44.0", From dbb574f08170edb31c092dc5605c3081fbf032c3 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 3 Sep 2026 10:24:31 -0700 Subject: [PATCH 3/5] Fix lint and add CHANGELOG --- CHANGELOG.md | 5 +++++ src/microsoft/opentelemetry/_distro.py | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 738fdc15..8aa623f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Release History +# 1.3.9 (Unreleased) +### Features Added +- Update opentelemetry dependencies to latest, update the langchain core minimum version to address S360 item and add a httpx2 entry point which + `opentelemetry-instrumentation-httpx2` now supports + ([#254](https://github.com/microsoft/opentelemetry-distro-python/pull/254)) # 1.3.8 (2026-08-20) ### Features Added diff --git a/src/microsoft/opentelemetry/_distro.py b/src/microsoft/opentelemetry/_distro.py index 73eccbde..f15ae08d 100644 --- a/src/microsoft/opentelemetry/_distro.py +++ b/src/microsoft/opentelemetry/_distro.py @@ -855,7 +855,6 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None: def _setup_httpx2_instrumentation(instrumentation_kwargs: Dict[str, Any]) -> None: """Enable HTTPX2 when supported by the installed HTTPX instrumentor.""" try: - import httpx2 # noqa: F401 from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor except ImportError: return From a8a4789e8f5552c30b32cd348298fbebd31846a2 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 3 Sep 2026 13:38:41 -0700 Subject: [PATCH 4/5] Include coverage for httpx2 --- A365_DOCUMENTATION.md | 1 + CHANGELOG.md | 3 +- README.md | 2 + src/microsoft/opentelemetry/_constants.py | 2 + src/microsoft/opentelemetry/_distro.py | 11 ---- .../utils/test_configurations.py | 11 ++++ tests/test_httpx_instrumentation.py | 7 +- tests/test_instrumentation_options.py | 64 +++++++++++++++++++ 8 files changed, 83 insertions(+), 18 deletions(-) diff --git a/A365_DOCUMENTATION.md b/A365_DOCUMENTATION.md index 9e9b3b16..17aa49ac 100644 --- a/A365_DOCUMENTATION.md +++ b/A365_DOCUMENTATION.md @@ -167,6 +167,7 @@ When `enable_a365=True` (and `enable_azure_monitor` is **not** set), the **web-f | `fastapi` | disabled | | `flask` | disabled | | `httpx` | disabled | +| `httpx2` | disabled | | `psycopg2` | disabled | | `requests` | disabled | | `urllib` | disabled | diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa623f7..d43b5ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,7 @@ # Release History # 1.3.9 (Unreleased) ### Features Added -- Update opentelemetry dependencies to latest, update the langchain core minimum version to address S360 item and add a httpx2 entry point which - `opentelemetry-instrumentation-httpx2` now supports +- - 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)) # 1.3.8 (2026-08-20) diff --git a/README.md b/README.md index 883eb5aa..edae89b4 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,7 @@ Microsoft OpenTelemetry automatically instruments the following libraries when i | `fastapi` | Web framework | | `flask` | Web framework | | `httpx` | HTTP client | +| `httpx2` | HTTP client | | `psycopg2` | Database | | `requests` | HTTP client | | `urllib` | HTTP client | @@ -245,6 +246,7 @@ following instrumentations by default** when `enable_a365=True`: | `fastapi` | disabled | | `flask` | disabled | | `httpx` | disabled | +| `httpx2` | disabled | | `psycopg2` | disabled | | `requests` | disabled | | `urllib` | disabled | diff --git a/src/microsoft/opentelemetry/_constants.py b/src/microsoft/opentelemetry/_constants.py index 8d4ee085..7685c418 100644 --- a/src/microsoft/opentelemetry/_constants.py +++ b/src/microsoft/opentelemetry/_constants.py @@ -33,6 +33,7 @@ "fastapi", "flask", "httpx", + "httpx2", "psycopg2", "requests", "urllib", @@ -52,6 +53,7 @@ "fastapi", "flask", "httpx", + "httpx2", "psycopg2", "requests", "urllib", diff --git a/src/microsoft/opentelemetry/_distro.py b/src/microsoft/opentelemetry/_distro.py index f15ae08d..f71a30c4 100644 --- a/src/microsoft/opentelemetry/_distro.py +++ b/src/microsoft/opentelemetry/_distro.py @@ -841,8 +841,6 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None: merged_kwargs[ENABLE_SENSITIVE_DATA_ARG] = enable_sensitive_data instrumentor: Any = entry_point.load() instrumentor().instrument(skip_dep_check=True, **merged_kwargs) - if lib_name == "httpx": - _setup_httpx2_instrumentation(merged_kwargs) set_sdkstats_instrumentation_by_name(lib_name) except Exception as ex: # pylint: disable=broad-except _logger.warning( @@ -852,15 +850,6 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None: ) -def _setup_httpx2_instrumentation(instrumentation_kwargs: Dict[str, Any]) -> None: - """Enable HTTPX2 when supported by the installed HTTPX instrumentor.""" - try: - from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor - except ImportError: - return - HTTPX2ClientInstrumentor().instrument(skip_dep_check=True, **instrumentation_kwargs) - - def _setup_a365_openai_agents_instrumentation() -> None: """Register the A365 OpenAI Agents trace processor.""" try: diff --git a/tests/azure_monitor/utils/test_configurations.py b/tests/azure_monitor/utils/test_configurations.py index 307321a1..cd1964de 100644 --- a/tests/azure_monitor/utils/test_configurations.py +++ b/tests/azure_monitor/utils/test_configurations.py @@ -107,6 +107,7 @@ def test_get_configurations(self, resource_create_mock): "fastapi": {"enabled": True}, "flask": {"enabled": False}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": True}, "urllib": {"enabled": True}, @@ -147,6 +148,7 @@ def test_get_configurations_defaults(self, resource_create_mock): "fastapi": {"enabled": True}, "flask": {"enabled": True}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": True}, "urllib": {"enabled": True}, @@ -201,6 +203,7 @@ def test_get_configurations_env_vars(self, resource_create_mock): "fastapi": {"enabled": False}, "flask": {"enabled": False}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": False}, "urllib": {"enabled": True}, @@ -289,6 +292,7 @@ def test_merge_instrumentation_options_conflict(self, resource_create_mock): "fastapi": {"enabled": True}, "flask": {"enabled": False}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "previewlib1": {"enabled": True}, # Explicit configuration takes priority "previewlib2": {"enabled": False}, @@ -332,6 +336,7 @@ def test_merge_instrumentation_options_extra_args(self, resource_create_mock): "fastapi": {"enabled": True, "foo": "bar"}, "flask": {"enabled": False, "foo": "bar"}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True, "foo": "bar"}, "previewlib1": {"enabled": True, "foo": "bar"}, "previewlib2": {"enabled": False, "foo": "bar"}, @@ -485,6 +490,7 @@ def test_get_configurations_env_vars_rate_limited(self, resource_create_mock): "fastapi": {"enabled": False}, "flask": {"enabled": False}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": False}, "urllib": {"enabled": True}, @@ -518,6 +524,7 @@ def test_get_configurations_rate_limited_sampler_param(self, resource_create_moc "fastapi": {"enabled": True}, "flask": {"enabled": True}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": True}, "urllib": {"enabled": True}, @@ -562,6 +569,7 @@ def test_get_configurations_env_vars_no_preference(self, resource_create_mock): "fastapi": {"enabled": False}, "flask": {"enabled": False}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": False}, "urllib": {"enabled": True}, @@ -606,6 +614,7 @@ def test_get_configurations_env_vars_check_default(self, resource_create_mock): "fastapi": {"enabled": False}, "flask": {"enabled": False}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": False}, "urllib": {"enabled": True}, @@ -651,6 +660,7 @@ def test_get_configurations_env_vars_fixed_percentage(self, resource_create_mock "fastapi": {"enabled": False}, "flask": {"enabled": False}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": False}, "urllib": {"enabled": True}, @@ -695,6 +705,7 @@ def test_get_configurations_env_vars_always_on(self, resource_create_mock): "fastapi": {"enabled": False}, "flask": {"enabled": False}, "httpx": {"enabled": True}, + "httpx2": {"enabled": True}, "psycopg2": {"enabled": True}, "requests": {"enabled": False}, "urllib": {"enabled": True}, diff --git a/tests/test_httpx_instrumentation.py b/tests/test_httpx_instrumentation.py index b607274f..da1f32a3 100644 --- a/tests/test_httpx_instrumentation.py +++ b/tests/test_httpx_instrumentation.py @@ -31,7 +31,6 @@ _A365_DISABLED_INSTRUMENTATIONS, _SUPPORTED_INSTRUMENTED_LIBRARIES, ) -from microsoft.opentelemetry._distro import _setup_httpx2_instrumentation # noqa: E402 # pylint: enable=wrong-import-position @@ -53,13 +52,11 @@ class TestHttpxInstrumentationConfig(unittest.TestCase): def test_httpx_in_supported_libraries(self): self.assertIn("httpx", _SUPPORTED_INSTRUMENTED_LIBRARIES) + self.assertIn("httpx2", _SUPPORTED_INSTRUMENTED_LIBRARIES) def test_httpx_in_a365_disabled_list(self): self.assertIn("httpx", _A365_DISABLED_INSTRUMENTATIONS) - - def test_httpx2_is_ignored_when_not_available(self): - with patch("builtins.__import__", side_effect=ImportError): - _setup_httpx2_instrumentation({}) + self.assertIn("httpx2", _A365_DISABLED_INSTRUMENTATIONS) class TestHttpxInstrumentorLifecycle(unittest.TestCase): diff --git a/tests/test_instrumentation_options.py b/tests/test_instrumentation_options.py index 2ffabf99..dff96d6b 100644 --- a/tests/test_instrumentation_options.py +++ b/tests/test_instrumentation_options.py @@ -273,6 +273,17 @@ def test_httpx_forwards_kwargs(self): self.assertEqual(call_kwargs["request_hook"], "req_hook") self.assertEqual(call_kwargs["response_hook"], "resp_hook") + def test_httpx2_forwards_kwargs(self): + call_kwargs = self._run_with_options( + "httpx2", + { + "request_hook": "req_hook", + "response_hook": "resp_hook", + }, + ) + self.assertEqual(call_kwargs["request_hook"], "req_hook") + self.assertEqual(call_kwargs["response_hook"], "resp_hook") + def test_psycopg2_forwards_kwargs(self): call_kwargs = self._run_with_options( "psycopg2", @@ -448,6 +459,59 @@ def test_multiple_libs_each_get_own_kwargs(self): self.assertEqual(b_kwargs["request_hook"], "b_hook") self.assertNotIn("excluded_urls", b_kwargs) + def test_httpx2_can_be_disabled_independently(self): + httpx_instrumentor = MagicMock() + httpx2_instrumentor = MagicMock() + + 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( + {"instrumentation_options": {"httpx2": {"enabled": False}}} + ) + + httpx_instrumentor.instrument.assert_called_once() + httpx2_instrumentor.instrument.assert_not_called() + + @patch("microsoft.opentelemetry._distro.set_sdkstats_instrumentation_by_name") + def test_httpx2_failure_does_not_affect_httpx(self, set_sdkstats): + httpx_instrumentor = MagicMock() + httpx2_instrumentor = MagicMock() + httpx2_instrumentor.instrument.side_effect = RuntimeError("httpx2 failed") + + 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({}) + + httpx_instrumentor.instrument.assert_called_once() + httpx2_instrumentor.instrument.assert_called_once() + set_sdkstats.assert_called_once_with("httpx") + def test_kwargs_not_forwarded_to_disabled_lib(self): """A disabled library should not have instrument() called at all.""" instrumentor_instance = MagicMock() From 3d0bf17866191c27f84b5231d9a84709d8bb4a3c Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 3 Sep 2026 13:43:29 -0700 Subject: [PATCH 5/5] Fix lint --- tests/test_httpx_instrumentation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_httpx_instrumentation.py b/tests/test_httpx_instrumentation.py index da1f32a3..e608b8ba 100644 --- a/tests/test_httpx_instrumentation.py +++ b/tests/test_httpx_instrumentation.py @@ -14,7 +14,6 @@ import threading import unittest from http.server import HTTPServer, BaseHTTPRequestHandler -from unittest.mock import patch import pytest