mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-08 12:24:04 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user