import subprocess import sys import time import httpx import pytest import yaml from fastapi import FastAPI import ray from ray import serve from ray._common.test_utils import wait_for_condition from ray._common.usage.usage_lib import get_extra_usage_tags_to_report from ray.serve._private.constants import ( RAY_SERVE_USE_GRPC_BY_DEFAULT, SERVE_MULTIPLEXED_MODEL_ID, ) from ray.serve._private.test_utils import ( check_apps_running, check_telemetry, ) from ray.serve._private.usage import ServeUsageTag from ray.serve.context import _get_global_client from ray.serve.schema import ApplicationStatus, ServeDeploySchema def test_fastapi_detected(manage_ray_with_telemetry): """ Check that FastAPI is detected by telemetry. """ storage_handle = manage_ray_with_telemetry # Check that telemetry related to FastAPI app is not set def check_report_before_fastapi(): report = ray.get(storage_handle.get_report.remote()) assert ServeUsageTag.FASTAPI_USED.get_value_from_report(report) is None assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2" assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 1 assert int(ServeUsageTag.NUM_DEPLOYMENTS.get_value_from_report(report)) == 1 assert int(ServeUsageTag.NUM_GPU_DEPLOYMENTS.get_value_from_report(report)) == 0 return True wait_for_condition(check_report_before_fastapi) app = FastAPI() @serve.deployment @serve.ingress(app) class FastAPIDeployment: @app.get("/route1") async def route1(self): return "Welcome to route 1!" @app.get("/route2") async def app2(self): return "Welcome to route 2!" fastapi_app = FastAPIDeployment.bind() serve.run(fastapi_app, name="fastapi_app", route_prefix="/fastapi") wait_for_condition( lambda: serve.status().applications["fastapi_app"].status == ApplicationStatus.RUNNING, timeout=15, ) current_num_reports = ray.get(storage_handle.get_reports_received.remote()) wait_for_condition( lambda: ray.get(storage_handle.get_reports_received.remote()) > current_num_reports, timeout=5, ) report = ray.get(storage_handle.get_report.remote()) # Check all telemetry relevant to the Serve apps on this cluster. assert int(ServeUsageTag.FASTAPI_USED.get_value_from_report(report)) == 1 assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2" assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 2 assert int(ServeUsageTag.NUM_DEPLOYMENTS.get_value_from_report(report)) == 2 assert int(ServeUsageTag.NUM_GPU_DEPLOYMENTS.get_value_from_report(report)) == 0 # Check that Serve telemetry not relevant to the running apps is omitted. assert ServeUsageTag.DAG_DRIVER_USED.get_value_from_report(report) is None assert ServeUsageTag.HTTP_ADAPTER_USED.get_value_from_report(report) is None assert ServeUsageTag.GRPC_INGRESS_USED.get_value_from_report(report) is None assert ServeUsageTag.REST_API_VERSION.get_value_from_report(report) is None @serve.deployment class Stub: pass stub_app = Stub.bind() @pytest.mark.skipif(sys.platform == "win32", reason="File path incorrect on Windows.") def test_rest_api(manage_ray_with_telemetry, tmp_dir): """ Check that telemetry works with REST API. """ storage = manage_ray_with_telemetry # Check that REST API telemetry is not set check_telemetry(ServeUsageTag.REST_API_VERSION, expected=None) config = { "applications": [ { "name": "stub_app", "import_path": "ray.serve.tests.test_telemetry_1.stub_app", "route_prefix": "/stub", }, ] } config_file_path = f"{tmp_dir}/config.yaml" with open(config_file_path, "w+") as f: yaml.safe_dump(config, f) subprocess.check_output(["serve", "deploy", config_file_path]) client = _get_global_client() # Make sure the applications are RUNNING. wait_for_condition(check_apps_running, apps=["stub_app"], timeout=15) current_num_reports = ray.get(storage.get_reports_received.remote()) wait_for_condition( lambda: ray.get(storage.get_reports_received.remote()) > current_num_reports, timeout=5, ) report = ray.get(storage.get_report.remote()) # Check all telemetry relevant to the Serve apps on this cluster assert ServeUsageTag.REST_API_VERSION.get_value_from_report(report) == "v2" assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2" assert int(ServeUsageTag.NUM_GPU_DEPLOYMENTS.get_value_from_report(report)) == 0 # Assert num of deployments from controller. assert len(client.get_all_deployment_statuses()) == 2 assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 2 assert int(ServeUsageTag.NUM_DEPLOYMENTS.get_value_from_report(report)) == 2 # Check that Serve telemetry not relevant to the running apps is omitted assert ServeUsageTag.FASTAPI_USED.get_value_from_report(report) is None assert ServeUsageTag.GRPC_INGRESS_USED.get_value_from_report(report) is None assert ServeUsageTag.HTTP_ADAPTER_USED.get_value_from_report(report) is None assert ServeUsageTag.DAG_DRIVER_USED.get_value_from_report(report) is None assert ServeUsageTag.AUTO_NUM_REPLICAS_USED.get_value_from_report(report) is None # Check that app deletions are tracked. new_config = {"applications": []} with open(config_file_path, "w+") as f: yaml.safe_dump(new_config, f) subprocess.check_output(["serve", "deploy", config_file_path]) wait_for_condition( lambda: int( ServeUsageTag.NUM_APPS.get_value_from_report( ray.get(storage.get_report.remote()) ) ) == 1, timeout=15, ) report = ray.get(storage.get_report.remote()) assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 1 assert int(ServeUsageTag.NUM_DEPLOYMENTS.get_value_from_report(report)) == 1 @serve.deployment(ray_actor_options={"num_cpus": 0}) class Tester: def __call__(self, *args): pass def reconfigure(self, *args): pass tester = Tester.bind() @pytest.mark.parametrize( "lightweight_option,value,new_value", [ ("num_replicas", 1, 2), ("user_config", {}, {"some_setting": 10}), ("autoscaling_config", {"max_replicas": 3}, {"max_replicas": 5}), ], ) def test_lightweight_config_options( manage_ray_with_telemetry, lightweight_option, value, new_value ): """Check that lightweight config options are detected by telemetry.""" storage = manage_ray_with_telemetry lightweight_tagkeys = { "num_replicas": ServeUsageTag.NUM_REPLICAS_LIGHTWEIGHT_UPDATED, "user_config": ServeUsageTag.USER_CONFIG_LIGHTWEIGHT_UPDATED, "autoscaling_config": ServeUsageTag.AUTOSCALING_CONFIG_LIGHTWEIGHT_UPDATED, } # Check that REST API telemetry is not set check_telemetry(ServeUsageTag.REST_API_VERSION, expected=None) config = { "applications": [ { "name": "test_app", "import_path": "ray.serve.tests.test_telemetry_1.tester", "deployments": [{"name": "Tester"}], }, ] } config["applications"][0]["deployments"][0][lightweight_option] = value # Deploy first config client = _get_global_client() client.deploy_apps(ServeDeploySchema(**config)) wait_for_condition(check_apps_running, apps=["test_app"], timeout=15) current_num_reports = ray.get(storage.get_reports_received.remote()) wait_for_condition( lambda: ray.get(storage.get_reports_received.remote()) > current_num_reports, timeout=10, ) report = ray.get(storage.get_report.remote()) # Check assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 2 assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2" for tagkey in lightweight_tagkeys.values(): assert tagkey.get_value_from_report(report) is None # Change config and deploy again config["applications"][0]["deployments"][0][lightweight_option] = new_value client.deploy_apps(ServeDeploySchema(**config)) wait_for_condition(check_apps_running, apps=["test_app"], timeout=15) # Check again wait_for_condition( check_telemetry, tag=lightweight_tagkeys[lightweight_option], expected="True", timeout=5, ) report = ray.get(storage.get_report.remote()) assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 2 assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2" for tagkey in lightweight_tagkeys.values(): if tagkey != lightweight_tagkeys[lightweight_option]: assert tagkey.get_value_from_report(report) is None @pytest.mark.parametrize("call_in_deployment", [False, True]) def test_handle_apis_detected(manage_ray_with_telemetry, call_in_deployment): """Check that the various handles are detected correctly by telemetry.""" check_telemetry(ServeUsageTag.DEPLOYMENT_HANDLE_API_USED, expected=None) @serve.deployment class Downstream: def __call__(self): return "hi" @serve.deployment class Caller: def __init__(self, h): self._h = h async def __call__(self, call_downstream=True): if call_downstream: await self._h.remote() return "ok" handle = serve.run(Caller.bind(Downstream.bind())) if call_in_deployment: result = httpx.get("http://localhost:8000/").text else: result = handle.remote(call_downstream=False).result() assert result == "ok" wait_for_condition( check_telemetry, tag=ServeUsageTag.DEPLOYMENT_HANDLE_API_USED, expected="1" ) @pytest.mark.skipif( RAY_SERVE_USE_GRPC_BY_DEFAULT, reason="cannot convert to object ref when using gRPC", ) @pytest.mark.parametrize("mode", ["http", "outside_deployment", "inside_deployment"]) def test_deployment_handle_to_obj_ref_detected(manage_ray_with_telemetry, mode): """Check that the handle to_object_ref API is detected correctly by telemetry.""" check_telemetry( ServeUsageTag.DEPLOYMENT_HANDLE_TO_OBJECT_REF_API_USED, expected=None ) @serve.deployment class Downstream: def __call__(self): return "hi" @serve.deployment class Caller: def __init__(self, h): self._h = h async def get(self, call_downstream=False): if call_downstream: await self._h.remote()._to_object_ref() return "ok" async def __call__(self): return await self.get() handle = serve.run(Caller.bind(Downstream.bind())) if mode == "http": result = httpx.get("http://localhost:8000/").text elif mode == "outside_deployment": result = ray.get(handle.get.remote()._to_object_ref_sync()) else: result = handle.get.remote(call_downstream=True).result() assert result == "ok" if mode == "http": for _ in range(20): check_telemetry( ServeUsageTag.DEPLOYMENT_HANDLE_TO_OBJECT_REF_API_USED, expected=None ) time.sleep(0.1) else: wait_for_condition( check_telemetry, tag=ServeUsageTag.DEPLOYMENT_HANDLE_TO_OBJECT_REF_API_USED, expected="1", ) def test_multiplexed_detect(manage_ray_with_telemetry): """Check that multiplexed api is detected by telemetry.""" @serve.deployment class Model: @serve.multiplexed(max_num_models_per_replica=1) async def get_model(self, tag): return tag async def __call__(self, request): tag = serve.get_multiplexed_model_id() await self.get_model(tag) return tag serve.run(Model.bind(), name="app", route_prefix="/app") wait_for_condition(check_apps_running, apps=["app"]) check_telemetry(ServeUsageTag.MULTIPLEXED_API_USED, expected=None) headers = {SERVE_MULTIPLEXED_MODEL_ID: "1"} resp = httpx.get("http://localhost:8000/app", headers=headers) assert resp.status_code == 200 wait_for_condition( check_telemetry, tag=ServeUsageTag.MULTIPLEXED_API_USED, expected="1" ) class TestProxyTelemetry: def test_both_proxies_detected(manage_ray, ray_shutdown): """Test that both HTTP and gRPC proxies are detected by telemetry. When both HTTP and gRPC proxies are used, both telemetry should be detected. """ result = get_extra_usage_tags_to_report( ray.experimental.internal_kv.internal_kv_get_gcs_client() ) report = {"extra_usage_tags": result} # Ensure neither the HTTP nor gRPC proxy telemetry exist. assert ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report) is None assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None grpc_servicer_functions = [ "ray.serve.generated.serve_pb2_grpc." "add_UserDefinedServiceServicer_to_server", ] serve.start(grpc_options={"grpc_servicer_functions": grpc_servicer_functions}) result = get_extra_usage_tags_to_report( ray.experimental.internal_kv.internal_kv_get_gcs_client() ) report = {"extra_usage_tags": result} # Ensure both HTTP and gRPC proxy telemetry exist. assert int(ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report)) == 1 assert int(ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report)) == 1 def test_only_http_proxy_detected(manage_ray, ray_shutdown): """Test that only HTTP proxy is detected by telemetry. When only HTTP proxy is used, only the http proxy telemetry should be detected. """ result = get_extra_usage_tags_to_report( ray.experimental.internal_kv.internal_kv_get_gcs_client() ) report = {"extra_usage_tags": result} # Ensure the telemetry does not yet exist. assert ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report) is None assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None serve.start() result = get_extra_usage_tags_to_report( ray.experimental.internal_kv.internal_kv_get_gcs_client() ) report = {"extra_usage_tags": result} # Ensure only the HTTP proxy telemetry exist. assert int(ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report)) == 1 assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None def test_no_proxy_detected(manage_ray, ray_shutdown): """Test that no proxy is detected by telemetry. When neither HTTP nor gRPC proxy is used, no proxy telemetry should be detected. """ result = get_extra_usage_tags_to_report( ray.experimental.internal_kv.internal_kv_get_gcs_client() ) report = {"extra_usage_tags": result} # Ensure neither the HTTP nor gRPC proxy telemetry exist. assert ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report) is None assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None serve.start(http_options={"location": "NoServer"}) result = get_extra_usage_tags_to_report( ray.experimental.internal_kv.internal_kv_get_gcs_client() ) report = {"extra_usage_tags": result} # Ensure neither the HTTP nor gRPC proxy telemetry exist. assert ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report) is None assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None if __name__ == "__main__": sys.exit(pytest.main(["-v", "-s", __file__]))