diff --git a/docs/my-website/docs/observability/phoenix_integration.md b/docs/my-website/docs/observability/phoenix_integration.md index d15eea9a83..ad33743993 100644 --- a/docs/my-website/docs/observability/phoenix_integration.md +++ b/docs/my-website/docs/observability/phoenix_integration.md @@ -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 diff --git a/litellm/integrations/arize/arize_phoenix.py b/litellm/integrations/arize/arize_phoenix.py index 60566ee55c..666d322cb2 100644 --- a/litellm/integrations/arize/arize_phoenix.py +++ b/litellm/integrations/arize/arize_phoenix.py @@ -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( diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 53b7825b3d..468fbc9140 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -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), diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index e5c52ce48a..6a36eb6dc3 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -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"] = ( diff --git a/tests/test_litellm/integrations/arize/test_arize_phoenix.py b/tests/test_litellm/integrations/arize/test_arize_phoenix.py index fd81d9d9d7..552f753844 100644 --- a/tests/test_litellm/integrations/arize/test_arize_phoenix.py +++ b/tests/test_litellm/integrations/arize/test_arize_phoenix.py @@ -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()