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 738fdc15..d43b5ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Release History +# 1.3.9 (Unreleased) +### 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)) # 1.3.8 (2026-08-20) ### Features Added 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/pyproject.toml b/pyproject.toml index b9d0ac77..5918c02c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,23 +23,23 @@ classifiers = [ 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", + "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", + "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/_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/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 0adf795a..e608b8ba 100644 --- a/tests/test_httpx_instrumentation.py +++ b/tests/test_httpx_instrumentation.py @@ -51,9 +51,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) + 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()