151 lines
5.6 KiB
Python
151 lines
5.6 KiB
Python
"""Utilities for Ray Data telemetry: operator naming / logical-op usage
|
|
recording, plus helpers for the cluster metric queries fired by the usage collector
|
|
in background threads.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import threading
|
|
from typing import Dict, Optional
|
|
|
|
import requests
|
|
|
|
from ray._common.usage.usage_lib import TagKey, record_extra_usage_tag
|
|
from ray.data._internal.logical.interfaces import LogicalOperator
|
|
from ray.data._internal.logical.operators import Read, ReadFiles, Write
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# The dictionary for the operator name and count.
|
|
_recorded_operators = dict()
|
|
_recorded_operators_lock = threading.Lock()
|
|
|
|
# Bounded timeout for the Prometheus counter HTTP queries.
|
|
_PROMETHEUS_QUERY_TIMEOUT_S = 0.25
|
|
|
|
|
|
def _prometheus_host() -> str:
|
|
"""Prometheus base URL, matching the dashboard's ``RAY_PROMETHEUS_HOST``."""
|
|
return os.environ.get("RAY_PROMETHEUS_HOST", "http://localhost:9090")
|
|
|
|
|
|
def _prometheus_headers() -> Dict[str, str]:
|
|
"""Parse ``RAY_PROMETHEUS_HEADERS`` (a JSON dict or list of ``[key, value]``
|
|
pairs). Returns an empty dict on any parse failure.
|
|
"""
|
|
try:
|
|
headers = json.loads(os.environ.get("RAY_PROMETHEUS_HEADERS", "{}"))
|
|
except json.JSONDecodeError:
|
|
logger.debug("Failed to parse RAY_PROMETHEUS_HEADERS as JSON", exc_info=True)
|
|
return {}
|
|
if isinstance(headers, list):
|
|
return dict(headers)
|
|
return headers or {}
|
|
|
|
|
|
def query_prometheus_counter(promql: str) -> Optional[int]:
|
|
"""Instant-query a cumulative Prometheus counter and return its cluster-wide
|
|
integer value.
|
|
|
|
Returns None on any failure (Prometheus unreachable, non-200, empty result),
|
|
as usage collection is best effort
|
|
"""
|
|
try:
|
|
resp = requests.get(
|
|
f"{_prometheus_host()}/api/v1/query",
|
|
params={"query": promql},
|
|
headers=_prometheus_headers(),
|
|
timeout=_PROMETHEUS_QUERY_TIMEOUT_S,
|
|
)
|
|
if resp.status_code != 200:
|
|
logger.debug(
|
|
"Prometheus counter query %r returned %d", promql, resp.status_code
|
|
)
|
|
return None
|
|
results = resp.json()["data"]["result"]
|
|
if not results:
|
|
logger.debug("Prometheus counter query %r returned empty result", promql)
|
|
return None
|
|
return int(sum(float(r["value"][1]) for r in results))
|
|
except (requests.RequestException, ValueError, KeyError, IndexError, TypeError):
|
|
# RequestException: unreachable/timeout/non-HTTP error; the rest:
|
|
# unexpected/empty response shape. Best-effort — return None on any.
|
|
logger.debug("Failed to query Prometheus counter %r", promql, exc_info=True)
|
|
return None
|
|
|
|
|
|
def compute_delta(start: Optional[int], end: Optional[int]) -> Optional[int]:
|
|
"""Non-negative delta between two cumulative samples. Returns None if
|
|
either sample is missing"""
|
|
if start is None or end is None:
|
|
return None
|
|
return max(0, end - start)
|
|
|
|
|
|
def _is_builtin_cls(cls: type) -> bool:
|
|
"""Return True if ``cls`` is defined under the ``ray`` package.
|
|
|
|
Used to gate which operator / datasource / datasink class names are safe
|
|
to surface in telemetry. Anything outside ``ray.*`` is treated as
|
|
user-defined and anonymized.
|
|
"""
|
|
return (cls.__module__ or "").startswith("ray.")
|
|
|
|
|
|
def record_operators_usage(op: LogicalOperator):
|
|
"""Record logical operator usage with Ray telemetry."""
|
|
ops_dict = dict()
|
|
_collect_operators_to_dict(op, ops_dict)
|
|
ops_json_str = ""
|
|
with _recorded_operators_lock:
|
|
for op_name, count in ops_dict.items():
|
|
_recorded_operators.setdefault(op_name, 0)
|
|
_recorded_operators[op_name] += count
|
|
ops_json_str = json.dumps(_recorded_operators)
|
|
|
|
record_extra_usage_tag(TagKey.DATA_LOGICAL_OPS, ops_json_str)
|
|
|
|
|
|
def anonymize_op_name(op: LogicalOperator) -> str:
|
|
"""Return an op name suitable for usage collection.
|
|
|
|
Read/Write surface their datasource/datasink suffix (``ReadParquet``,
|
|
``WriteIceberg``) when the underlying class ships under ``ray.data.*``;
|
|
user-defined datasources/datasinks collapse to ``ReadCustom`` /
|
|
``WriteCustom``. ``ReadFiles`` (the V2 file-read op) surfaces its
|
|
format via ``datasource_name`` (e.g. ``ReadFilesParquetV2``) when the
|
|
scanner class is built-in; user-defined scanners collapse to
|
|
``ReadFilesCustom``. All other built-in operators emit their class
|
|
name (``Sort``, ``MapBatches``, ``Limit``, …); user-defined
|
|
``LogicalOperator`` subclasses collapse to ``Unknown``.
|
|
"""
|
|
if isinstance(op, Read):
|
|
if _is_builtin_cls(type(op.datasource)):
|
|
return f"Read{op.datasource.get_name()}"
|
|
return "ReadCustom"
|
|
if isinstance(op, Write):
|
|
sink = op.datasink_or_legacy_datasource
|
|
if _is_builtin_cls(type(sink)):
|
|
return f"Write{sink.get_name()}"
|
|
return "WriteCustom"
|
|
if isinstance(op, ReadFiles):
|
|
# Gate on the scanner class — the string ``datasource_name`` field
|
|
# could be set to anything by a user-defined V2 datasource, so it's
|
|
# not safe to surface on its own.
|
|
if _is_builtin_cls(type(op.scanner)):
|
|
return f"ReadFiles{op.datasource_name}"
|
|
return "ReadFilesCustom"
|
|
cls = type(op)
|
|
return cls.__name__ if _is_builtin_cls(cls) else "Unknown"
|
|
|
|
|
|
def _collect_operators_to_dict(op: LogicalOperator, ops_dict: Dict[str, int]):
|
|
"""Collect the logical operator name and count into `ops_dict`."""
|
|
for child in op.input_dependencies:
|
|
_collect_operators_to_dict(child, ops_dict)
|
|
|
|
op_name = anonymize_op_name(op)
|
|
ops_dict.setdefault(op_name, 0)
|
|
ops_dict[op_name] += 1
|