1278 lines
44 KiB
Python
1278 lines
44 KiB
Python
import json
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import signal
|
|
import sys
|
|
import time
|
|
import warnings
|
|
from pprint import pformat
|
|
from unittest.mock import MagicMock
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import requests
|
|
from google.protobuf.timestamp_pb2 import Timestamp
|
|
|
|
import ray
|
|
from ray._common.network_utils import build_address, find_free_port
|
|
from ray._common.test_utils import (
|
|
PrometheusTimeseries,
|
|
SignalActor,
|
|
fetch_prometheus_metric_timeseries,
|
|
fetch_prometheus_timeseries,
|
|
wait_for_condition,
|
|
)
|
|
from ray._private.metrics_agent import (
|
|
Gauge as MetricsAgentGauge,
|
|
PrometheusServiceDiscoveryWriter,
|
|
)
|
|
from ray._private.ray_constants import (
|
|
AGENT_PROCESS_TYPE_DASHBOARD_AGENT,
|
|
AGENT_PROCESS_TYPE_RUNTIME_ENV_AGENT,
|
|
PROMETHEUS_SERVICE_DISCOVERY_FILE,
|
|
)
|
|
from ray._private.test_utils import (
|
|
get_log_batch,
|
|
raw_metric_timeseries,
|
|
)
|
|
from ray._raylet import JobID, TaskID
|
|
from ray.autoscaler._private.constants import AUTOSCALER_METRIC_PORT
|
|
from ray.core.generated.common_pb2 import TaskAttempt
|
|
from ray.core.generated.events_base_event_pb2 import RayEvent
|
|
from ray.core.generated.events_event_aggregator_service_pb2 import (
|
|
AddEventsRequest,
|
|
RayEventsData,
|
|
TaskEventsMetadata,
|
|
)
|
|
from ray.dashboard.consts import DASHBOARD_METRIC_PORT
|
|
from ray.dashboard.modules.aggregator.constants import CONSUMER_TAG_KEY
|
|
from ray.dashboard.modules.aggregator.tests.test_aggregator_agent import (
|
|
get_event_aggregator_grpc_stub,
|
|
)
|
|
from ray.util.metrics import Counter, Gauge, Histogram, Metric
|
|
from ray.util.state import list_nodes
|
|
|
|
os.environ["RAY_event_stats"] = "1"
|
|
|
|
try:
|
|
import prometheus_client
|
|
except ImportError:
|
|
prometheus_client = None
|
|
|
|
# This list of metrics should be kept in sync with metric definitions across the codebase
|
|
# NOTE: Commented out metrics are not available in this test.
|
|
# TODO(Clark): Find ways to trigger commented out metrics in cluster setup.
|
|
_METRICS = [
|
|
"ray_node_disk_usage",
|
|
"ray_node_mem_used",
|
|
"ray_node_mem_total",
|
|
"ray_node_mem_used_host",
|
|
"ray_node_mem_total_host",
|
|
"ray_node_cpu_utilization",
|
|
# TODO(rickyx): refactoring the below 3 metric seem to be a bit involved
|
|
# , e.g. need to see how users currently depend on them.
|
|
"ray_object_store_available_memory",
|
|
"ray_object_store_used_memory",
|
|
"ray_object_store_num_local_objects",
|
|
"ray_object_store_memory",
|
|
"ray_object_manager_num_pull_requests",
|
|
"ray_object_directory_subscriptions",
|
|
"ray_object_directory_updates",
|
|
"ray_object_directory_lookups",
|
|
"ray_object_directory_added_locations",
|
|
"ray_object_directory_removed_locations",
|
|
"ray_internal_num_processes_started",
|
|
"ray_internal_num_spilled_tasks",
|
|
# "ray_unintentional_worker_failures_total",
|
|
# "ray_node_failure_total",
|
|
"ray_grpc_server_req_process_time_ms_sum",
|
|
"ray_grpc_server_req_process_time_ms_bucket",
|
|
"ray_grpc_server_req_process_time_ms_count",
|
|
"ray_grpc_server_req_new_total",
|
|
"ray_grpc_server_req_handling_total",
|
|
"ray_grpc_server_req_finished_total",
|
|
"ray_object_manager_received_chunks",
|
|
"ray_pull_manager_usage_bytes",
|
|
"ray_pull_manager_requested_bundles",
|
|
"ray_pull_manager_requests",
|
|
"ray_pull_manager_active_bundles",
|
|
"ray_pull_manager_retries_total",
|
|
"ray_push_manager_num_pushes_remaining",
|
|
"ray_push_manager_chunks",
|
|
"ray_scheduler_failed_worker_startup_total",
|
|
"ray_scheduler_tasks",
|
|
"ray_scheduler_unscheduleable_tasks",
|
|
"ray_spill_manager_objects",
|
|
"ray_spill_manager_objects_bytes",
|
|
"ray_spill_manager_request_total",
|
|
# "ray_spill_manager_throughput_mb",
|
|
"ray_gcs_placement_group_creation_latency_ms_sum",
|
|
"ray_gcs_placement_group_scheduling_latency_ms_sum",
|
|
"ray_gcs_placement_group_count",
|
|
"ray_gcs_actors_count",
|
|
]
|
|
|
|
# This list of metrics should be kept in sync with
|
|
# ray/python/ray/autoscaler/_private/prom_metrics.py
|
|
_AUTOSCALER_METRICS = [
|
|
"autoscaler_config_validation_exceptions",
|
|
"autoscaler_node_launch_exceptions",
|
|
"autoscaler_pending_nodes",
|
|
"autoscaler_reset_exceptions",
|
|
"autoscaler_running_workers",
|
|
"autoscaler_started_nodes",
|
|
"autoscaler_stopped_nodes",
|
|
"autoscaler_update_loop_exceptions",
|
|
"autoscaler_worker_create_node_time",
|
|
"autoscaler_worker_update_time",
|
|
"autoscaler_updating_nodes",
|
|
"autoscaler_successful_updates",
|
|
"autoscaler_failed_updates",
|
|
"autoscaler_failed_create_nodes",
|
|
"autoscaler_recovering_nodes",
|
|
"autoscaler_successful_recoveries",
|
|
"autoscaler_failed_recoveries",
|
|
"autoscaler_drain_node_exceptions",
|
|
"autoscaler_update_time",
|
|
"autoscaler_cluster_resources",
|
|
"autoscaler_pending_resources",
|
|
]
|
|
|
|
|
|
# This list of metrics should be kept in sync with
|
|
# dashboard/dashboard_metrics.py
|
|
_DASHBOARD_METRICS = [
|
|
"ray_dashboard_api_requests_duration_seconds_bucket",
|
|
"ray_dashboard_api_requests_duration_seconds_created",
|
|
"ray_dashboard_api_requests_count_requests_total",
|
|
"ray_dashboard_api_requests_count_requests_created",
|
|
"ray_component_cpu_percentage",
|
|
"ray_component_uss_mb",
|
|
"ray_component_uss_bytes",
|
|
]
|
|
|
|
_EVENT_AGGREGATOR_METRICS = [
|
|
"ray_aggregator_agent_events_received_total",
|
|
"ray_aggregator_agent_published_events_total",
|
|
"ray_aggregator_agent_filtered_events_total",
|
|
"ray_aggregator_agent_queue_dropped_events_total",
|
|
"ray_aggregator_agent_consecutive_failures_since_last_success",
|
|
"ray_aggregator_agent_time_since_last_success_seconds",
|
|
"ray_aggregator_agent_publish_latency_seconds_bucket",
|
|
"ray_aggregator_agent_publish_latency_seconds_count",
|
|
"ray_aggregator_agent_publish_latency_seconds_sum",
|
|
]
|
|
|
|
_NODE_METRICS = [
|
|
"ray_node_cpu_utilization",
|
|
"ray_node_cpu_count",
|
|
"ray_node_mem_used",
|
|
"ray_node_mem_available",
|
|
"ray_node_mem_total",
|
|
"ray_node_mem_total_host",
|
|
"ray_node_mem_used_host",
|
|
"ray_node_disk_io_read",
|
|
"ray_node_disk_io_write",
|
|
"ray_node_disk_io_read_count",
|
|
"ray_node_disk_io_write_count",
|
|
"ray_node_disk_io_read_speed",
|
|
"ray_node_disk_io_write_speed",
|
|
"ray_node_disk_read_iops",
|
|
"ray_node_disk_write_iops",
|
|
"ray_node_disk_usage",
|
|
"ray_node_disk_free",
|
|
"ray_node_disk_utilization_percentage",
|
|
"ray_node_network_sent",
|
|
"ray_node_network_received",
|
|
"ray_node_network_send_speed",
|
|
"ray_node_network_receive_speed",
|
|
]
|
|
|
|
|
|
if sys.platform == "linux" or sys.platform == "linux2":
|
|
_NODE_METRICS.append("ray_node_mem_shared_bytes")
|
|
|
|
|
|
_NODE_COMPONENT_METRICS = [
|
|
"ray_component_cpu_percentage",
|
|
"ray_component_rss_mb",
|
|
"ray_component_rss_bytes",
|
|
"ray_component_uss_mb",
|
|
"ray_component_uss_bytes",
|
|
"ray_component_num_fds",
|
|
]
|
|
|
|
_METRICS.append("ray_health_check_rpc_latency_ms_sum")
|
|
|
|
|
|
@pytest.fixture
|
|
def _setup_cluster_for_test(request, ray_start_cluster):
|
|
enable_metrics_collection = request.param
|
|
NUM_NODES = 2
|
|
cluster = ray_start_cluster
|
|
# Add a head node.
|
|
cluster.add_node(
|
|
_system_config={
|
|
"metrics_report_interval_ms": 1000,
|
|
"event_stats_print_interval_ms": 500,
|
|
"event_stats": True,
|
|
"enable_metrics_collection": enable_metrics_collection,
|
|
}
|
|
)
|
|
# Add worker nodes.
|
|
[cluster.add_node() for _ in range(NUM_NODES - 1)]
|
|
cluster.wait_for_nodes()
|
|
ray_context = ray.init(address=cluster.address)
|
|
|
|
worker_should_exit = SignalActor.remote()
|
|
|
|
extra_tags = {"ray_version": ray.__version__}
|
|
|
|
# Generate metrics in the driver.
|
|
counter = Counter("test_driver_counter", description="desc")
|
|
counter.inc(tags=extra_tags)
|
|
gauge = Gauge("test_gauge", description="gauge")
|
|
gauge.set(1, tags=extra_tags)
|
|
|
|
# Generate some metrics from actor & tasks.
|
|
@ray.remote
|
|
def f():
|
|
counter = Counter("test_counter", description="desc")
|
|
counter.inc()
|
|
counter = ray.get(ray.put(counter)) # Test serialization.
|
|
counter.inc(tags=extra_tags)
|
|
counter.inc(2, tags=extra_tags)
|
|
ray.get(worker_should_exit.wait.remote())
|
|
|
|
# Generate some metrics for the placement group.
|
|
pg = ray.util.placement_group(bundles=[{"CPU": 1}])
|
|
ray.get(pg.ready())
|
|
ray.util.remove_placement_group(pg)
|
|
|
|
@ray.remote
|
|
class A:
|
|
async def ping(self):
|
|
histogram = Histogram(
|
|
"test_histogram", description="desc", boundaries=[0.1, 1.6]
|
|
)
|
|
histogram = ray.get(ray.put(histogram)) # Test serialization.
|
|
histogram.observe(1.5, tags=extra_tags)
|
|
histogram.observe(0.0, tags=extra_tags)
|
|
ray.get(worker_should_exit.wait.remote())
|
|
|
|
a = A.remote()
|
|
obj_refs = [f.remote(), a.ping.remote()]
|
|
# Infeasible task
|
|
b = f.options(resources={"a": 1}) # noqa
|
|
|
|
# Make a request to the dashboard to produce some dashboard metrics
|
|
requests.get(f"http://{ray_context.dashboard_url}/nodes")
|
|
|
|
node_info_list = ray.nodes()
|
|
prom_addresses = []
|
|
for node_info in node_info_list:
|
|
metrics_export_port = node_info["MetricsExportPort"]
|
|
if enable_metrics_collection:
|
|
# When metrics are enabled, all nodes should have valid ports
|
|
assert metrics_export_port > 0, (
|
|
f"Expected MetricsExportPort > 0 when metrics enabled, "
|
|
f"but got {metrics_export_port} for node {node_info['NodeID']}"
|
|
)
|
|
else:
|
|
# When metrics are disabled, all nodes should have port == -1
|
|
assert metrics_export_port == -1, (
|
|
f"Expected MetricsExportPort == -1 when metrics disabled, "
|
|
f"but got {metrics_export_port} for node {node_info['NodeID']}"
|
|
)
|
|
continue
|
|
addr = node_info["NodeManagerAddress"]
|
|
prom_addresses.append(build_address(addr, metrics_export_port))
|
|
autoscaler_export_addr = build_address(
|
|
cluster.head_node.node_ip_address, AUTOSCALER_METRIC_PORT
|
|
)
|
|
dashboard_export_addr = build_address(
|
|
cluster.head_node.node_ip_address, DASHBOARD_METRIC_PORT
|
|
)
|
|
yield prom_addresses, autoscaler_export_addr, dashboard_export_addr
|
|
|
|
ray.get(worker_should_exit.send.remote())
|
|
ray.get(obj_refs)
|
|
ray.shutdown()
|
|
cluster.shutdown()
|
|
|
|
|
|
@pytest.mark.skipif(prometheus_client is None, reason="Prometheus not installed")
|
|
@pytest.mark.parametrize("_setup_cluster_for_test", [True], indirect=True)
|
|
def test_metrics_export_end_to_end(_setup_cluster_for_test):
|
|
TEST_TIMEOUT_S = 30
|
|
(
|
|
prom_addresses,
|
|
autoscaler_export_addr,
|
|
dashboard_export_addr,
|
|
) = _setup_cluster_for_test
|
|
ray_timeseries = PrometheusTimeseries()
|
|
autoscaler_timeseries = PrometheusTimeseries()
|
|
dashboard_timeseries = PrometheusTimeseries()
|
|
|
|
def test_cases():
|
|
fetch_prometheus_timeseries(prom_addresses, ray_timeseries)
|
|
components_dict = ray_timeseries.components_dict
|
|
metric_descriptors = ray_timeseries.metric_descriptors
|
|
metric_samples = ray_timeseries.metric_samples.values()
|
|
metric_names = metric_descriptors.keys()
|
|
|
|
session_name = ray._private.worker.global_worker.node.session_name
|
|
|
|
# Raylet should be on every node
|
|
assert all("raylet" in components for components in components_dict.values())
|
|
|
|
# GCS server should be on one node
|
|
assert any(
|
|
"gcs_server" in components for components in components_dict.values()
|
|
)
|
|
|
|
# Core worker should be on at least on node
|
|
assert any(
|
|
"core_worker" in components for components in components_dict.values()
|
|
)
|
|
# The list of custom or user defined metrics. Open Telemetry backend does not
|
|
# support exporting Counter as Gauge, so we skip some metrics in that case.
|
|
custom_metrics = (
|
|
"test_counter_total",
|
|
"test_driver_counter_total",
|
|
"test_gauge",
|
|
)
|
|
|
|
# Make sure our user defined metrics exist and have the correct types
|
|
for metric_name in custom_metrics:
|
|
metric_name = f"ray_{metric_name}"
|
|
assert metric_name in metric_names
|
|
if metric_name.endswith("_total"):
|
|
assert metric_descriptors[metric_name].type == "counter"
|
|
elif metric_name.endswith("_counter"):
|
|
# Make sure we emit counter as gauge for bug compatibility
|
|
assert metric_descriptors[metric_name].type == "gauge"
|
|
elif metric_name.endswith("_bucket"):
|
|
assert metric_descriptors[metric_name].type == "histogram"
|
|
elif metric_name.endswith("_gauge"):
|
|
assert metric_descriptors[metric_name].type == "gauge"
|
|
|
|
# Make sure metrics are recorded.
|
|
for metric in _METRICS:
|
|
assert metric in metric_names, f"metric {metric} not in {metric_names}"
|
|
|
|
for sample in metric_samples:
|
|
# All Ray metrics have label "Version" and "SessionName".
|
|
if sample.name in _METRICS or sample.name in _DASHBOARD_METRICS:
|
|
assert sample.labels.get("Version") == ray.__version__, sample
|
|
assert sample.labels["SessionName"] == session_name, sample
|
|
|
|
# Make sure the numeric values are correct
|
|
test_counter_sample = [m for m in metric_samples if "test_counter" in m.name][0]
|
|
assert test_counter_sample.value == 4.0
|
|
|
|
test_driver_counter_sample = [
|
|
m for m in metric_samples if "test_driver_counter" in m.name
|
|
][0]
|
|
assert test_driver_counter_sample.value == 1.0
|
|
|
|
# Make sure the gRPC stats are not reported from workers. We disabled
|
|
# it there because it has too high cardinality.
|
|
grpc_metrics = [
|
|
"ray_grpc_server_req_process_time_ms_sum",
|
|
"ray_grpc_server_req_process_time_ms_bucket",
|
|
"ray_grpc_server_req_process_time_ms_count",
|
|
"ray_grpc_server_req_new_total",
|
|
"ray_grpc_server_req_handling_total",
|
|
"ray_grpc_server_req_finished_total",
|
|
]
|
|
for grpc_metric in grpc_metrics:
|
|
grpc_samples = [m for m in metric_samples if grpc_metric in m.name]
|
|
for grpc_sample in grpc_samples:
|
|
assert grpc_sample.labels["Component"] != "core_worker"
|
|
|
|
# Autoscaler metrics
|
|
fetch_prometheus_timeseries([autoscaler_export_addr], autoscaler_timeseries)
|
|
autoscaler_metric_descriptors = autoscaler_timeseries.metric_descriptors
|
|
autoscaler_samples = autoscaler_timeseries.metric_samples.values()
|
|
autoscaler_metric_names = autoscaler_metric_descriptors.keys()
|
|
for metric in _AUTOSCALER_METRICS:
|
|
# Metric name should appear with some suffix (_count, _total,
|
|
# etc...) in the list of all names
|
|
assert any(
|
|
name.startswith(metric) for name in autoscaler_metric_names
|
|
), f"{metric} not in {autoscaler_metric_names}"
|
|
for sample in autoscaler_samples:
|
|
assert sample.labels["SessionName"] == session_name
|
|
|
|
# Dashboard metrics
|
|
fetch_prometheus_timeseries([dashboard_export_addr], dashboard_timeseries)
|
|
dashboard_metric_descriptors = dashboard_timeseries.metric_descriptors
|
|
dashboard_metric_names = dashboard_metric_descriptors.keys()
|
|
for metric in _DASHBOARD_METRICS:
|
|
# Metric name should appear with some suffix (_count, _total,
|
|
# etc...) in the list of all names
|
|
assert any(
|
|
name.startswith(metric) for name in dashboard_metric_names
|
|
), f"{metric} not in {dashboard_metric_names}"
|
|
|
|
def wrap_test_case_for_retry():
|
|
try:
|
|
test_cases()
|
|
return True
|
|
except AssertionError:
|
|
return False
|
|
|
|
try:
|
|
wait_for_condition(
|
|
wrap_test_case_for_retry,
|
|
timeout=TEST_TIMEOUT_S,
|
|
retry_interval_ms=1000, # Yield resource for other processes
|
|
)
|
|
except RuntimeError:
|
|
# print(f"The components are {pformat(ray_timeseries)}")
|
|
test_cases() # Should fail assert
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Not working in Windows.")
|
|
@pytest.mark.skipif(prometheus_client is None, reason="Prometheus not installed")
|
|
def test_metrics_export_node_metrics(shutdown_only):
|
|
# Verify node metrics are available.
|
|
addr = ray.init()
|
|
dashboard_export_addr = build_address(
|
|
addr["node_ip_address"], DASHBOARD_METRIC_PORT
|
|
)
|
|
node_timeseries = PrometheusTimeseries()
|
|
dashboard_timeseries = PrometheusTimeseries()
|
|
|
|
def verify_node_metrics():
|
|
avail_metrics = raw_metric_timeseries(addr, node_timeseries)
|
|
|
|
components = set()
|
|
for metric in _NODE_COMPONENT_METRICS:
|
|
samples = avail_metrics[metric]
|
|
for sample in samples:
|
|
components.add(sample.labels["Component"])
|
|
assert components == {
|
|
AGENT_PROCESS_TYPE_DASHBOARD_AGENT,
|
|
AGENT_PROCESS_TYPE_RUNTIME_ENV_AGENT,
|
|
"gcs",
|
|
"raylet",
|
|
"agent",
|
|
"ray::IDLE",
|
|
sys.executable,
|
|
}
|
|
|
|
avail_metrics = set(avail_metrics)
|
|
|
|
for node_metric in _NODE_METRICS:
|
|
assert node_metric in avail_metrics
|
|
for node_metric in _NODE_COMPONENT_METRICS:
|
|
assert node_metric in avail_metrics
|
|
return True
|
|
|
|
def verify_dashboard_metrics():
|
|
avail_metrics = fetch_prometheus_metric_timeseries(
|
|
[dashboard_export_addr], dashboard_timeseries
|
|
)
|
|
# Run list nodes to trigger dashboard API.
|
|
list_nodes()
|
|
|
|
# Verify metrics exist.
|
|
for metric in _DASHBOARD_METRICS:
|
|
# Metric name should appear with some suffix (_count, _total,
|
|
# etc...) in the list of all names
|
|
assert len(avail_metrics[metric]) > 0
|
|
|
|
samples = avail_metrics[metric]
|
|
for sample in samples:
|
|
assert sample.labels["Component"].startswith("dashboard")
|
|
|
|
return True
|
|
|
|
wait_for_condition(verify_node_metrics)
|
|
wait_for_condition(verify_dashboard_metrics)
|
|
|
|
|
|
_EVENT_AGGREGATOR_AGENT_TARGET_PORT = find_free_port()
|
|
_EVENT_AGGREGATOR_AGENT_TARGET_IP = "127.0.0.1"
|
|
_EVENT_AGGREGATOR_AGENT_TARGET_ADDR = (
|
|
f"http://{_EVENT_AGGREGATOR_AGENT_TARGET_IP}:{_EVENT_AGGREGATOR_AGENT_TARGET_PORT}"
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def httpserver_listen_address():
|
|
return ("127.0.0.1", _EVENT_AGGREGATOR_AGENT_TARGET_PORT)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster_head_with_env_vars",
|
|
[
|
|
{
|
|
"env_vars": {
|
|
"RAY_DASHBOARD_AGGREGATOR_AGENT_MAX_EVENT_BUFFER_SIZE": 2,
|
|
"RAY_DASHBOARD_AGGREGATOR_AGENT_EVENTS_EXPORT_ADDR": _EVENT_AGGREGATOR_AGENT_TARGET_ADDR,
|
|
"RAY_DASHBOARD_AGGREGATOR_AGENT_PUBLISH_EVENTS_TO_GCS": "True",
|
|
# Turn off task events generation to avoid the task events from the
|
|
# cluster impacting the test result
|
|
"RAY_task_events_report_interval_ms": 0,
|
|
"RAY_enable_open_telemetry": "true",
|
|
},
|
|
},
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_metrics_export_event_aggregator_agent(
|
|
ray_start_cluster_head_with_env_vars, httpserver
|
|
):
|
|
cluster = ray_start_cluster_head_with_env_vars
|
|
stub = get_event_aggregator_grpc_stub(
|
|
cluster.gcs_address, cluster.head_node.node_id
|
|
)
|
|
httpserver.expect_request("/", method="POST").respond_with_data("", status=200)
|
|
|
|
metrics_export_port = cluster.head_node.metrics_export_port
|
|
addr = cluster.head_node.node_ip_address
|
|
prom_addresses = [build_address(addr, metrics_export_port)]
|
|
timeseries = PrometheusTimeseries()
|
|
|
|
def test_case_stats_exist():
|
|
fetch_prometheus_timeseries(prom_addresses, timeseries)
|
|
metric_descriptors = timeseries.metric_descriptors
|
|
metrics_names = metric_descriptors.keys()
|
|
event_aggregator_metrics = [
|
|
"ray_aggregator_agent_events_received_total",
|
|
"ray_aggregator_agent_published_events_total",
|
|
"ray_aggregator_agent_filtered_events_total",
|
|
"ray_aggregator_agent_queue_dropped_events_total",
|
|
"ray_aggregator_agent_consecutive_failures_since_last_success",
|
|
"ray_aggregator_agent_time_since_last_success_seconds",
|
|
"ray_aggregator_agent_publish_latency_seconds_bucket",
|
|
"ray_aggregator_agent_publish_latency_seconds_count",
|
|
"ray_aggregator_agent_publish_latency_seconds_sum",
|
|
]
|
|
return all(metric in metrics_names for metric in event_aggregator_metrics)
|
|
|
|
def test_case_value_correct():
|
|
fetch_prometheus_timeseries(prom_addresses, timeseries)
|
|
metric_samples = timeseries.metric_samples.values()
|
|
expected_metrics_values = {
|
|
"ray_aggregator_agent_events_received_total": 3.0,
|
|
}
|
|
for descriptor, expected_value in expected_metrics_values.items():
|
|
samples = [m for m in metric_samples if m.name == descriptor]
|
|
if not samples:
|
|
return False
|
|
if samples[0].value != expected_value:
|
|
return False
|
|
return True
|
|
|
|
def test_case_publisher_specific_metrics_value_correct(
|
|
consumer_name: str, expected_metrics_values: dict
|
|
):
|
|
fetch_prometheus_timeseries(prom_addresses, timeseries)
|
|
metric_samples = timeseries.metric_samples.values()
|
|
for descriptor, expected_value in expected_metrics_values.items():
|
|
samples = [
|
|
m
|
|
for m in metric_samples
|
|
if m.name == descriptor and m.labels[CONSUMER_TAG_KEY] == consumer_name
|
|
]
|
|
if not samples:
|
|
return False
|
|
if samples[0].value != expected_value:
|
|
return False
|
|
return True
|
|
|
|
now = time.time_ns()
|
|
seconds, nanos = divmod(now, 10**9)
|
|
timestamp = Timestamp(seconds=seconds, nanos=nanos)
|
|
job_id = JobID.from_int(1)
|
|
valid_task_id_bytes = TaskID.for_fake_task(job_id).binary()
|
|
request = AddEventsRequest(
|
|
events_data=RayEventsData(
|
|
events=[
|
|
RayEvent(
|
|
event_id=b"1",
|
|
source_type=RayEvent.SourceType.CORE_WORKER,
|
|
event_type=RayEvent.EventType.TASK_DEFINITION_EVENT,
|
|
timestamp=timestamp,
|
|
severity=RayEvent.Severity.INFO,
|
|
message="hello",
|
|
),
|
|
RayEvent(
|
|
event_id=b"2",
|
|
source_type=RayEvent.SourceType.CORE_WORKER,
|
|
event_type=RayEvent.EventType.TASK_PROFILE_EVENT,
|
|
timestamp=timestamp,
|
|
severity=RayEvent.Severity.INFO,
|
|
message="hello 2",
|
|
),
|
|
RayEvent(
|
|
event_id=b"3",
|
|
source_type=RayEvent.SourceType.CORE_WORKER,
|
|
event_type=RayEvent.EventType.TASK_DEFINITION_EVENT,
|
|
timestamp=timestamp,
|
|
severity=RayEvent.Severity.INFO,
|
|
message="hello 3",
|
|
),
|
|
],
|
|
task_events_metadata=TaskEventsMetadata(
|
|
dropped_task_attempts=[
|
|
TaskAttempt(
|
|
task_id=valid_task_id_bytes,
|
|
attempt_number=1,
|
|
),
|
|
],
|
|
),
|
|
)
|
|
)
|
|
|
|
stub.AddEvents(request)
|
|
wait_for_condition(lambda: len(httpserver.log) == 1)
|
|
|
|
wait_for_condition(test_case_stats_exist, timeout=30, retry_interval_ms=1000)
|
|
|
|
wait_for_condition(test_case_value_correct, timeout=30, retry_interval_ms=1000)
|
|
|
|
expected_http_publisher_metrics_values = {
|
|
"ray_aggregator_agent_published_events_total": 1.0,
|
|
"ray_aggregator_agent_filtered_events_total": 1.0,
|
|
"ray_aggregator_agent_queue_dropped_events_total": 1.0,
|
|
}
|
|
wait_for_condition(
|
|
lambda: test_case_publisher_specific_metrics_value_correct(
|
|
"http_service", expected_http_publisher_metrics_values
|
|
),
|
|
timeout=30,
|
|
retry_interval_ms=1000,
|
|
)
|
|
|
|
expected_gcs_publisher_metrics_values = {
|
|
"ray_aggregator_agent_published_events_total": 2.0,
|
|
"ray_aggregator_agent_queue_dropped_events_total": 1.0,
|
|
}
|
|
wait_for_condition(
|
|
lambda: test_case_publisher_specific_metrics_value_correct(
|
|
"ray_gcs", expected_gcs_publisher_metrics_values
|
|
),
|
|
timeout=30,
|
|
retry_interval_ms=1000,
|
|
)
|
|
|
|
|
|
def test_operation_stats(monkeypatch, shutdown_only):
|
|
# Test operation stats are available when flag is on.
|
|
operation_metrics = [
|
|
"ray_operation_count_total",
|
|
"ray_operation_run_time_ms_bucket",
|
|
"ray_operation_queue_time_ms_bucket",
|
|
"ray_operation_active_count",
|
|
]
|
|
|
|
monkeypatch.setenv("RAY_emit_main_service_metrics", "1")
|
|
timeseries = PrometheusTimeseries()
|
|
addr = ray.init()
|
|
remote_signal = SignalActor.remote()
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def __init__(self, signal):
|
|
self.signal = signal
|
|
|
|
def get_worker_id(self):
|
|
return ray.get_runtime_context().get_worker_id()
|
|
|
|
def wait(self):
|
|
ray.get(self.signal.wait.remote())
|
|
|
|
actor = Actor.remote(remote_signal)
|
|
ray.get(actor.get_worker_id.remote())
|
|
obj_ref = actor.wait.remote()
|
|
|
|
ray.get(remote_signal.send.remote())
|
|
ray.get(obj_ref)
|
|
|
|
def verify():
|
|
metrics = raw_metric_timeseries(addr, timeseries)
|
|
|
|
samples = metrics["ray_operation_active_count"]
|
|
found = False
|
|
for sample in samples:
|
|
if (
|
|
sample.labels["Name"] == "gcs_server_main_io_context"
|
|
and sample.labels["Component"] == "gcs_server"
|
|
):
|
|
found = True
|
|
if not found:
|
|
return False
|
|
|
|
found = False
|
|
for sample in samples:
|
|
if (
|
|
sample.labels["Name"] == "raylet_main_io_context"
|
|
and sample.labels["Component"] == "raylet"
|
|
):
|
|
found = True
|
|
if not found:
|
|
return False
|
|
|
|
metric_names = set(metrics.keys())
|
|
for op_metric in operation_metrics:
|
|
assert op_metric in metric_names
|
|
samples = metrics[op_metric]
|
|
components = set()
|
|
print(components)
|
|
for sample in samples:
|
|
components.add(sample.labels["Component"])
|
|
assert {"raylet", "gcs_server"} == components
|
|
return True
|
|
|
|
wait_for_condition(verify, timeout=30)
|
|
|
|
|
|
@pytest.mark.skipif(prometheus_client is None, reason="Prometheus not installed")
|
|
@pytest.mark.parametrize("_setup_cluster_for_test", [True], indirect=True)
|
|
def test_histogram(_setup_cluster_for_test):
|
|
TEST_TIMEOUT_S = 30
|
|
(
|
|
prom_addresses,
|
|
autoscaler_export_addr,
|
|
dashboard_export_addr,
|
|
) = _setup_cluster_for_test
|
|
timeseries = PrometheusTimeseries()
|
|
|
|
def test_cases():
|
|
fetch_prometheus_timeseries(prom_addresses, timeseries)
|
|
metric_descriptors = timeseries.metric_descriptors
|
|
metric_samples = timeseries.metric_samples.values()
|
|
metric_names = metric_descriptors.keys()
|
|
custom_histogram_metric_name = "ray_test_histogram_bucket"
|
|
assert custom_histogram_metric_name in metric_names
|
|
assert metric_descriptors[custom_histogram_metric_name].type == "histogram"
|
|
|
|
test_histogram_samples = [
|
|
m for m in metric_samples if "test_histogram" in m.name
|
|
]
|
|
buckets = {
|
|
m.labels["le"]: m.value
|
|
for m in test_histogram_samples
|
|
if "_bucket" in m.name
|
|
}
|
|
# In Prometheus data model
|
|
# the histogram is cumulative. So we expect the count to appear in
|
|
# <1.1 and <+Inf buckets.
|
|
assert buckets == {"0.1": 1.0, "1.6": 2.0, "+Inf": 2.0}
|
|
hist_count = [m for m in test_histogram_samples if "_count" in m.name][0].value
|
|
assert hist_count == 2
|
|
|
|
def wrap_test_case_for_retry():
|
|
try:
|
|
test_cases()
|
|
return True
|
|
except AssertionError:
|
|
return False
|
|
|
|
try:
|
|
wait_for_condition(
|
|
wrap_test_case_for_retry,
|
|
timeout=TEST_TIMEOUT_S,
|
|
retry_interval_ms=1000, # Yield resource for other processes
|
|
)
|
|
except RuntimeError:
|
|
print(f"The components are {pformat(timeseries)}")
|
|
test_cases() # Should fail assert
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Not working in Windows.")
|
|
def test_counter(monkeypatch, shutdown_only):
|
|
# Test to make sure we don't export counter as gauge
|
|
# if RAY_EXPORT_COUNTER_AS_GAUGE is 0
|
|
monkeypatch.setenv("RAY_EXPORT_COUNTER_AS_GAUGE", "0")
|
|
context = ray.init()
|
|
timeseries = PrometheusTimeseries()
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def __init__(self):
|
|
self.counter = Counter("test_counter", description="desc")
|
|
self.counter.inc(2.0)
|
|
|
|
_ = Actor.remote()
|
|
|
|
def check_metrics():
|
|
metrics_page = "localhost:{}".format(
|
|
context.address_info["metrics_export_port"]
|
|
)
|
|
fetch_prometheus_timeseries([metrics_page], timeseries)
|
|
metric_descriptors = timeseries.metric_descriptors
|
|
|
|
assert "ray_test_counter" not in metric_descriptors
|
|
assert "ray_test_counter_total" in metric_descriptors
|
|
|
|
return True
|
|
|
|
wait_for_condition(check_metrics, timeout=60)
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Not working in Windows.")
|
|
def test_per_func_name_stats(shutdown_only):
|
|
# Test operation stats are available when flag is on.
|
|
comp_metrics = [
|
|
"ray_component_cpu_percentage",
|
|
"ray_component_rss_mb",
|
|
"ray_component_rss_bytes",
|
|
"ray_component_num_fds",
|
|
]
|
|
timeseries = PrometheusTimeseries()
|
|
if sys.platform == "linux" or sys.platform == "linux2":
|
|
# Uss only available from Linux
|
|
comp_metrics.append("ray_component_uss_mb")
|
|
comp_metrics.append("ray_component_uss_bytes")
|
|
comp_metrics.append("ray_component_shared_bytes")
|
|
addr = ray.init(num_cpus=2)
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def __init__(self):
|
|
self.arr = np.random.rand(5 * 1024 * 1024) # 40 MB
|
|
self.shared_arr = ray.put(np.random.rand(5 * 1024 * 1024))
|
|
|
|
def pid(self):
|
|
return os.getpid()
|
|
|
|
@ray.remote
|
|
class ActorB:
|
|
def __init__(self):
|
|
self.arr = np.random.rand(5 * 1024 * 1024) # 40 MB
|
|
self.shared_arr = ray.put(np.random.rand(5 * 1024 * 1024))
|
|
|
|
a = Actor.remote() # noqa
|
|
b = ActorB.remote()
|
|
|
|
ray.get(a.__ray_ready__.remote())
|
|
ray.get(b.__ray_ready__.remote())
|
|
|
|
# Run a short lived task to make sure there's a ray::IDLE component.
|
|
@ray.remote
|
|
def do_nothing():
|
|
pass
|
|
|
|
ray.get(do_nothing.remote())
|
|
|
|
def verify_components():
|
|
metrics = raw_metric_timeseries(addr, timeseries)
|
|
metric_names = set(metrics.keys())
|
|
components = set()
|
|
for metric in comp_metrics:
|
|
assert metric in metric_names
|
|
samples = metrics[metric]
|
|
for sample in samples:
|
|
components.add(sample.labels["Component"])
|
|
print(components)
|
|
assert {
|
|
sys.executable, # driver process
|
|
"raylet",
|
|
"agent",
|
|
"ray::Actor",
|
|
"ray::ActorB",
|
|
"ray::IDLE",
|
|
} <= components
|
|
return True
|
|
|
|
wait_for_condition(verify_components, timeout=30)
|
|
|
|
def verify_mem_usage():
|
|
metrics = raw_metric_timeseries(addr, timeseries)
|
|
for metric in comp_metrics:
|
|
samples = metrics[metric]
|
|
for sample in samples:
|
|
if sample.labels["Component"] == "ray::ActorB":
|
|
assert sample.value > 0.0
|
|
print(sample)
|
|
print(sample.value)
|
|
if sample.labels["Component"] == "ray::Actor":
|
|
assert sample.value > 0.0
|
|
print(sample)
|
|
print(sample.value)
|
|
return True
|
|
|
|
wait_for_condition(verify_mem_usage, timeout=30)
|
|
|
|
# Verify ActorB is reported as value 0 because it is killed.
|
|
ray.kill(b)
|
|
# Kill Actor by sigkill, which happens upon OOM.
|
|
pid = ray.get(a.pid.remote())
|
|
os.kill(pid, signal.SIGKILL)
|
|
|
|
def verify_mem_cleaned():
|
|
metrics = raw_metric_timeseries(addr, timeseries)
|
|
for metric in comp_metrics:
|
|
samples = metrics[metric]
|
|
for sample in samples:
|
|
if sample.labels["Component"] == "ray::ActorB":
|
|
assert sample.value == 0.0
|
|
if sample.labels["Component"] == "ray::Actor":
|
|
assert sample.value == 0.0
|
|
return True
|
|
|
|
wait_for_condition(verify_mem_cleaned, timeout=30)
|
|
|
|
|
|
def test_prometheus_file_based_service_discovery(ray_start_cluster):
|
|
# Make sure Prometheus service discovery file is correctly written
|
|
# when number of nodes are dynamically changed.
|
|
NUM_NODES = 5
|
|
cluster = ray_start_cluster
|
|
nodes = [cluster.add_node() for _ in range(NUM_NODES)]
|
|
cluster.wait_for_nodes()
|
|
addr = ray.init(address=cluster.address)
|
|
writer = PrometheusServiceDiscoveryWriter(
|
|
addr["gcs_address"],
|
|
"/tmp/ray",
|
|
"/tmp/ray/session_latest",
|
|
)
|
|
|
|
def get_metrics_export_address_from_node(nodes):
|
|
node_export_addrs = [
|
|
build_address(node.node_ip_address, node.metrics_export_port)
|
|
for node in nodes
|
|
]
|
|
# monitor should be run on head node for `ray_start_cluster` fixture
|
|
autoscaler_export_addr = build_address(
|
|
cluster.head_node.node_ip_address, AUTOSCALER_METRIC_PORT
|
|
)
|
|
dashboard_export_addr = build_address(
|
|
cluster.head_node.node_ip_address, DASHBOARD_METRIC_PORT
|
|
)
|
|
return node_export_addrs + [autoscaler_export_addr, dashboard_export_addr]
|
|
|
|
loaded_json_data = json.loads(writer.get_file_discovery_content())
|
|
assert loaded_json_data == writer.get_latest_service_discovery_content()
|
|
assert set(get_metrics_export_address_from_node(nodes)) == set(
|
|
loaded_json_data[0]["targets"]
|
|
)
|
|
|
|
# Let's update nodes.
|
|
for _ in range(3):
|
|
nodes.append(cluster.add_node())
|
|
|
|
# Make sure service discovery file content is correctly updated.
|
|
loaded_json_data = json.loads(writer.get_file_discovery_content())
|
|
assert loaded_json_data == writer.get_latest_service_discovery_content()
|
|
assert set(get_metrics_export_address_from_node(nodes)) == set(
|
|
loaded_json_data[0]["targets"]
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Symlinks may need privileges.")
|
|
def test_prom_service_discovery_session_scoped(ray_start_cluster, tmp_path):
|
|
"""Test that the discovery file is written to session_dir and a
|
|
backward-compatible symlink is created at temp_dir."""
|
|
cluster = ray_start_cluster
|
|
cluster.add_node()
|
|
cluster.wait_for_nodes()
|
|
addr = ray.init(address=cluster.address)
|
|
|
|
temp_dir = str(tmp_path / "ray")
|
|
session_dir = str(tmp_path / "ray" / "session_test")
|
|
os.makedirs(temp_dir, exist_ok=True)
|
|
os.makedirs(session_dir, exist_ok=True)
|
|
|
|
writer = PrometheusServiceDiscoveryWriter(
|
|
addr["gcs_address"],
|
|
temp_dir,
|
|
session_dir,
|
|
)
|
|
|
|
# Verify the target file is in session_dir, not temp_dir
|
|
assert writer.get_target_file_name().startswith(session_dir)
|
|
assert writer.get_temp_file_name().startswith(session_dir)
|
|
|
|
# Write the discovery file
|
|
writer.write()
|
|
|
|
# Verify the file exists in session_dir
|
|
target_file = writer.get_target_file_name()
|
|
assert os.path.exists(target_file)
|
|
|
|
# Verify the symlink exists at the old temp_dir location
|
|
legacy_path = os.path.join(
|
|
temp_dir,
|
|
ray._private.ray_constants.PROMETHEUS_SERVICE_DISCOVERY_FILE,
|
|
)
|
|
assert os.path.islink(legacy_path)
|
|
assert os.path.realpath(legacy_path) == os.path.realpath(target_file)
|
|
|
|
# Verify the content is valid JSON and matches
|
|
with open(target_file) as f:
|
|
session_content = json.load(f)
|
|
with open(legacy_path) as f:
|
|
legacy_content = json.load(f)
|
|
assert session_content == legacy_content
|
|
|
|
|
|
def test_prome_file_discovery_run_by_dashboard(shutdown_only):
|
|
ray.init(num_cpus=0)
|
|
global_node = ray._private.worker._global_node
|
|
session_dir = global_node.get_session_dir_path()
|
|
|
|
def is_service_discovery_exist():
|
|
for path in pathlib.Path(session_dir).iterdir():
|
|
if PROMETHEUS_SERVICE_DISCOVERY_FILE in str(path):
|
|
return True
|
|
return False
|
|
|
|
wait_for_condition(is_service_discovery_exist)
|
|
|
|
|
|
@pytest.fixture
|
|
def metric_mock():
|
|
mock = MagicMock()
|
|
mock.record.return_value = "haha"
|
|
yield mock
|
|
|
|
|
|
"""
|
|
Unit test custom metrics.
|
|
"""
|
|
|
|
|
|
def test_basic_custom_metrics(metric_mock):
|
|
# Make sure each of metric works as expected.
|
|
# -- Counter --
|
|
count = Counter("count", tag_keys=("a",))
|
|
with pytest.raises(TypeError):
|
|
count.inc("hi")
|
|
with pytest.raises(ValueError):
|
|
count.inc(0)
|
|
with pytest.raises(ValueError):
|
|
count.inc(-1)
|
|
count._metric = metric_mock
|
|
count.inc(1, {"a": "1"})
|
|
metric_mock.record.assert_called_with(1, tags={"a": "1"})
|
|
|
|
# -- Gauge --
|
|
gauge = Gauge("gauge", description="gauge")
|
|
gauge._metric = metric_mock
|
|
gauge.set(4)
|
|
metric_mock.record.assert_called_with(4, tags={})
|
|
|
|
# -- Histogram
|
|
histogram = Histogram(
|
|
"hist", description="hist", boundaries=[1.0, 3.0], tag_keys=("a", "b")
|
|
)
|
|
histogram._metric = metric_mock
|
|
tags = {"a": "10", "b": "b"}
|
|
histogram.observe(8, tags=tags)
|
|
metric_mock.record.assert_called_with(8, tags=tags)
|
|
|
|
|
|
def test_custom_metrics_with_extra_tags(metric_mock):
|
|
base_tags = {"a": "1"}
|
|
extra_tags = {"a": "1", "b": "2"}
|
|
|
|
# -- Counter --
|
|
count = Counter("count", tag_keys=("a",))
|
|
with pytest.raises(ValueError):
|
|
count.inc(1)
|
|
|
|
count._metric = metric_mock
|
|
|
|
# Increment with base tags
|
|
count.inc(1, tags=base_tags)
|
|
metric_mock.record.assert_called_with(1, tags=base_tags)
|
|
metric_mock.reset_mock()
|
|
|
|
# Increment with extra tags
|
|
count.inc(1, tags=extra_tags)
|
|
metric_mock.record.assert_called_with(1, tags=extra_tags)
|
|
metric_mock.reset_mock()
|
|
|
|
# -- Gauge --
|
|
gauge = Gauge("gauge", description="gauge", tag_keys=("a",))
|
|
gauge._metric = metric_mock
|
|
|
|
# Record with base tags
|
|
gauge.set(4, tags=base_tags)
|
|
metric_mock.record.assert_called_with(4, tags=base_tags)
|
|
metric_mock.reset_mock()
|
|
|
|
# Record with extra tags
|
|
gauge.set(4, tags=extra_tags)
|
|
metric_mock.record.assert_called_with(4, tags=extra_tags)
|
|
metric_mock.reset_mock()
|
|
|
|
# -- Histogram
|
|
histogram = Histogram(
|
|
"hist", description="hist", boundaries=[1.0, 3.0], tag_keys=("a",)
|
|
)
|
|
histogram._metric = metric_mock
|
|
|
|
# Record with base tags
|
|
histogram.observe(8, tags=base_tags)
|
|
metric_mock.record.assert_called_with(8, tags=base_tags)
|
|
metric_mock.reset_mock()
|
|
|
|
# Record with extra tags
|
|
histogram.observe(8, tags=extra_tags)
|
|
metric_mock.record.assert_called_with(8, tags=extra_tags)
|
|
metric_mock.reset_mock()
|
|
|
|
|
|
def test_custom_metrics_info(metric_mock):
|
|
# Make sure .info public method works.
|
|
histogram = Histogram(
|
|
"hist", description="hist", boundaries=[1.0, 2.0], tag_keys=("a", "b")
|
|
)
|
|
assert histogram.info["name"] == "hist"
|
|
assert histogram.info["description"] == "hist"
|
|
assert histogram.info["boundaries"] == [1.0, 2.0]
|
|
assert histogram.info["tag_keys"] == ("a", "b")
|
|
assert histogram.info["default_tags"] == {}
|
|
histogram.set_default_tags({"a": "a"})
|
|
assert histogram.info["default_tags"] == {"a": "a"}
|
|
|
|
|
|
def test_custom_metrics_default_tags(metric_mock):
|
|
histogram = Histogram(
|
|
"hist", description="hist", boundaries=[1.0, 2.0], tag_keys=("a", "b")
|
|
).set_default_tags({"b": "b"})
|
|
histogram._metric = metric_mock
|
|
|
|
# Check specifying non-default tags.
|
|
histogram.observe(10, tags={"a": "a"})
|
|
metric_mock.record.assert_called_with(10, tags={"a": "a", "b": "b"})
|
|
|
|
# Check overriding default tags.
|
|
tags = {"a": "10", "b": "c"}
|
|
histogram.observe(8, tags=tags)
|
|
metric_mock.record.assert_called_with(8, tags=tags)
|
|
|
|
|
|
def test_custom_metrics_edge_cases(metric_mock):
|
|
# None or empty boundaries are not allowed.
|
|
with pytest.raises(ValueError):
|
|
Histogram("hist")
|
|
|
|
with pytest.raises(ValueError):
|
|
Histogram("hist", boundaries=[])
|
|
|
|
# Empty name is not allowed.
|
|
with pytest.raises(ValueError):
|
|
Counter("")
|
|
|
|
# The tag keys must be a tuple type.
|
|
with pytest.raises(TypeError):
|
|
Counter("name", tag_keys=("a"))
|
|
|
|
with pytest.raises(ValueError):
|
|
Histogram("hist", boundaries=[-1, 1, 2])
|
|
|
|
with pytest.raises(ValueError):
|
|
Histogram("hist", boundaries=[0, 1, 2])
|
|
|
|
with pytest.raises(ValueError):
|
|
Histogram("hist", boundaries=[-1, -0.5, -0.1])
|
|
|
|
|
|
def test_metrics_override_shouldnt_warn(ray_start_regular, log_pubsub):
|
|
# https://github.com/ray-project/ray/issues/12859
|
|
|
|
@ray.remote
|
|
def override():
|
|
a = Counter("num_count", description="")
|
|
b = Counter("num_count", description="")
|
|
a.inc(1)
|
|
b.inc(1)
|
|
|
|
ray.get(override.remote())
|
|
|
|
# Check the stderr from the worker.
|
|
def matcher(log_batch):
|
|
return any("Attempt to register measure" in line for line in log_batch["lines"])
|
|
|
|
match = get_log_batch(log_pubsub, 1, timeout=5, matcher=matcher)
|
|
assert len(match) == 0, match
|
|
|
|
|
|
def test_custom_metrics_validation(shutdown_only):
|
|
ray.init()
|
|
# Missing tag(s) from tag_keys.
|
|
metric = Counter("name", tag_keys=("a", "b"))
|
|
metric.set_default_tags({"a": "1"})
|
|
|
|
metric.inc(1.0, {"b": "2"})
|
|
metric.inc(1.0, {"a": "1", "b": "2"})
|
|
|
|
with pytest.raises(ValueError):
|
|
metric.inc(1.0)
|
|
|
|
with pytest.raises(ValueError):
|
|
metric.inc(1.0, {"a": "2"})
|
|
|
|
# tag_keys must be tuple.
|
|
with pytest.raises(TypeError):
|
|
Counter("name", tag_keys="a")
|
|
# tag_keys must be strs.
|
|
with pytest.raises(TypeError):
|
|
Counter("name", tag_keys=(1,))
|
|
|
|
metric = Counter("name", tag_keys=("a",))
|
|
# Set default tag that isn't in tag_keys.
|
|
with pytest.raises(ValueError):
|
|
metric.set_default_tags({"a": "1", "c": "2"})
|
|
# Default tag value must be str.
|
|
with pytest.raises(TypeError):
|
|
metric.set_default_tags({"a": 1})
|
|
# Tag value must be str.
|
|
with pytest.raises(TypeError):
|
|
metric.inc(1.0, {"a": 1})
|
|
|
|
|
|
@pytest.mark.parametrize("_setup_cluster_for_test", [False], indirect=True)
|
|
def test_metrics_disablement(_setup_cluster_for_test):
|
|
"""Make sure the metrics are not exported when it is disabled."""
|
|
prom_addresses, _, _ = _setup_cluster_for_test
|
|
# When metrics are disabled, prom_addresses should be empty
|
|
assert len(prom_addresses) == 0, (
|
|
f"Expected no prometheus addresses when metrics disabled, "
|
|
f"but got {prom_addresses}"
|
|
)
|
|
|
|
|
|
_FAULTY_METRIC_REGEX = re.compile(".*Invalid metric name.*")
|
|
|
|
|
|
def test_invalid_application_metric_names():
|
|
warnings.simplefilter("always")
|
|
with pytest.raises(
|
|
ValueError, match="Empty name is not allowed. Please provide a metric name."
|
|
):
|
|
Metric("")
|
|
with pytest.warns(UserWarning, match=_FAULTY_METRIC_REGEX):
|
|
Metric("name-cannot-have-dashes")
|
|
with pytest.warns(UserWarning, match=_FAULTY_METRIC_REGEX):
|
|
Metric("1namecannotstartwithnumber")
|
|
with pytest.warns(UserWarning, match=_FAULTY_METRIC_REGEX):
|
|
Metric("name.cannot.have.dots")
|
|
|
|
|
|
def test_invalid_system_metric_names(caplog):
|
|
with pytest.raises(
|
|
ValueError, match="Empty name is not allowed. Please provide a metric name."
|
|
):
|
|
MetricsAgentGauge("", "", "", [])
|
|
with pytest.raises(ValueError, match=_FAULTY_METRIC_REGEX):
|
|
MetricsAgentGauge("name-cannot-have-dashes", "", "", [])
|
|
with pytest.raises(ValueError, match=_FAULTY_METRIC_REGEX):
|
|
MetricsAgentGauge("1namecannotstartwithnumber", "", "", [])
|
|
with pytest.raises(ValueError, match=_FAULTY_METRIC_REGEX):
|
|
MetricsAgentGauge("name.cannot.have.dots", "", "", [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|