fix: arize phoenix logging (#16301)

* arize phx

* fix arize integration

* traces to specific project name

* fix

* look for http endpoint
This commit is contained in:
Mubashir Osmani
2025-11-21 18:46:18 -08:00
committed by GitHub
parent eb48d5cc42
commit db58f6aeb1
5 changed files with 105 additions and 29 deletions
@@ -33,6 +33,8 @@ import os
os.environ["PHOENIX_API_KEY"] = "" # Necessary only using Phoenix Cloud
os.environ["PHOENIX_COLLECTOR_HTTP_ENDPOINT"] = "" # The URL of your Phoenix OSS instance e.g. http://localhost:6006/v1/traces
os.environ["PHOENIX_PROJECT_NAME"]="litellm" # OPTIONAL: you can configure project names, otherwise traces would go to "default" project
# This defaults to https://app.phoenix.arize.com/v1/traces for Phoenix Cloud
# LLM API Keys
+38 -24
View File
@@ -1,5 +1,4 @@
import os
import urllib.parse
from typing import TYPE_CHECKING, Any, Union
from litellm._logging import verbose_logger
@@ -23,7 +22,7 @@ else:
Span = Any
ARIZE_HOSTED_PHOENIX_ENDPOINT = "https://app.phoenix.arize.com/v1/traces"
ARIZE_HOSTED_PHOENIX_ENDPOINT = "https://otlp.arize.com/v1/traces"
class ArizePhoenixLogger:
@@ -41,38 +40,53 @@ class ArizePhoenixLogger:
ArizePhoenixConfig: A Pydantic model containing Arize Phoenix configuration.
"""
api_key = os.environ.get("PHOENIX_API_KEY", None)
grpc_endpoint = os.environ.get("PHOENIX_COLLECTOR_ENDPOINT", None)
http_endpoint = os.environ.get("PHOENIX_COLLECTOR_HTTP_ENDPOINT", None)
collector_endpoint = os.environ.get("PHOENIX_COLLECTOR_HTTP_ENDPOINT", None)
if not collector_endpoint:
grpc_endpoint = os.environ.get("PHOENIX_COLLECTOR_ENDPOINT", None)
http_endpoint = os.environ.get("PHOENIX_COLLECTOR_HTTP_ENDPOINT", None)
collector_endpoint = http_endpoint or grpc_endpoint
endpoint = None
protocol: Protocol = "otlp_http"
if http_endpoint:
endpoint = http_endpoint
protocol = "otlp_http"
elif grpc_endpoint:
endpoint = grpc_endpoint
protocol = "otlp_grpc"
if collector_endpoint:
# Parse the endpoint to determine protocol
if collector_endpoint.startswith("grpc://") or (":4317" in collector_endpoint and "/v1/traces" not in collector_endpoint):
endpoint = collector_endpoint
protocol = "otlp_grpc"
else:
# Phoenix Cloud endpoints (app.phoenix.arize.com) include the space in the URL
if "app.phoenix.arize.com" in collector_endpoint:
endpoint = collector_endpoint
protocol = "otlp_http"
# For other HTTP endpoints, ensure they have the correct path
elif "/v1/traces" not in collector_endpoint:
if collector_endpoint.endswith("/v1"):
endpoint = collector_endpoint + "/traces"
elif collector_endpoint.endswith("/"):
endpoint = f"{collector_endpoint}v1/traces"
else:
endpoint = f"{collector_endpoint}/v1/traces"
else:
endpoint = collector_endpoint
protocol = "otlp_http"
else:
endpoint = ARIZE_HOSTED_PHOENIX_ENDPOINT
# If no endpoint specified, self hosted phoenix
endpoint = "http://localhost:6006/v1/traces"
protocol = "otlp_http"
verbose_logger.debug(
f"No PHOENIX_COLLECTOR_ENDPOINT or PHOENIX_COLLECTOR_HTTP_ENDPOINT found, using default endpoint with http: {ARIZE_HOSTED_PHOENIX_ENDPOINT}"
f"No PHOENIX_COLLECTOR_ENDPOINT found, using default local Phoenix endpoint: {endpoint}"
)
otlp_auth_headers = None
# If the endpoint is the Arize hosted Phoenix endpoint, use the api_key as the auth header as currently it is uses
# a slightly different auth header format than self hosted phoenix
if endpoint == ARIZE_HOSTED_PHOENIX_ENDPOINT:
if api_key is None:
raise ValueError(
"PHOENIX_API_KEY must be set when the Arize hosted Phoenix endpoint is used."
)
otlp_auth_headers = f"api_key={api_key}"
elif api_key is not None:
# api_key/auth is optional for self hosted phoenix
otlp_auth_headers = (
f"Authorization={urllib.parse.quote(f'Bearer {api_key}')}"
if api_key is not None:
otlp_auth_headers = f"Authorization=Bearer {api_key}"
elif "app.phoenix.arize.com" in endpoint:
# Phoenix Cloud requires an API key
raise ValueError(
"PHOENIX_API_KEY must be set when using Phoenix Cloud (app.phoenix.arize.com)."
)
return ArizePhoenixConfig(
+4
View File
@@ -1773,6 +1773,10 @@ class OpenTelemetry(CustomLogger):
"""
Create a span for the received proxy server request.
"""
# don't create proxy parent spans for arize phoenix
if self.callback_name == "arize_phoenix":
return None
return self.tracer.start_span(
name="Received Proxy Server Request",
start_time=self._to_ns(start_time),
@@ -3554,8 +3554,19 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915
otel_config = OpenTelemetryConfig(
exporter=arize_phoenix_config.protocol,
endpoint=arize_phoenix_config.endpoint,
headers=arize_phoenix_config.otlp_auth_headers,
)
# Set Phoenix project name from environment variable
phoenix_project_name = os.environ.get("PHOENIX_PROJECT_NAME", None)
if phoenix_project_name:
existing_attrs = os.environ.get("OTEL_RESOURCE_ATTRIBUTES", "")
# Add openinference.project.name attribute
if existing_attrs:
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = f"{existing_attrs},openinference.project.name={phoenix_project_name}"
else:
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = f"openinference.project.name={phoenix_project_name}"
# auth can be disabled on local deployments of arize phoenix
if arize_phoenix_config.otlp_auth_headers is not None:
os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = (
@@ -16,11 +16,11 @@ class TestArizePhoenixConfig(unittest.TestCase):
# Call the function to get the configuration
config = ArizePhoenixLogger.get_arize_phoenix_config()
# Verify the configuration
# Verify the configuration - now uses standard Authorization Bearer format
self.assertEqual(
config.otlp_auth_headers, "Authorization=Bearer%20test_api_key"
config.otlp_auth_headers, "Authorization=Bearer test_api_key"
)
self.assertEqual(config.endpoint, "http://test.endpoint")
self.assertEqual(config.endpoint, "http://test.endpoint/v1/traces")
self.assertEqual(config.protocol, "otlp_http")
@patch.dict(
@@ -34,13 +34,58 @@ class TestArizePhoenixConfig(unittest.TestCase):
# Call the function to get the configuration
config = ArizePhoenixLogger.get_arize_phoenix_config()
# Verify the configuration
# Verify the configuration - now uses standard Authorization Bearer format
self.assertEqual(
config.otlp_auth_headers, "Authorization=Bearer%20test_api_key"
config.otlp_auth_headers, "Authorization=Bearer test_api_key"
)
self.assertEqual(config.endpoint, "grpc://test.endpoint")
self.assertEqual(config.protocol, "otlp_grpc")
@patch.dict(
"os.environ",
{
"PHOENIX_API_KEY": "test_api_key",
"PHOENIX_COLLECTOR_ENDPOINT": "http://localhost:6006",
},
)
def test_get_arize_phoenix_config_http_local(self):
# Test with local Phoenix instance
config = ArizePhoenixLogger.get_arize_phoenix_config()
# Should automatically append /v1/traces to local endpoint
self.assertEqual(
config.otlp_auth_headers, "Authorization=Bearer test_api_key"
)
self.assertEqual(config.endpoint, "http://localhost:6006/v1/traces")
self.assertEqual(config.protocol, "otlp_http")
@patch.dict(
"os.environ",
{
"PHOENIX_COLLECTOR_ENDPOINT": "http://localhost:4317",
},
clear=True
)
def test_get_arize_phoenix_config_grpc_no_api_key(self):
# Test gRPC endpoint detection and no API key (for local development)
config = ArizePhoenixLogger.get_arize_phoenix_config()
# No API key should be fine for local development
self.assertIsNone(config.otlp_auth_headers)
self.assertEqual(config.endpoint, "http://localhost:4317")
self.assertEqual(config.protocol, "otlp_grpc")
@patch.dict("os.environ", {}, clear=True)
def test_get_arize_phoenix_config_defaults_to_local(self):
# Test that it defaults to local Phoenix when no config is provided
config = ArizePhoenixLogger.get_arize_phoenix_config()
# Should default to localhost
self.assertEqual(config.endpoint, "http://localhost:6006/v1/traces")
self.assertEqual(config.protocol, "otlp_http")
# No auth headers when no API key is provided for local instance
self.assertIsNone(config.otlp_auth_headers)
if __name__ == "__main__":
unittest.main()