From 0252ec11257af8244f42f78b5e42d0813b52a851 Mon Sep 17 00:00:00 2001 From: ari-abb Date: Thu, 13 Aug 2026 01:43:33 +0200 Subject: [PATCH 1/3] Fix server.address extraction for openai 3.x (httpx2) get_server_address_and_port checked isinstance(base_url, httpx.URL), but openai 3.x builds on httpx2, so base_url is an httpx2.URL. Neither that branch nor the str branch matched, the function returned (None, None), and server.address and server.port were dropped from every span with no warning. Reading the URL structurally with getattr covers httpx.URL, httpx2.URL, any other URL-like object and plain strings, so the extraction no longer depends on which HTTP client the SDK uses internally. Existing behaviour is preserved on every path that worked before, including omitting the default 443 port. Removing the isinstance check also removes the only use of the httpx import. That matters on its own: this package declares no dependency on httpx while openai 3.x depends on httpx2, so on a clean install httpx can be absent and the module-scope import made the package unimportable rather than merely degraded. Tests cover httpx.URL, httpx2.URL, a plain string, an explicit :443 (asserting the port is still omitted), a missing base_url, and that the package imports with httpx uninstalled. --- .../instrumentation/openai_v2/utils.py | 16 ++-- .../tests/test_server_address.py | 96 +++++++++++++++++++ 2 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_server_address.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py index 0bbaf39ccc..7ca43d0649 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py @@ -9,7 +9,6 @@ from urllib.parse import urlparse import openai -from httpx import URL from openai import NotGiven from opentelemetry._logs import LogRecord @@ -91,13 +90,14 @@ def get_server_address_and_port( base_url = getattr(base_client, "base_url", None) if not base_url: return None, None - address = None - port = None - if isinstance(base_url, URL): - address = base_url.host - port = base_url.port - elif isinstance(base_url, str): - url = urlparse(base_url) + + # Read the URL structurally rather than by type. openai 3.x builds on httpx2, so base_url is an + # httpx2.URL rather than an httpx.URL, and an isinstance check against either one silently stops + # matching when the SDK changes its HTTP client. + address = getattr(base_url, "host", None) + port = getattr(base_url, "port", None) + if address is None: + url = urlparse(str(base_url)) address = url.hostname port = url.port diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_server_address.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_server_address.py new file mode 100644 index 0000000000..7b1a9c9902 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_server_address.py @@ -0,0 +1,96 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for endpoint extraction in `get_server_address_and_port`. + +The extraction must not depend on which HTTP client library the OpenAI SDK happens to use internally. +openai 3.x builds on httpx2, so `client._client.base_url` is an `httpx2.URL`; an isinstance check against +`httpx.URL` silently stops matching and `server.address` disappears from every span. +""" + +import subprocess +import sys +import textwrap +from importlib.metadata import version + +import pytest + +from opentelemetry.instrumentation.openai_v2.utils import ( + get_server_address_and_port, +) + + +class _Client: + """Stands in for the resource object the instrumentation receives (e.g. `client.chat.completions`), + whose `_client` is the SDK client holding `base_url`.""" + + def __init__(self, base_url): + self._client = type("Inner", (), {"base_url": base_url})() + + +def test_httpx_url(): + httpx = pytest.importorskip("httpx") + address, port = get_server_address_and_port(_Client(httpx.URL("http://localhost:8080/v1"))) + assert address == "localhost" + assert port == 8080 + + +def test_httpx2_url(): + httpx2 = pytest.importorskip("httpx2") + address, port = get_server_address_and_port(_Client(httpx2.URL("http://localhost:8080/v1"))) + assert address == "localhost" + assert port == 8080 + + +def test_string_base_url(): + address, port = get_server_address_and_port(_Client("http://localhost:8080/v1")) + assert address == "localhost" + assert port == 8080 + + +def test_default_https_port_is_omitted(): + address, port = get_server_address_and_port(_Client("https://api.openai.com:443/v1")) + assert address == "api.openai.com" + assert port is None + + +def test_missing_base_url(): + assert get_server_address_and_port(_Client(None)) == (None, None) + assert get_server_address_and_port(object()) == (None, None) + + +def test_importable_without_httpx(): + """The module must import when httpx is not installed. + + This package declares no dependency on httpx, and openai 3.x depends on httpx2 instead, so httpx can + legitimately be absent. A module-scope `from httpx import URL` then makes the instrumentation + unimportable rather than merely degraded. + + Skipped on openai 2.x, which imports httpx itself, so the situation cannot arise there. + """ + if int(version("openai").split(".")[0]) < 3: + pytest.skip("openai 2.x depends on httpx itself, so httpx is never absent") + script = textwrap.dedent( + """ + import sys + + class Blocker: + def find_spec(self, name, path=None, target=None): + if name == "httpx" or name.startswith("httpx."): + raise ModuleNotFoundError("No module named 'httpx'") + return None + + sys.meta_path.insert(0, Blocker()) + for module in [m for m in sys.modules if m == "httpx" or m.startswith("httpx.")]: + del sys.modules[module] + + from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor # noqa: F401 + """ + ) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr From f6473a2e0a642ae99c3e8a16b67fd94d15c7c4c8 Mon Sep 17 00:00:00 2001 From: ari-abb Date: Thu, 13 Aug 2026 01:54:34 +0200 Subject: [PATCH 2/3] Add changelog fragment for #4954 --- .changelog/4954.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/4954.fixed diff --git a/.changelog/4954.fixed b/.changelog/4954.fixed new file mode 100644 index 0000000000..d57aa600d8 --- /dev/null +++ b/.changelog/4954.fixed @@ -0,0 +1 @@ +`opentelemetry-instrumentation-openai-v2`: fix `server.address` not being recorded with openai 3.x From c4c482dbd7569b3251a952619602f16b94ace656 Mon Sep 17 00:00:00 2001 From: ari-abb Date: Thu, 13 Aug 2026 01:54:51 +0200 Subject: [PATCH 3/3] Shorten changelog fragment --- .changelog/4954.fixed | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/4954.fixed b/.changelog/4954.fixed index d57aa600d8..724f7e96d5 100644 --- a/.changelog/4954.fixed +++ b/.changelog/4954.fixed @@ -1 +1 @@ -`opentelemetry-instrumentation-openai-v2`: fix `server.address` not being recorded with openai 3.x +`opentelemetry-instrumentation-openai-v2`: fix `server.address` on openai 3.x