chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+911
View File
@@ -0,0 +1,911 @@
import json
import logging
import os
import re
import threading
import time
import traceback
from collections import defaultdict, namedtuple
from typing import Any, Dict, List, Set, Tuple, Union
from opencensus.metrics.export.metric_descriptor import MetricDescriptorType
from opencensus.metrics.export.value import ValueDouble
from opencensus.stats import aggregation, measure as measure_module
from opencensus.stats.aggregation_data import (
CountAggregationData,
DistributionAggregationData,
LastValueAggregationData,
SumAggregationData,
)
from opencensus.stats.base_exporter import StatsExporter
from opencensus.stats.stats_recorder import StatsRecorder
from opencensus.stats.view import View
from opencensus.stats.view_manager import ViewManager
from opencensus.tags import (
tag_key as tag_key_module,
tag_map as tag_map_module,
tag_value as tag_value_module,
)
from prometheus_client.core import (
CounterMetricFamily,
GaugeMetricFamily,
HistogramMetricFamily,
Metric as PrometheusMetric,
)
import ray
from ray._common.network_utils import build_address
from ray._private.ray_constants import env_bool
from ray._private.telemetry.metric_cardinality import (
WORKER_ID_TAG_KEY,
MetricCardinality,
)
from ray._raylet import GcsClient
from ray.core.generated.metrics_pb2 import Metric
from ray.util.metrics import _is_invalid_metric_name
logger = logging.getLogger(__name__)
# Env var key to decide worker timeout.
# If the worker doesn't report for more than
# this time, we treat workers as dead.
RAY_WORKER_TIMEOUT_S = "RAY_WORKER_TIMEOUT_S"
GLOBAL_COMPONENT_KEY = "CORE"
RE_NON_ALPHANUMS = re.compile(r"[^a-zA-Z0-9]")
class Gauge(View):
"""Gauge representation of opencensus view.
This class is used to collect process metrics from the reporter agent.
Cpp metrics should be collected in a different way.
"""
def __init__(self, name, description, unit, tags: List[str]):
if _is_invalid_metric_name(name):
raise ValueError(
f"Invalid metric name: {name}. Metric will be discarded "
"and data will not be collected or published. "
"Metric names can only contain letters, numbers, _, and :. "
"Metric names cannot start with numbers."
)
self._measure = measure_module.MeasureInt(name, description, unit)
self._description = description
tags = [tag_key_module.TagKey(tag) for tag in tags]
self._view = View(
name, description, tags, self.measure, aggregation.LastValueAggregation()
)
@property
def measure(self):
return self._measure
@property
def view(self):
return self._view
@property
def name(self):
return self.measure.name
@property
def description(self):
return self._description
Record = namedtuple("Record", ["gauge", "value", "tags"])
def fix_grpc_metric(metric: Metric):
"""
Fix the inbound `opencensus.proto.metrics.v1.Metric` protos to make it acceptable
by opencensus.stats.DistributionAggregationData.
- metric name: gRPC OpenCensus metrics have names with slashes and dots, e.g.
`grpc.io/client/server_latency`[1]. However Prometheus metric names only take
alphanums,underscores and colons[2]. We santinize the name by replacing non-alphanum
chars to underscore, like the official opencensus prometheus exporter[3].
- distribution bucket bounds: The Metric proto asks distribution bucket bounds to
be > 0 [4]. However, gRPC OpenCensus metrics have their first bucket bound == 0 [1].
This makes the `DistributionAggregationData` constructor to raise Exceptions. This
applies to all bytes and milliseconds (latencies). The fix: we update the initial 0
bounds to be 0.000_000_1. This will not affect the precision of the metrics, since
we don't expect any less-than-1 bytes, or less-than-1-nanosecond times.
[1] https://github.com/census-instrumentation/opencensus-specs/blob/master/stats/gRPC.md#units # noqa: E501
[2] https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
[3] https://github.com/census-instrumentation/opencensus-cpp/blob/50eb5de762e5f87e206c011a4f930adb1a1775b1/opencensus/exporters/stats/prometheus/internal/prometheus_utils.cc#L39 # noqa: E501
[4] https://github.com/census-instrumentation/opencensus-proto/blob/master/src/opencensus/proto/metrics/v1/metrics.proto#L218 # noqa: E501
"""
if not metric.metric_descriptor.name.startswith("grpc.io/"):
return
metric.metric_descriptor.name = RE_NON_ALPHANUMS.sub(
"_", metric.metric_descriptor.name
)
for series in metric.timeseries:
for point in series.points:
if point.HasField("distribution_value"):
dist_value = point.distribution_value
bucket_bounds = dist_value.bucket_options.explicit.bounds
if len(bucket_bounds) > 0 and bucket_bounds[0] == 0:
bucket_bounds[0] = 0.000_000_1
class OpencensusProxyMetric:
def __init__(self, name: str, desc: str, unit: str, label_keys: List[str]):
"""Represents the OpenCensus metrics that will be proxy exported."""
self._name = name
self._desc = desc
self._unit = unit
# -- The label keys of the metric --
self._label_keys = label_keys
# -- The data that needs to be proxy exported --
# tuple of label values -> data (OpenCesnsus Aggregation data)
self._data = {}
@property
def name(self):
return self._name
@property
def desc(self):
return self._desc
@property
def unit(self):
return self._unit
@property
def label_keys(self):
return self._label_keys
@property
def data(self):
return self._data
def is_distribution_aggregation_data(self):
"""Check if the metric is a distribution aggreation metric."""
return len(self._data) > 0 and isinstance(
next(iter(self._data.values())), DistributionAggregationData
)
def add_data(self, label_values: Tuple, data: Any):
"""Add the data to the metric.
Args:
label_values: The label values of the metric.
data: The data to be added.
"""
self._data[label_values] = data
def record(self, metric: Metric):
"""Parse the Opencensus Protobuf and store the data.
The data can be accessed via `data` API once recorded.
"""
timeseries = metric.timeseries
if len(timeseries) == 0:
return
# Create the aggregation and fill it in the our stats
for series in timeseries:
labels = tuple(val.value for val in series.label_values)
# Aggregate points.
for point in series.points:
if (
metric.metric_descriptor.type
== MetricDescriptorType.CUMULATIVE_INT64
):
data = CountAggregationData(point.int64_value)
elif (
metric.metric_descriptor.type
== MetricDescriptorType.CUMULATIVE_DOUBLE
):
data = SumAggregationData(ValueDouble, point.double_value)
elif metric.metric_descriptor.type == MetricDescriptorType.GAUGE_DOUBLE:
data = LastValueAggregationData(ValueDouble, point.double_value)
elif (
metric.metric_descriptor.type
== MetricDescriptorType.CUMULATIVE_DISTRIBUTION
):
dist_value = point.distribution_value
counts_per_bucket = [bucket.count for bucket in dist_value.buckets]
bucket_bounds = dist_value.bucket_options.explicit.bounds
data = DistributionAggregationData(
dist_value.sum / dist_value.count,
dist_value.count,
dist_value.sum_of_squared_deviation,
counts_per_bucket,
bucket_bounds,
)
else:
raise ValueError("Summary is not supported")
self._data[labels] = data
class Component:
def __init__(self, id: str):
"""Represent a component that requests to proxy export metrics
Args:
id: Id of this component.
"""
self.id = id
# -- The time this component reported its metrics last time --
# It is used to figure out if this component is stale.
self._last_reported_time = time.monotonic()
# -- Metrics requested to proxy export from this component --
# metrics_name (str) -> metric (OpencensusProxyMetric)
self._metrics = {}
@property
def metrics(self) -> Dict[str, OpencensusProxyMetric]:
"""Return the metrics requested to proxy export from this component."""
return self._metrics
@property
def last_reported_time(self):
return self._last_reported_time
def record(self, metrics: List[Metric]):
"""Parse the Opencensus protobuf and store metrics.
Metrics can be accessed via `metrics` API for proxy export.
Args:
metrics: A list of Opencensus protobuf for proxy export.
"""
self._last_reported_time = time.monotonic()
for metric in metrics:
fix_grpc_metric(metric)
descriptor = metric.metric_descriptor
name = descriptor.name
label_keys = [label_key.key for label_key in descriptor.label_keys]
if name not in self._metrics:
self._metrics[name] = OpencensusProxyMetric(
name, descriptor.description, descriptor.unit, label_keys
)
self._metrics[name].record(metric)
class OpenCensusProxyCollector:
def __init__(self, namespace: str, component_timeout_s: int = 60):
"""Prometheus collector implementation for opencensus proxy export.
Prometheus collector requires to implement `collect` which is
invoked whenever Prometheus queries the endpoint.
The class is thread-safe.
Args:
namespace: Prometheus namespace.
component_timeout_s: Number of seconds after which a component
without new reports is considered stale and its metrics are
no longer exported.
"""
# -- Protect `self._components` --
self._components_lock = threading.Lock()
# -- Timeout until the component is marked as stale --
# Once the component is considered as stale,
# the metrics from that worker won't be exported.
self._component_timeout_s = component_timeout_s
# -- Prometheus namespace --
self._namespace = namespace
# -- Component that requests to proxy export metrics --
# Component means core worker, raylet, and GCS.
# component_id -> Components
# For workers, they contain worker ids.
# For other components (raylet, GCS),
# they contain the global key `GLOBAL_COMPONENT_KEY`.
self._components = {}
# Whether we want to export counter as gauge.
# This is for bug compatibility.
# See https://github.com/ray-project/ray/pull/43795.
self._export_counter_as_gauge = env_bool("RAY_EXPORT_COUNTER_AS_GAUGE", True)
def record(self, metrics: List[Metric], worker_id_hex: str = None):
"""Record the metrics reported from the component that reports it.
Args:
metrics: A list of opencensus protobuf to proxy export metrics.
worker_id_hex: A worker id that reports these metrics.
If None, it means they are reported from Raylet or GCS.
"""
key = GLOBAL_COMPONENT_KEY if not worker_id_hex else worker_id_hex
with self._components_lock:
if key not in self._components:
self._components[key] = Component(key)
self._components[key].record(metrics)
def clean_stale_components(self):
"""Clean up stale components.
Stale means the component is dead or unresponsive.
Stale components won't be reported to Prometheus anymore.
"""
with self._components_lock:
stale_components = []
stale_component_ids = []
for id, component in self._components.items():
elapsed = time.monotonic() - component.last_reported_time
if elapsed > self._component_timeout_s:
stale_component_ids.append(id)
logger.info(
"Metrics from a worker ({}) is cleaned up due to "
"timeout. Time since last report {}s".format(id, elapsed)
)
for id in stale_component_ids:
stale_components.append(self._components.pop(id))
return stale_components
# TODO(sang): add start and end timestamp
def to_prometheus_metrics(
self,
metric_name: str,
metric_description: str,
label_keys: List[str],
metric_units: str,
label_values: Tuple[tag_value_module.TagValue],
agg_data: Any,
metrics_map: Dict[str, List[PrometheusMetric]],
) -> None:
"""to_metric translate the data that OpenCensus create
to Prometheus format, using Prometheus Metric object.
This method is from Opencensus Prometheus Exporter.
Args:
metric_name: Name of the metric.
metric_description: Description of the metric.
label_keys: The fixed label keys of the metric.
metric_units: Units of the metric.
label_values: The values of `label_keys`.
agg_data: `opencensus.stats.aggregation_data.AggregationData` object.
Aggregated data that needs to be converted as Prometheus samples
metrics_map: The converted metric is added to this map.
"""
assert self._components_lock.locked()
metric_name = f"{self._namespace}_{metric_name}"
assert len(label_values) == len(label_keys), (label_values, label_keys)
# Prometheus requires that all tag values be strings hence
# the need to cast none to the empty string before exporting. See
# https://github.com/census-instrumentation/opencensus-python/issues/480
label_values = [tv if tv else "" for tv in label_values]
if isinstance(agg_data, CountAggregationData):
metrics = metrics_map.get(metric_name)
if not metrics:
metric = CounterMetricFamily(
name=metric_name,
documentation=metric_description,
unit=metric_units,
labels=label_keys,
)
metrics = [metric]
metrics_map[metric_name] = metrics
metrics[0].add_metric(labels=label_values, value=agg_data.count_data)
return
if isinstance(agg_data, SumAggregationData):
# This should be emitted as prometheus counter
# but we used to emit it as prometheus gauge.
# To keep the backward compatibility
# (changing from counter to gauge changes the metric name
# since prometheus client will add "_total" suffix to counter
# per OpenMetrics specification),
# we now emit both counter and gauge and in the
# next major Ray release (3.0) we can stop emitting gauge.
# This leaves people enough time to migrate their dashboards.
# See https://github.com/ray-project/ray/pull/43795.
metrics = metrics_map.get(metric_name)
if not metrics:
metric = CounterMetricFamily(
name=metric_name,
documentation=metric_description,
labels=label_keys,
)
metrics = [metric]
metrics_map[metric_name] = metrics
metrics[0].add_metric(labels=label_values, value=agg_data.sum_data)
if not self._export_counter_as_gauge:
pass
elif metric_name.endswith("_total"):
# In this case, we only need to emit prometheus counter
# since for metric name already ends with _total suffix
# prometheus client won't change it
# so there is no backward compatibility issue.
# See https://prometheus.github.io/client_python/instrumenting/counter/
pass
else:
if len(metrics) == 1:
metric = GaugeMetricFamily(
name=metric_name,
documentation=(
f"(DEPRECATED, use {metric_name}_total metric instead) "
f"{metric_description}"
),
labels=label_keys,
)
metrics.append(metric)
assert len(metrics) == 2
metrics[1].add_metric(labels=label_values, value=agg_data.sum_data)
return
elif isinstance(agg_data, DistributionAggregationData):
assert agg_data.bounds == sorted(agg_data.bounds)
# buckets are a list of buckets. Each bucket is another list with
# a pair of bucket name and value, or a triple of bucket name,
# value, and exemplar. buckets need to be in order.
buckets = []
cum_count = 0 # Prometheus buckets expect cumulative count.
for ii, bound in enumerate(agg_data.bounds):
cum_count += agg_data.counts_per_bucket[ii]
bucket = [str(bound), cum_count]
buckets.append(bucket)
# Prometheus requires buckets to be sorted, and +Inf present.
# In OpenCensus we don't have +Inf in the bucket bonds so need to
# append it here.
buckets.append(["+Inf", agg_data.count_data])
metrics = metrics_map.get(metric_name)
if not metrics:
metric = HistogramMetricFamily(
name=metric_name,
documentation=metric_description,
labels=label_keys,
)
metrics = [metric]
metrics_map[metric_name] = metrics
metrics[0].add_metric(
labels=label_values,
buckets=buckets,
sum_value=agg_data.sum,
)
return
elif isinstance(agg_data, LastValueAggregationData):
metrics = metrics_map.get(metric_name)
if not metrics:
metric = GaugeMetricFamily(
name=metric_name,
documentation=metric_description,
labels=label_keys,
)
metrics = [metric]
metrics_map[metric_name] = metrics
metrics[0].add_metric(labels=label_values, value=agg_data.value)
return
else:
raise ValueError(f"unsupported aggregation type {type(agg_data)}")
def _aggregate_metric_data(
self,
datas: List[
Union[LastValueAggregationData, CountAggregationData, SumAggregationData]
],
) -> Union[LastValueAggregationData, CountAggregationData, SumAggregationData]:
assert len(datas) > 0
sample = datas[0]
if isinstance(sample, LastValueAggregationData):
return LastValueAggregationData(
ValueDouble, sum([data.value for data in datas])
)
if isinstance(sample, CountAggregationData):
return CountAggregationData(sum([data.count_data for data in datas]))
if isinstance(sample, SumAggregationData):
return SumAggregationData(
ValueDouble, sum([data.sum_data for data in datas])
)
raise ValueError(
f"Unsupported aggregation type {type(sample)}. "
"Supported types are "
f"{CountAggregationData}, {LastValueAggregationData}, {SumAggregationData}."
f"Got {datas}."
)
def _aggregate_with_recommended_cardinality(
self,
per_worker_metrics: List[OpencensusProxyMetric],
) -> List[OpencensusProxyMetric]:
"""Collect per-worker metrics, aggregate them into per-node metrics and convert
them to Prometheus format.
Args:
per_worker_metrics: A list of per-worker metrics for the same metric name.
Returns:
A list of per-node metrics for the same metric name, with the high
cardinality labels removed and the values aggregated.
"""
metric = next(iter(per_worker_metrics), None)
if not metric or WORKER_ID_TAG_KEY not in metric.label_keys:
# No high cardinality labels, return the original metrics.
return per_worker_metrics
worker_id_label_index = metric.label_keys.index(WORKER_ID_TAG_KEY)
# map from the tuple of label values without worker_id to the list of per worker
# task metrics
label_value_to_data: Dict[
Tuple,
List[
Union[
LastValueAggregationData,
CountAggregationData,
SumAggregationData,
]
],
] = defaultdict(list)
for metric in per_worker_metrics:
for label_values, data in metric.data.items():
# remove the worker_id from the label values
label_value_to_data[
label_values[:worker_id_label_index]
+ label_values[worker_id_label_index + 1 :]
].append(data)
aggregated_metric = OpencensusProxyMetric(
name=metric.name,
desc=metric.desc,
unit=metric.unit,
# remove the worker_id from the label keys
label_keys=metric.label_keys[:worker_id_label_index]
+ metric.label_keys[worker_id_label_index + 1 :],
)
for label_values, datas in label_value_to_data.items():
aggregated_metric.add_data(
label_values,
self._aggregate_metric_data(datas),
)
return [aggregated_metric]
def collect(self): # pragma: NO COVER
"""Collect fetches the statistics from OpenCensus
and delivers them as Prometheus Metrics.
Collect is invoked every time a prometheus.Gatherer is run
for example when the HTTP endpoint is invoked by Prometheus.
This method is required as a Prometheus Collector.
"""
with self._components_lock:
# First construct the list of opencensus metrics to be converted to
# prometheus metrics. For LEGACY cardinality level, this comprises all
# metrics from all components. For RECOMMENDED cardinality level, we need
# to remove the high cardinality labels and aggreate the component metrics.
open_cencus_metrics: List[OpencensusProxyMetric] = []
# The metrics that need to be aggregated with recommended cardinality. Key
# is the metric name and value is the list of per-worker metrics.
to_lower_cardinality: Dict[str, List[OpencensusProxyMetric]] = defaultdict(
list
)
cardinality_level = MetricCardinality.get_cardinality_level()
for component in self._components.values():
for metric in component.metrics.values():
if (
cardinality_level == MetricCardinality.RECOMMENDED
and not metric.is_distribution_aggregation_data()
):
# We reduce the cardinality for all metrics except for histogram
# metrics. The aggregation of histogram metrics from worker
# level to node level is not well defined. In addition, we
# currently have very few histogram metrics in Ray
# so the impact of them is negligible.
to_lower_cardinality[metric.name].append(metric)
else:
open_cencus_metrics.append(metric)
for per_worker_metrics in to_lower_cardinality.values():
open_cencus_metrics.extend(
self._aggregate_with_recommended_cardinality(
per_worker_metrics,
)
)
prometheus_metrics_map = {}
for metric in open_cencus_metrics:
for label_values, data in metric.data.items():
self.to_prometheus_metrics(
metric.name,
metric.desc,
metric.label_keys,
metric.unit,
label_values,
data,
prometheus_metrics_map,
)
for metrics in prometheus_metrics_map.values():
for metric in metrics:
yield metric
class MetricsAgent:
def __init__(
self,
view_manager: ViewManager,
stats_recorder: StatsRecorder,
stats_exporter: StatsExporter = None,
):
"""A class to record and export metrics.
The class exports metrics in 2 different ways.
- Directly record and export metrics using OpenCensus.
- Proxy metrics from other core components
(e.g., raylet, GCS, core workers).
This class is thread-safe.
"""
# Lock required because gRPC server uses
# multiple threads to process requests.
self._lock = threading.Lock()
#
# Opencensus components to record metrics.
#
# Managing views to export metrics
# If the stats_exporter is None, we disable all metrics export.
self.view_manager = view_manager
# A class that's used to record metrics
# emitted from the current process.
self.stats_recorder = stats_recorder
# A class to export metrics.
self.stats_exporter = stats_exporter
# -- A Prometheus custom collector to proxy export metrics --
# `None` if the prometheus server is not started.
self.proxy_exporter_collector = None
if self.stats_exporter is None:
# If the exporter is not given,
# we disable metrics collection.
self.view_manager = None
else:
self.view_manager.register_exporter(stats_exporter)
self.proxy_exporter_collector = OpenCensusProxyCollector(
self.stats_exporter.options.namespace,
component_timeout_s=int(os.getenv(RAY_WORKER_TIMEOUT_S, 120)),
)
# Registered view names.
self._registered_views: Set[str] = set()
def record_and_export(self, records: List[Record], global_tags=None):
"""Directly record and export stats from the same process."""
global_tags = global_tags or {}
with self._lock:
if not self.view_manager:
return
for record in records:
gauge = record.gauge
value = record.value
tags = record.tags
try:
self._record_gauge(gauge, value, {**tags, **global_tags})
except Exception as e:
logger.error(
f"Failed to record metric {gauge.name} with value {value} with tags {tags!r} and global tags {global_tags!r} due to: {e!r}"
)
def _record_gauge(self, gauge: Gauge, value: float, tags: dict):
if gauge.name not in self._registered_views:
self.view_manager.register_view(gauge.view)
self._registered_views.add(gauge.name)
measurement_map = self.stats_recorder.new_measurement_map()
tag_map = tag_map_module.TagMap()
for key, tag_val in tags.items():
try:
tag_key = tag_key_module.TagKey(key)
except ValueError as e:
logger.error(
f"Failed to create tag key {key} for metric {gauge.name} due to: {e!r}"
)
raise e
try:
tag_value = tag_value_module.TagValue(tag_val)
except ValueError as e:
logger.error(
f"Failed to create tag value {tag_val} for key {key} for metric {gauge.name} due to: {e!r}"
)
raise e
tag_map.insert(tag_key, tag_value)
measurement_map.measure_float_put(gauge.measure, value)
# NOTE: When we record this metric, timestamp will be renewed.
measurement_map.record(tag_map)
def proxy_export_metrics(self, metrics: List[Metric], worker_id_hex: str = None):
"""Proxy export metrics specified by a Opencensus Protobuf.
This API is used to export metrics emitted from
core components.
Args:
metrics: A list of protobuf Metric defined from OpenCensus.
worker_id_hex: The worker ID it proxies metrics export. None
if the metric is not from a worker (i.e., raylet, GCS).
Returns:
None.
"""
with self._lock:
if not self.view_manager:
return
self._proxy_export_metrics(metrics, worker_id_hex)
def _proxy_export_metrics(self, metrics: List[Metric], worker_id_hex: str = None):
self.proxy_exporter_collector.record(metrics, worker_id_hex)
def clean_all_dead_worker_metrics(self):
"""Clean dead worker's metrics.
Worker metrics are cleaned up and won't be exported once
it is considered as dead.
This method has to be periodically called by a caller.
"""
with self._lock:
if not self.view_manager:
return
self.proxy_exporter_collector.clean_stale_components()
class PrometheusServiceDiscoveryWriter(threading.Thread):
"""A class to support Prometheus service discovery.
It supports file-based service discovery. Checkout
https://prometheus.io/docs/guides/file-sd/ for more details.
Args:
gcs_address: Gcs address for this cluster.
temp_dir: Temporary directory used by
Ray to store logs and metadata.
session_dir: Session-specific directory for this Ray session.
If provided, the discovery file is written here instead of
temp_dir, and a backward-compatible symlink is created at
the old temp_dir location.
"""
def __init__(self, gcs_address: str, temp_dir: str, session_dir: str = None):
gcs_client_options = ray._raylet.GcsClientOptions.create(
gcs_address, None, allow_cluster_id_nil=True, fetch_cluster_id_if_nil=False
)
self.gcs_address = gcs_address
ray._private.state.state._initialize_global_state(gcs_client_options)
self.temp_dir = temp_dir
self.session_dir = session_dir if session_dir else temp_dir
# Tracks whether the backward-compatible symlink has been successfully created.
# This prevents recreating the symlink on every periodic write, avoiding
# unnecessary disk I/O, race conditions, and log flooding.
self._symlink_created = False
# If symlink creation fails (e.g., due to lack of permissions on Windows
# without developer mode, or restricted filesystems), this fallback flag is set
# to True. When True, the writer copies the file directly instead of symlinking.
self._use_fallback_copy = False
self.default_service_discovery_flush_period = 5
# The last service discovery content that PrometheusServiceDiscoveryWriter has seen
self.latest_service_discovery_content = []
self._content_lock = threading.RLock()
super().__init__()
def get_latest_service_discovery_content(self):
"""Return the latest stored service discovery content."""
with self._content_lock:
return self.latest_service_discovery_content
def get_file_discovery_content(self):
"""Return the content for Prometheus service discovery."""
nodes = ray.nodes()
metrics_export_addresses = [
build_address(node["NodeManagerAddress"], node["MetricsExportPort"])
for node in nodes
if node["alive"] is True
]
gcs_client = GcsClient(address=self.gcs_address)
autoscaler_addr = gcs_client.internal_kv_get(b"AutoscalerMetricsAddress", None)
if autoscaler_addr:
metrics_export_addresses.append(autoscaler_addr.decode("utf-8"))
dashboard_addr = gcs_client.internal_kv_get(b"DashboardMetricsAddress", None)
if dashboard_addr:
metrics_export_addresses.append(dashboard_addr.decode("utf-8"))
content = [{"labels": {"job": "ray"}, "targets": metrics_export_addresses}]
with self._content_lock:
self.latest_service_discovery_content = content
return json.dumps(content)
def write(self):
# Write a file based on https://prometheus.io/docs/guides/file-sd/
# Write should be atomic. Otherwise, Prometheus raises an error that
# json file format is invalid because it reads a file when
# file is re-written. Note that Prometheus still works although we
# have this error.
temp_file_name = self.get_temp_file_name()
with open(temp_file_name, "w") as json_file:
json_file.write(self.get_file_discovery_content())
# NOTE: os.replace is atomic on both Linux and Windows, so we won't
# have race condition reading this file.
os.replace(temp_file_name, self.get_target_file_name())
# Create a backward-compatible symlink at the old temp_dir location
# so that existing Prometheus configurations that reference the old
# path continue to work. Verify if the symlink is still valid and
# pointing to the correct target, repairing it if it has been deleted or modified.
if self.session_dir != self.temp_dir:
legacy_path = os.path.join(
self.temp_dir,
ray._private.ray_constants.PROMETHEUS_SERVICE_DISCOVERY_FILE,
)
if self._symlink_created and not self._use_fallback_copy:
try:
if not (
os.path.islink(legacy_path)
and os.readlink(legacy_path) == self.get_target_file_name()
):
self._symlink_created = False
except OSError:
self._symlink_created = False
if not self._symlink_created and not self._use_fallback_copy:
try:
if os.path.islink(legacy_path) or os.path.exists(legacy_path):
os.remove(legacy_path)
os.symlink(self.get_target_file_name(), legacy_path)
self._symlink_created = True
except OSError:
logger.warning(
f"Failed to create backward-compatible symlink at "
f"{legacy_path}. Falling back to copying the service discovery file."
)
self._use_fallback_copy = True
if self._use_fallback_copy:
try:
import shutil
temp_legacy_path = legacy_path + ".tmp"
shutil.copy(self.get_target_file_name(), temp_legacy_path)
os.replace(temp_legacy_path, legacy_path)
except OSError as e:
logger.warning(
f"Failed to copy service discovery file to legacy path {legacy_path}: {e}"
)
def get_target_file_name(self):
return os.path.join(
self.session_dir,
ray._private.ray_constants.PROMETHEUS_SERVICE_DISCOVERY_FILE,
)
def get_temp_file_name(self):
return os.path.join(
self.session_dir,
"{}_{}".format(
"tmp", ray._private.ray_constants.PROMETHEUS_SERVICE_DISCOVERY_FILE
),
)
def run(self):
while True:
# This thread won't be broken by exceptions.
try:
self.write()
except Exception as e:
logger.warning(
"Writing a service discovery file, {},failed.".format(
self.get_target_file_name()
)
)
logger.warning(traceback.format_exc())
logger.warning(f"Error message: {e}")
time.sleep(self.default_service_discovery_flush_period)