Skip to content
Open
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
1 change: 1 addition & 0 deletions .changelog/4954.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-instrumentation-openai-v2`: fix `server.address` on openai 3.x
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from urllib.parse import urlparse

import openai
from httpx import URL
from openai import NotGiven

from opentelemetry._logs import LogRecord
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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