601 lines
18 KiB
Python
601 lines
18 KiB
Python
import random
|
|
import sys
|
|
import time
|
|
from typing import List
|
|
|
|
import pytest
|
|
from opencensus.metrics.export.metric_descriptor import MetricDescriptorType
|
|
from opencensus.metrics.export.value import ValueDouble
|
|
from opencensus.stats import execution_context
|
|
from opencensus.stats.aggregation_data import (
|
|
CountAggregationData,
|
|
DistributionAggregationData,
|
|
LastValueAggregationData,
|
|
SumAggregationData,
|
|
)
|
|
from opencensus.stats.stats_recorder import StatsRecorder
|
|
from opencensus.stats.view_manager import ViewManager
|
|
from prometheus_client.core import REGISTRY
|
|
|
|
import ray._private.prometheus_exporter as prometheus_exporter
|
|
from ray._common.test_utils import (
|
|
fetch_prometheus_metrics,
|
|
fetch_raw_prometheus,
|
|
wait_for_condition,
|
|
)
|
|
from ray._private.metrics_agent import (
|
|
RAY_WORKER_TIMEOUT_S,
|
|
Gauge,
|
|
MetricsAgent,
|
|
OpenCensusProxyCollector,
|
|
OpencensusProxyMetric,
|
|
Record,
|
|
)
|
|
from ray._private.telemetry.metric_cardinality import WORKER_ID_TAG_KEY
|
|
from ray._raylet import WorkerID
|
|
from ray.core.generated.metrics_pb2 import (
|
|
LabelKey,
|
|
LabelValue,
|
|
Metric,
|
|
MetricDescriptor,
|
|
Point,
|
|
TimeSeries,
|
|
)
|
|
|
|
|
|
def raw_metrics(export_port):
|
|
metrics_page = "localhost:{}".format(export_port)
|
|
res = fetch_prometheus_metrics([metrics_page])
|
|
return res
|
|
|
|
|
|
def get_metric(metric_name, export_port):
|
|
res = raw_metrics(export_port)
|
|
for name, samples in res.items():
|
|
if name == metric_name:
|
|
return name, samples
|
|
|
|
return None
|
|
|
|
|
|
def get_prom_metric_name(namespace, metric_name):
|
|
return f"{namespace}_{metric_name}"
|
|
|
|
|
|
def generate_timeseries(label_values: List[str], points: List[float]):
|
|
return TimeSeries(
|
|
label_values=[LabelValue(value=val) for val in label_values],
|
|
points=[Point(double_value=val) for val in points],
|
|
)
|
|
|
|
|
|
def generate_protobuf_metric(
|
|
name: str,
|
|
desc: str,
|
|
unit: str,
|
|
type: MetricDescriptorType,
|
|
label_keys: List[str] = None,
|
|
timeseries: List[TimeSeries] = None,
|
|
):
|
|
if not label_keys:
|
|
label_keys = []
|
|
if not timeseries:
|
|
timeseries = []
|
|
|
|
return Metric(
|
|
metric_descriptor=MetricDescriptor(
|
|
name=name,
|
|
description=desc,
|
|
unit=unit,
|
|
type=type,
|
|
label_keys=[LabelKey(key="a"), LabelKey(key="b")],
|
|
),
|
|
timeseries=timeseries,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def get_agent(request, monkeypatch):
|
|
with monkeypatch.context() as m:
|
|
if hasattr(request, "param"):
|
|
delay = request.param
|
|
else:
|
|
delay = 0
|
|
|
|
m.setenv(RAY_WORKER_TIMEOUT_S, delay)
|
|
agent_port = random.randint(10000, 65535)
|
|
stats_recorder = StatsRecorder()
|
|
view_manager = ViewManager()
|
|
stats_exporter = prometheus_exporter.new_stats_exporter(
|
|
prometheus_exporter.Options(
|
|
namespace="test",
|
|
port=agent_port,
|
|
address="127.0.0.1",
|
|
)
|
|
)
|
|
agent = MetricsAgent(view_manager, stats_recorder, stats_exporter)
|
|
REGISTRY.register(agent.proxy_exporter_collector)
|
|
yield agent, agent_port
|
|
REGISTRY.unregister(agent.stats_exporter.collector)
|
|
REGISTRY.unregister(agent.proxy_exporter_collector)
|
|
execution_context.set_measure_to_view_map({})
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Flaky on Windows.")
|
|
def test_metrics_agent_record_and_export(get_agent):
|
|
namespace = "test"
|
|
agent, agent_port = get_agent
|
|
|
|
# Record a new gauge.
|
|
metric_name = "test"
|
|
test_gauge = Gauge(metric_name, "desc", "unit", ["tag"])
|
|
record_a = Record(
|
|
gauge=test_gauge,
|
|
value=3,
|
|
tags={"tag": "a"},
|
|
)
|
|
agent.record_and_export([record_a])
|
|
name, samples = get_metric(get_prom_metric_name(namespace, metric_name), agent_port)
|
|
assert name == get_prom_metric_name(namespace, metric_name)
|
|
assert len(samples) == 1
|
|
assert samples[0].value == 3
|
|
assert samples[0].labels == {"tag": "a"}
|
|
|
|
# Record the same gauge.
|
|
record_b = Record(
|
|
gauge=test_gauge,
|
|
value=4,
|
|
tags={"tag": "a"},
|
|
)
|
|
record_c = Record(
|
|
gauge=test_gauge,
|
|
value=4,
|
|
tags={"tag": "a"},
|
|
)
|
|
agent.record_and_export([record_b, record_c])
|
|
name, samples = get_metric(get_prom_metric_name(namespace, metric_name), agent_port)
|
|
assert name == get_prom_metric_name(namespace, metric_name)
|
|
assert len(samples) == 1
|
|
assert samples[0].value == 4
|
|
assert samples[0].labels == {"tag": "a"}
|
|
|
|
# Record the same gauge with different ag.
|
|
record_d = Record(
|
|
gauge=test_gauge,
|
|
value=6,
|
|
tags={"tag": "aa"},
|
|
)
|
|
agent.record_and_export(
|
|
[
|
|
record_d,
|
|
]
|
|
)
|
|
name, samples = get_metric(get_prom_metric_name(namespace, metric_name), agent_port)
|
|
assert name == get_prom_metric_name(namespace, metric_name)
|
|
assert len(samples) == 2
|
|
assert samples[0].value == 4
|
|
assert samples[0].labels == {"tag": "a"}
|
|
assert samples[1].value == 6
|
|
assert samples[1].labels == {"tag": "aa"}
|
|
|
|
# Record more than 1 gauge.
|
|
metric_name_2 = "test2"
|
|
test_gauge_2 = Gauge(metric_name_2, "desc", "unit", ["tag"])
|
|
record_e = Record(
|
|
gauge=test_gauge_2,
|
|
value=1,
|
|
tags={"tag": "b"},
|
|
)
|
|
agent.record_and_export([record_e])
|
|
name, samples = get_metric(
|
|
get_prom_metric_name(namespace, metric_name_2), agent_port
|
|
)
|
|
|
|
assert name == get_prom_metric_name(namespace, metric_name_2)
|
|
assert samples[0].value == 1
|
|
assert samples[0].labels == {"tag": "b"}
|
|
|
|
# Make sure the previous record is still there.
|
|
name, samples = get_metric(get_prom_metric_name(namespace, metric_name), agent_port)
|
|
assert name == get_prom_metric_name(namespace, metric_name)
|
|
assert len(samples) == 2
|
|
assert samples[0].value == 4
|
|
assert samples[0].labels == {"tag": "a"}
|
|
assert samples[1].value == 6
|
|
assert samples[1].labels == {"tag": "aa"}
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Flaky on Windows.")
|
|
def test_metrics_agent_record_and_export_failed_records_dont_block_other_records(
|
|
get_agent,
|
|
capsys,
|
|
):
|
|
namespace = "test"
|
|
agent, agent_port = get_agent
|
|
|
|
metric_name = "test"
|
|
test_gauge = Gauge(metric_name, "desc", "unit", ["tag"])
|
|
record_a = Record(
|
|
gauge=test_gauge,
|
|
value=1,
|
|
tags={"tag": "a"},
|
|
)
|
|
record_b = Record(
|
|
gauge=test_gauge,
|
|
value=1,
|
|
# this tag is much too long (>255 characters), so recording this metric will fail
|
|
tags={"tag": "b" * 1000},
|
|
)
|
|
record_c = Record(
|
|
gauge=test_gauge,
|
|
value=1,
|
|
tags={"tag": "c"},
|
|
)
|
|
agent.record_and_export([record_a, record_b, record_c])
|
|
|
|
name, samples = get_metric(get_prom_metric_name(namespace, metric_name), agent_port)
|
|
assert name == get_prom_metric_name(namespace, metric_name)
|
|
|
|
# a and c should be recorded, b's failure should be ignored
|
|
assert {sample.labels["tag"] for sample in samples} == {"a", "c"}
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Flaky on Windows.")
|
|
def test_metrics_agent_proxy_record_and_export_basic(get_agent):
|
|
"""Test the case the metrics are exported without worker_id."""
|
|
namespace = "test"
|
|
agent, agent_port = get_agent
|
|
|
|
# Test the basic case.
|
|
m = generate_protobuf_metric(
|
|
"test",
|
|
"desc",
|
|
"",
|
|
MetricDescriptorType.GAUGE_DOUBLE,
|
|
label_keys=["a", "b"],
|
|
timeseries=[],
|
|
)
|
|
m.timeseries.append(generate_timeseries(["a", "b"], [1, 2, 3]))
|
|
agent.proxy_export_metrics([m])
|
|
name, samples = get_metric(f"{namespace}_test", agent_port)
|
|
assert name == f"{namespace}_test"
|
|
assert len(samples) == 1
|
|
assert samples[0].labels == {"a": "a", "b": "b"}
|
|
assert samples[0].value == 3
|
|
|
|
# Test new metric has proxyed.
|
|
m = generate_protobuf_metric(
|
|
"test",
|
|
"desc",
|
|
"",
|
|
MetricDescriptorType.GAUGE_DOUBLE,
|
|
label_keys=["a", "b"],
|
|
timeseries=[],
|
|
)
|
|
m.timeseries.append(generate_timeseries(["a", "b"], [4]))
|
|
agent.proxy_export_metrics([m])
|
|
name, samples = get_metric(f"{namespace}_test", agent_port)
|
|
assert name == f"{namespace}_test"
|
|
assert len(samples) == 1
|
|
assert samples[0].labels == {"a": "a", "b": "b"}
|
|
assert samples[0].value == 4
|
|
|
|
# Test new metric with different tag is reported.
|
|
m = generate_protobuf_metric(
|
|
"test",
|
|
"desc",
|
|
"",
|
|
MetricDescriptorType.GAUGE_DOUBLE,
|
|
label_keys=["a", "b"],
|
|
timeseries=[],
|
|
)
|
|
m.timeseries.append(generate_timeseries(["a", "c"], [5]))
|
|
agent.proxy_export_metrics([m])
|
|
name, samples = get_metric(f"{namespace}_test", agent_port)
|
|
assert name == f"{namespace}_test"
|
|
assert len(samples) == 2
|
|
assert samples[0].labels == {"a": "a", "b": "b"}
|
|
assert samples[0].value == 4
|
|
# Newly added metric has different tags and values.
|
|
assert samples[1].labels == {"a": "a", "b": "c"}
|
|
assert samples[1].value == 5
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Flaky on Windows.")
|
|
def test_metrics_agent_proxy_record_and_export_from_workers(get_agent):
|
|
"""
|
|
Test the basic worker death case.
|
|
"""
|
|
namespace = "test"
|
|
agent, agent_port = get_agent
|
|
worker_id = WorkerID.from_random()
|
|
|
|
m = generate_protobuf_metric(
|
|
"test",
|
|
"desc",
|
|
"",
|
|
MetricDescriptorType.GAUGE_DOUBLE,
|
|
label_keys=["a", "b"],
|
|
timeseries=[],
|
|
)
|
|
m.timeseries.append(generate_timeseries(["a", "b"], [1, 2, 3]))
|
|
agent.proxy_export_metrics([m], worker_id_hex=worker_id.hex())
|
|
# Metrics should be exposed.
|
|
assert get_metric(f"{namespace}_test", agent_port) is not None
|
|
agent.clean_all_dead_worker_metrics()
|
|
# Once the worker is dead, metrics should be unavailble.
|
|
assert get_metric(f"{namespace}_test", agent_port) is None
|
|
# Once the worker metrics is re-reported, it is treated as alive again.
|
|
agent.proxy_export_metrics([m], worker_id_hex=worker_id.hex())
|
|
assert get_metric(f"{namespace}_test", agent_port) is not None
|
|
# Clean it again and the worker metrics is cleaned again.
|
|
agent.clean_all_dead_worker_metrics()
|
|
assert get_metric(f"{namespace}_test", agent_port) is None
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Flaky on Windows.")
|
|
def test_metrics_agent_proxy_record_and_export_from_workers_complicated(
|
|
get_agent,
|
|
): # noqa
|
|
"""
|
|
Test the complicated worker death case.
|
|
"""
|
|
namespace = "test"
|
|
agent, agent_port = get_agent
|
|
|
|
# Each worker will report 2 metrics.
|
|
# i.e.,
|
|
# worker 1 => test_1, test_2.
|
|
# worker 2 => test_3, test_4.
|
|
# ...
|
|
worker_ids = [WorkerID.from_random() for _ in range(4)]
|
|
|
|
metrics = []
|
|
for i in range(8):
|
|
m = generate_protobuf_metric(
|
|
f"test_{i}",
|
|
"desc",
|
|
"",
|
|
MetricDescriptorType.GAUGE_DOUBLE,
|
|
label_keys=["a", "b"],
|
|
timeseries=[],
|
|
)
|
|
m.timeseries.append(generate_timeseries(["a", str(i)], [3]))
|
|
metrics.append(m)
|
|
|
|
i = 0
|
|
for worker_id in worker_ids:
|
|
agent.proxy_export_metrics(
|
|
[metrics[i], metrics[i + 1]], worker_id_hex=worker_id.hex()
|
|
)
|
|
i += 2
|
|
|
|
# All metrics must be available.
|
|
for i in range(len(metrics)):
|
|
assert get_metric(f"{namespace}_test_{i}", agent_port) is not None
|
|
|
|
# Mark the worker as dead and make sure metrics are properly cleaned.
|
|
i = 0
|
|
|
|
while len(worker_ids):
|
|
for worker_id in worker_ids:
|
|
agent.clean_all_dead_worker_metrics()
|
|
assert get_metric(f"{namespace}_test_{i}", agent_port) is None
|
|
assert get_metric(f"{namespace}_test_{i+1}", agent_port) is None
|
|
|
|
worker_ids.pop(0)
|
|
metrics.pop(0)
|
|
metrics.pop(0)
|
|
|
|
i = 0
|
|
for worker_id in worker_ids:
|
|
agent.proxy_export_metrics(
|
|
[metrics[i], metrics[i + 1]], worker_id_hex=worker_id.hex()
|
|
)
|
|
i += 2
|
|
|
|
# Make sure the rest of metrics are still there because new metrics
|
|
# are reported.
|
|
for j in range(i + 2, len(metrics)):
|
|
assert get_metric(f"{namespace}_test_{j}", agent_port) is not None, j
|
|
i += 2
|
|
|
|
|
|
DELAY = 3
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Flaky on Windows.")
|
|
@pytest.mark.parametrize("get_agent", [DELAY], indirect=True)
|
|
def test_metrics_agent_proxy_record_and_export_from_workers_delay(get_agent): # noqa
|
|
"""
|
|
Test the worker metrics are deleted after the delay.
|
|
"""
|
|
namespace = "test"
|
|
agent, agent_port = get_agent
|
|
worker_id = WorkerID.from_random()
|
|
|
|
m = generate_protobuf_metric(
|
|
"test",
|
|
"desc",
|
|
"",
|
|
MetricDescriptorType.GAUGE_DOUBLE,
|
|
label_keys=["a", "b"],
|
|
timeseries=[],
|
|
)
|
|
m.timeseries.append(generate_timeseries(["a", "b"], [1, 2, 3]))
|
|
agent.proxy_export_metrics([m], worker_id_hex=worker_id.hex())
|
|
agent.clean_all_dead_worker_metrics()
|
|
start = time.time()
|
|
|
|
def verify():
|
|
agent.clean_all_dead_worker_metrics()
|
|
return get_metric(f"{namespace}_test", agent_port) is None
|
|
|
|
wait_for_condition(verify)
|
|
assert time.time() - start > DELAY
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Flaky on Windows.")
|
|
def test_metrics_agent_export_format_correct(get_agent):
|
|
"""
|
|
Verifies that there is one metric per metric name and not one
|
|
per metric name + tag combination.
|
|
Also verifies that the prometheus output is in the right format.
|
|
"""
|
|
namespace = "test"
|
|
agent, agent_port = get_agent
|
|
|
|
# Record a new gauge.
|
|
metric_name = "test"
|
|
test_gauge = Gauge(metric_name, "desc", "unit", ["tag"])
|
|
record_a = Record(
|
|
gauge=test_gauge,
|
|
value=3,
|
|
tags={"tag": "a"},
|
|
)
|
|
agent.record_and_export([record_a])
|
|
|
|
# Record a different tag.
|
|
record_b = Record(
|
|
gauge=test_gauge,
|
|
value=4,
|
|
tags={"tag": "b"},
|
|
)
|
|
agent.record_and_export([record_b])
|
|
|
|
# Record more than 1 gauge.
|
|
metric_name_2 = "test2"
|
|
test_gauge_2 = Gauge(metric_name_2, "desc", "unit", ["tag"])
|
|
record_c = Record(
|
|
gauge=test_gauge_2,
|
|
value=1,
|
|
tags={"tag": "c"},
|
|
)
|
|
agent.record_and_export([record_c])
|
|
|
|
# Basic assertions
|
|
name, samples = get_metric(
|
|
get_prom_metric_name(namespace, metric_name_2), agent_port
|
|
)
|
|
assert name == get_prom_metric_name(namespace, metric_name_2)
|
|
assert len(samples) == 1
|
|
assert samples[0].value == 1
|
|
assert samples[0].labels == {"tag": "c"}
|
|
|
|
name, samples = get_metric(get_prom_metric_name(namespace, metric_name), agent_port)
|
|
assert name == get_prom_metric_name(namespace, metric_name)
|
|
assert len(samples) == 2
|
|
assert samples[0].value == 3
|
|
assert samples[0].labels == {"tag": "a"}
|
|
assert samples[1].value == 4
|
|
assert samples[1].labels == {"tag": "b"}
|
|
|
|
# Assert there is not multiple HELP text per metric
|
|
# Need to manually parse the prometheus output because the official
|
|
# `prometheus_client.parser` is more lenient than the actual
|
|
# specification and ignores the multiple HELP / TYPE comments.
|
|
metrics_page = "localhost:{}".format(agent_port)
|
|
_, response = list(fetch_raw_prometheus([metrics_page]))[0]
|
|
assert response.count("# HELP test_test desc") == 1
|
|
assert response.count("# TYPE test_test gauge") == 1
|
|
assert response.count("# HELP test_test2 desc") == 1
|
|
assert response.count("# TYPE test_test2 gauge") == 1
|
|
|
|
|
|
def _stub_node_level_metric(label: str, value: float) -> OpencensusProxyMetric:
|
|
metric = OpencensusProxyMetric(
|
|
name="test_metric_01",
|
|
desc="",
|
|
unit="",
|
|
label_keys=["NodeId"],
|
|
)
|
|
metric.add_data(
|
|
(label,),
|
|
LastValueAggregationData(ValueDouble, value),
|
|
)
|
|
return metric
|
|
|
|
|
|
def _stub_worker_level_metric(label: str, value: float) -> OpencensusProxyMetric:
|
|
metric = OpencensusProxyMetric(
|
|
name="test_metric_01",
|
|
desc="",
|
|
unit="",
|
|
label_keys=["NodeId", WORKER_ID_TAG_KEY],
|
|
)
|
|
metric.add_data(
|
|
(label, "worker_01"),
|
|
LastValueAggregationData(ValueDouble, value),
|
|
)
|
|
return metric
|
|
|
|
|
|
def test_aggregate_metric_data():
|
|
collector = OpenCensusProxyCollector("")
|
|
collector._aggregate_metric_data(
|
|
[
|
|
LastValueAggregationData(ValueDouble, 1.0),
|
|
LastValueAggregationData(ValueDouble, 2.0),
|
|
LastValueAggregationData(ValueDouble, 3.0),
|
|
]
|
|
).value == 6.0
|
|
collector._aggregate_metric_data(
|
|
[
|
|
SumAggregationData(ValueDouble, 1.0),
|
|
SumAggregationData(ValueDouble, 4.0),
|
|
]
|
|
).sum_data == 5.0
|
|
collector._aggregate_metric_data(
|
|
[
|
|
CountAggregationData(1),
|
|
CountAggregationData(1),
|
|
]
|
|
).count_data == 2
|
|
with pytest.raises(ValueError, match="Unsupported aggregation type"):
|
|
collector._aggregate_metric_data(
|
|
[
|
|
DistributionAggregationData(
|
|
mean_data=1.0,
|
|
count_data=1,
|
|
sum_of_sqd_deviations=1.0,
|
|
)
|
|
]
|
|
)
|
|
|
|
|
|
def test_collect_worker_metrics_with_recommended_cardinality():
|
|
aggregated_metrics = OpenCensusProxyCollector(
|
|
""
|
|
)._aggregate_with_recommended_cardinality(
|
|
[
|
|
_stub_worker_level_metric("node_01", 1.0),
|
|
_stub_worker_level_metric("node_01", 3.0),
|
|
_stub_worker_level_metric("node_02", 2.0),
|
|
]
|
|
)
|
|
assert len(aggregated_metrics) == 1
|
|
metric = aggregated_metrics[0]
|
|
assert metric.name == "test_metric_01"
|
|
assert metric.label_keys == ["NodeId"]
|
|
# Check that the worker id is removed from the label keys, and the correct metric
|
|
# values are returned.
|
|
assert metric._data.get(("node_01",)).value == 4.0
|
|
assert metric._data.get(("node_02",)).value == 2.0
|
|
|
|
|
|
def test_collect_node_metrics_with_recommended_cardinality():
|
|
aggregated_metrics = OpenCensusProxyCollector(
|
|
""
|
|
)._aggregate_with_recommended_cardinality(
|
|
[
|
|
_stub_node_level_metric("node_01", 1.0),
|
|
_stub_node_level_metric("node_02", 2.0),
|
|
]
|
|
)
|
|
# Metrics are already at node level, so they should be returned as is.
|
|
assert len(aggregated_metrics) == 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|