542 lines
18 KiB
Python
542 lines
18 KiB
Python
import copy
|
|
import json
|
|
import math
|
|
import os
|
|
import re
|
|
from dataclasses import asdict
|
|
from typing import List, Tuple
|
|
|
|
import ray
|
|
from ray.dashboard.modules.metrics.dashboards.common import (
|
|
DashboardConfig,
|
|
Panel,
|
|
PanelTemplate,
|
|
)
|
|
from ray.dashboard.modules.metrics.dashboards.data_dashboard_panels import (
|
|
data_dashboard_config,
|
|
)
|
|
from ray.dashboard.modules.metrics.dashboards.data_llm_dashboard_panels import (
|
|
data_llm_dashboard_config,
|
|
)
|
|
from ray.dashboard.modules.metrics.dashboards.default_dashboard_panels import (
|
|
default_dashboard_config,
|
|
)
|
|
from ray.dashboard.modules.metrics.dashboards.serve_deployment_dashboard_panels import (
|
|
serve_deployment_dashboard_config,
|
|
)
|
|
from ray.dashboard.modules.metrics.dashboards.serve_llm_dashboard_panels import (
|
|
serve_llm_dashboard_config,
|
|
)
|
|
from ray.dashboard.modules.metrics.dashboards.train_dashboard_panels import (
|
|
train_dashboard_config,
|
|
)
|
|
from ray.dashboard.modules.metrics.default_impl import get_serve_dashboard_config
|
|
|
|
GRAFANA_DASHBOARD_UID_OVERRIDE_ENV_VAR_TEMPLATE = "RAY_GRAFANA_{name}_DASHBOARD_UID"
|
|
GRAFANA_DASHBOARD_GLOBAL_FILTERS_OVERRIDE_ENV_VAR_TEMPLATE = (
|
|
"RAY_GRAFANA_{name}_DASHBOARD_GLOBAL_FILTERS"
|
|
)
|
|
GRAFANA_DASHBOARD_LOG_LINK_URL_ENV_VAR_TEMPLATE = "RAY_GRAFANA_{name}_LOG_LINK_URL"
|
|
|
|
# Grafana dashboard layout constants
|
|
# Dashboard uses a 24-column grid with 2-column panels
|
|
ROW_WIDTH = 24 # Full dashboard width
|
|
PANELS_PER_ROW = 2
|
|
PANEL_WIDTH = ROW_WIDTH // PANELS_PER_ROW # Width of each panel
|
|
PANEL_HEIGHT = 8 # Height of each panel
|
|
ROW_HEIGHT = 1 # Height of row container
|
|
|
|
|
|
def _read_configs_for_dashboard(
|
|
dashboard_config: DashboardConfig,
|
|
) -> Tuple[str, List[str], str]:
|
|
"""Reads environment variable configs for overriding uid, global_filters, and the log link URL for a given dashboard.
|
|
|
|
Args:
|
|
dashboard_config: The dashboard whose env-var overrides are read.
|
|
``dashboard_config.name`` selects the env-var suffix and
|
|
``default_uid`` is used as a fallback.
|
|
|
|
Returns:
|
|
Tuple with format uid, global_filters, log_link_url
|
|
"""
|
|
uid = (
|
|
os.environ.get(
|
|
GRAFANA_DASHBOARD_UID_OVERRIDE_ENV_VAR_TEMPLATE.format(
|
|
name=dashboard_config.name
|
|
)
|
|
)
|
|
or dashboard_config.default_uid
|
|
)
|
|
global_filters_str = (
|
|
os.environ.get(
|
|
GRAFANA_DASHBOARD_GLOBAL_FILTERS_OVERRIDE_ENV_VAR_TEMPLATE.format(
|
|
name=dashboard_config.name
|
|
)
|
|
)
|
|
or ""
|
|
)
|
|
if global_filters_str == "":
|
|
global_filters = []
|
|
else:
|
|
global_filters = global_filters_str.split(",")
|
|
|
|
log_link_url = (
|
|
os.environ.get(
|
|
GRAFANA_DASHBOARD_LOG_LINK_URL_ENV_VAR_TEMPLATE.format(
|
|
name=dashboard_config.name
|
|
)
|
|
)
|
|
or ""
|
|
)
|
|
|
|
return uid, global_filters, log_link_url
|
|
|
|
|
|
def generate_default_grafana_dashboard() -> Tuple[str, str]:
|
|
"""
|
|
Generates the dashboard output for the default dashboard and returns
|
|
both the content and the uid.
|
|
|
|
Returns:
|
|
Tuple with format content, uid
|
|
"""
|
|
return _generate_grafana_dashboard(default_dashboard_config)
|
|
|
|
|
|
def generate_serve_grafana_dashboard() -> Tuple[str, str]:
|
|
"""
|
|
Generates the dashboard output for the serve dashboard and returns
|
|
both the content and the uid.
|
|
|
|
Returns:
|
|
Tuple with format content, uid
|
|
"""
|
|
return _generate_grafana_dashboard(get_serve_dashboard_config())
|
|
|
|
|
|
def generate_serve_deployment_grafana_dashboard() -> Tuple[str, str]:
|
|
"""
|
|
Generates the dashboard output for the serve dashboard and returns
|
|
both the content and the uid.
|
|
|
|
Returns:
|
|
Tuple with format content, uid
|
|
"""
|
|
return _generate_grafana_dashboard(serve_deployment_dashboard_config)
|
|
|
|
|
|
def generate_serve_llm_grafana_dashboard() -> Tuple[str, str]:
|
|
"""
|
|
Generates the dashboard output for the serve dashboard and returns
|
|
both the content and the uid.
|
|
|
|
Returns:
|
|
Tuple with format content, uid
|
|
"""
|
|
return _generate_grafana_dashboard(serve_llm_dashboard_config)
|
|
|
|
|
|
def generate_data_grafana_dashboard() -> Tuple[str, str]:
|
|
"""
|
|
Generates the dashboard output for the data dashboard and returns
|
|
both the content and the uid.
|
|
|
|
Returns:
|
|
Tuple with format content, uid
|
|
"""
|
|
return _generate_grafana_dashboard(data_dashboard_config)
|
|
|
|
|
|
def generate_data_llm_grafana_dashboard() -> Tuple[str, str]:
|
|
"""
|
|
Generates the dashboard output for the Data LLM dashboard and returns
|
|
both the content and the uid.
|
|
|
|
This dashboard provides vLLM metrics visibility for Ray Data LLM workloads,
|
|
including latency (TTFT, TPOT), throughput, cache utilization, and
|
|
prefix cache hit rate.
|
|
|
|
Returns:
|
|
Tuple with format content, uid
|
|
"""
|
|
return _generate_grafana_dashboard(data_llm_dashboard_config)
|
|
|
|
|
|
def generate_train_grafana_dashboard() -> Tuple[str, str]:
|
|
"""
|
|
Generates the dashboard output for the train dashboard and returns
|
|
both the content and the uid.
|
|
|
|
Returns:
|
|
Tuple with format content, uid
|
|
"""
|
|
return _generate_grafana_dashboard(train_dashboard_config)
|
|
|
|
|
|
def _generate_grafana_dashboard(dashboard_config: DashboardConfig) -> str:
|
|
"""Render the Grafana dashboard JSON for the given config.
|
|
|
|
Args:
|
|
dashboard_config: Configuration describing the panels and base
|
|
template JSON file to use for rendering.
|
|
|
|
Returns:
|
|
Tuple with format dashboard_content, uid
|
|
"""
|
|
uid, global_filters, log_link_url = _read_configs_for_dashboard(dashboard_config)
|
|
panels = _generate_grafana_panels(dashboard_config, global_filters, log_link_url)
|
|
base_file_name = dashboard_config.base_json_file_name
|
|
|
|
base_json = json.load(
|
|
open(os.path.join(os.path.dirname(__file__), "dashboards", base_file_name))
|
|
)
|
|
base_json["panels"] = panels
|
|
# Update variables to use global_filters
|
|
global_filters_str = ",".join(global_filters)
|
|
variables = base_json.get("templating", {}).get("list", [])
|
|
for variable in variables:
|
|
if "definition" not in variable:
|
|
continue
|
|
definition = variable["definition"].format(global_filters=global_filters_str)
|
|
query = variable["query"]["query"].format(global_filters=global_filters_str)
|
|
if not global_filters_str:
|
|
definition = _clean_empty_filters(definition)
|
|
query = _clean_empty_filters(query)
|
|
variable["definition"] = definition
|
|
variable["query"]["query"] = query
|
|
|
|
tags = base_json.get("tags", []) or []
|
|
tags.append(f"rayVersion:{ray.__version__}")
|
|
base_json["tags"] = tags
|
|
base_json["uid"] = uid
|
|
# Ray metadata can be used to put arbitrary metadata
|
|
ray_meta = base_json.get("rayMeta", []) or []
|
|
ray_meta.append("supportsGlobalFilterOverride")
|
|
base_json["rayMeta"] = ray_meta
|
|
return json.dumps(base_json, indent=4), uid
|
|
|
|
|
|
def _generate_panel_template(
|
|
panel: Panel,
|
|
panel_global_filters: List[str],
|
|
panel_index: int,
|
|
base_y_position: int,
|
|
log_link_url: str,
|
|
) -> dict:
|
|
"""
|
|
Helper method to generate a panel template with common configuration.
|
|
|
|
Args:
|
|
panel: The panel configuration
|
|
panel_global_filters: List of global filters to apply
|
|
panel_index: The index of the panel within its row (0-based)
|
|
base_y_position: The base y-coordinate for the row in the dashboard grid
|
|
log_link_url: The URL to the log link for the panel
|
|
|
|
Returns:
|
|
dict: The configured panel template
|
|
"""
|
|
# Create base template from panel configuration
|
|
template = copy.deepcopy(panel.template.value)
|
|
template.update(
|
|
{
|
|
"title": panel.title,
|
|
"description": panel.description,
|
|
"id": panel.id,
|
|
"targets": _generate_targets(panel, panel_global_filters),
|
|
}
|
|
)
|
|
|
|
# Set panel position and dimensions
|
|
if panel.grid_pos:
|
|
template["gridPos"] = asdict(panel.grid_pos)
|
|
else:
|
|
# Calculate panel position in 2-column grid layout
|
|
# x: 0 or 12 (left or right column)
|
|
# y: base position + (row number * panel height)
|
|
row_number = panel_index // PANELS_PER_ROW
|
|
template["gridPos"] = {
|
|
"h": PANEL_HEIGHT,
|
|
"w": PANEL_WIDTH,
|
|
"x": PANEL_WIDTH * (panel_index % PANELS_PER_ROW),
|
|
"y": base_y_position + (row_number * PANEL_HEIGHT),
|
|
}
|
|
|
|
# Set unit format for legacy graph-style panels (GRAPH, HEATMAP, STAT, GAUGE, PIE_CHART, BAR_CHART)
|
|
if panel.template in (
|
|
PanelTemplate.GRAPH,
|
|
PanelTemplate.HEATMAP,
|
|
PanelTemplate.STAT,
|
|
PanelTemplate.GAUGE,
|
|
PanelTemplate.PIE_CHART,
|
|
PanelTemplate.BAR_CHART,
|
|
):
|
|
template["yaxes"][0]["format"] = panel.unit
|
|
|
|
# Set fieldConfig unit (for newer panel types with fieldConfig.defaults)
|
|
if panel.template in (
|
|
PanelTemplate.STAT,
|
|
PanelTemplate.GAUGE,
|
|
PanelTemplate.HEATMAP,
|
|
PanelTemplate.PIE_CHART,
|
|
PanelTemplate.BAR_CHART,
|
|
PanelTemplate.TABLE,
|
|
PanelTemplate.GRAPH,
|
|
):
|
|
template["fieldConfig"]["defaults"]["unit"] = panel.unit
|
|
|
|
# Set fill, stack, linewidth, nullPointMode (only for GRAPH panels)
|
|
if panel.template == PanelTemplate.GRAPH:
|
|
template["fill"] = panel.fill
|
|
template["stack"] = panel.stack
|
|
template["linewidth"] = panel.linewidth
|
|
if panel.stack is True:
|
|
template["nullPointMode"] = "connected"
|
|
|
|
if panel.hideXAxis:
|
|
template.setdefault("xaxis", {})["show"] = False
|
|
|
|
# Handle optional panel customization fields
|
|
|
|
# Thresholds (for panels with fieldConfig.defaults.thresholds)
|
|
if panel.thresholds is not None:
|
|
if panel.template in (PanelTemplate.STAT, PanelTemplate.GAUGE):
|
|
template["fieldConfig"]["defaults"]["thresholds"][
|
|
"steps"
|
|
] = panel.thresholds
|
|
|
|
# Value mappings (for panels with fieldConfig.defaults.mappings)
|
|
if panel.value_mappings is not None:
|
|
if panel.template in (
|
|
PanelTemplate.STAT,
|
|
PanelTemplate.GAUGE,
|
|
PanelTemplate.TABLE,
|
|
):
|
|
template["fieldConfig"]["defaults"]["mappings"] = panel.value_mappings
|
|
|
|
# Color mode (for STAT panels with options.colorMode)
|
|
if panel.color_mode is not None:
|
|
if panel.template == PanelTemplate.STAT:
|
|
template["options"]["colorMode"] = panel.color_mode
|
|
|
|
# Legend mode
|
|
if panel.legend_mode is not None:
|
|
if panel.template in (PanelTemplate.GRAPH, PanelTemplate.BAR_CHART):
|
|
# For graph panels (legacy format with top-level legend object)
|
|
template["legend"]["show"] = panel.legend_mode != "hidden"
|
|
template["legend"]["alignAsTable"] = panel.legend_mode == "table"
|
|
elif panel.template == PanelTemplate.PIE_CHART:
|
|
# For PIE_CHART (options.legend.displayMode)
|
|
template["options"]["legend"]["displayMode"] = panel.legend_mode
|
|
|
|
# Min/max values (for panels with fieldConfig.defaults)
|
|
if panel.min_val is not None or panel.max_val is not None:
|
|
if panel.template in (
|
|
PanelTemplate.STAT,
|
|
PanelTemplate.GAUGE,
|
|
PanelTemplate.HEATMAP,
|
|
PanelTemplate.PIE_CHART,
|
|
PanelTemplate.BAR_CHART,
|
|
PanelTemplate.TABLE,
|
|
PanelTemplate.GRAPH,
|
|
):
|
|
if panel.min_val is not None:
|
|
template["fieldConfig"]["defaults"]["min"] = panel.min_val
|
|
if panel.max_val is not None:
|
|
template["fieldConfig"]["defaults"]["max"] = panel.max_val
|
|
|
|
# Reduce calculation (for panels with options.reduceOptions)
|
|
if panel.reduce_calc is not None:
|
|
if panel.template in (
|
|
PanelTemplate.STAT,
|
|
PanelTemplate.GAUGE,
|
|
PanelTemplate.PIE_CHART,
|
|
):
|
|
template["options"]["reduceOptions"]["calcs"] = [panel.reduce_calc]
|
|
|
|
# Handle heatmap-specific options
|
|
if panel.heatmap_color_scheme is not None:
|
|
if panel.template == PanelTemplate.HEATMAP:
|
|
template["options"]["color"]["scheme"] = panel.heatmap_color_scheme
|
|
|
|
if panel.heatmap_color_reverse is not None:
|
|
if panel.template == PanelTemplate.HEATMAP:
|
|
template["options"]["color"]["reverse"] = panel.heatmap_color_reverse
|
|
|
|
if panel.heatmap_yaxis_label is not None:
|
|
if panel.template in (
|
|
PanelTemplate.GRAPH,
|
|
PanelTemplate.HEATMAP,
|
|
PanelTemplate.STAT,
|
|
PanelTemplate.GAUGE,
|
|
PanelTemplate.PIE_CHART,
|
|
PanelTemplate.BAR_CHART,
|
|
):
|
|
template["yaxes"][0]["label"] = panel.heatmap_yaxis_label
|
|
|
|
# Add log link if URL is provided via environment variable.
|
|
if log_link_url:
|
|
template["links"] = [
|
|
{
|
|
"targetBlank": True,
|
|
"title": "View Logs",
|
|
"url": log_link_url,
|
|
}
|
|
]
|
|
|
|
return template
|
|
|
|
|
|
def _create_row_panel(row: Panel, y_position: int) -> dict:
|
|
"""
|
|
Creates a Grafana row panel that spans the full dashboard width.
|
|
Row panels can be collapsed to hide their contained panels.
|
|
|
|
Args:
|
|
row: Row config with title, id, and collapse state
|
|
y_position: Vertical position in dashboard grid
|
|
|
|
Returns:
|
|
Grafana row panel configuration
|
|
"""
|
|
return {
|
|
"collapsed": row.collapsed,
|
|
"gridPos": {"h": ROW_HEIGHT, "w": ROW_WIDTH, "x": 0, "y": y_position},
|
|
"id": row.id,
|
|
"title": row.title,
|
|
"type": "row",
|
|
"panels": [],
|
|
}
|
|
|
|
|
|
def _calculate_panel_heights(num_panels: int) -> int:
|
|
"""
|
|
Calculate the total height needed for a set of panels.
|
|
|
|
Args:
|
|
num_panels: Number of panels to position
|
|
|
|
Returns:
|
|
Total height needed for the panels
|
|
"""
|
|
rows_needed = math.ceil(num_panels / PANELS_PER_ROW)
|
|
return rows_needed * PANEL_HEIGHT
|
|
|
|
|
|
def _generate_grafana_panels(
|
|
config: DashboardConfig, global_filters: List[str], log_link_url: str
|
|
) -> List[dict]:
|
|
"""
|
|
Generates Grafana panel configurations for a dashboard.
|
|
|
|
The dashboard layout follows these rules:
|
|
- Panels are arranged in 2 columns (12 units wide each)
|
|
- Each panel is 8 units high
|
|
- Rows are 1 unit high and can be collapsed
|
|
- Panels within rows follow the same 2-column layout
|
|
- Panel positions can be overridden via panel.grid_pos or auto-calculated
|
|
|
|
Args:
|
|
config: Dashboard configuration containing panels and rows
|
|
global_filters: List of filters to apply to all panels
|
|
log_link_url: Optional URL for panel log links. When set, each panel
|
|
gets a "View Logs" link pointing to this URL.
|
|
|
|
Returns:
|
|
List of Grafana panel configurations for the dashboard
|
|
"""
|
|
panels = []
|
|
panel_global_filters = [*config.standard_global_filters, *global_filters]
|
|
current_y_position = 0
|
|
|
|
# Add top-level panels in 2-column grid
|
|
for panel_index, panel in enumerate(config.panels):
|
|
panel_template = _generate_panel_template(
|
|
panel, panel_global_filters, panel_index, current_y_position, log_link_url
|
|
)
|
|
panels.append(panel_template)
|
|
|
|
# Calculate space needed for top-level panels
|
|
current_y_position += _calculate_panel_heights(len(config.panels))
|
|
|
|
# Add rows and their panels
|
|
if not config.rows:
|
|
return panels
|
|
|
|
for row in config.rows:
|
|
# Create and add row panel
|
|
row_panel = _create_row_panel(row, current_y_position)
|
|
panels.append(row_panel)
|
|
current_y_position += ROW_HEIGHT
|
|
|
|
# Add panels within row using 2-column grid
|
|
for panel_index, panel in enumerate(row.panels):
|
|
panel_template = _generate_panel_template(
|
|
panel,
|
|
panel_global_filters,
|
|
panel_index,
|
|
current_y_position,
|
|
log_link_url,
|
|
)
|
|
|
|
# Add panel to row if collapsed, otherwise to main dashboard
|
|
if row.collapsed:
|
|
row_panel["panels"].append(panel_template)
|
|
else:
|
|
panels.append(panel_template)
|
|
|
|
# Update y position for next row based on actual panel positions
|
|
# when explicit grid_pos is used, or fallback to calculated height.
|
|
if any(p.grid_pos for p in row.panels):
|
|
max_y_bottom = max(
|
|
(p.grid_pos.y + p.grid_pos.h for p in row.panels if p.grid_pos),
|
|
default=current_y_position,
|
|
)
|
|
current_y_position = max_y_bottom
|
|
else:
|
|
current_y_position += _calculate_panel_heights(len(row.panels))
|
|
|
|
return panels
|
|
|
|
|
|
def _clean_empty_filters(expr: str) -> str:
|
|
"""Clean up malformed PromQL when global_filters is empty.
|
|
|
|
Removes artifacts like trailing/leading commas in label matchers:
|
|
", ," → ","
|
|
", }" → "}"
|
|
"{ ," → "{"
|
|
"""
|
|
expr = re.sub(r",\s*,", ",", expr)
|
|
expr = re.sub(r",\s*}", "}", expr)
|
|
expr = re.sub(r"{\s*,", "{", expr)
|
|
return expr
|
|
|
|
|
|
def gen_incrementing_alphabets(length):
|
|
assert 65 + length < 96, "we only support up to 26 targets at a time."
|
|
# 65: ascii code of 'A'.
|
|
return list(map(chr, range(65, 65 + length)))
|
|
|
|
|
|
def _generate_targets(panel: Panel, panel_global_filters: List[str]) -> List[dict]:
|
|
targets = []
|
|
for target, ref_id in zip(
|
|
panel.targets, gen_incrementing_alphabets(len(panel.targets))
|
|
):
|
|
template = copy.deepcopy(target.template.value)
|
|
global_filters_str = ",".join(panel_global_filters)
|
|
expr = target.expr.format(global_filters=global_filters_str)
|
|
if not global_filters_str:
|
|
expr = _clean_empty_filters(expr)
|
|
template.update(
|
|
{
|
|
"expr": expr,
|
|
"legendFormat": target.legend,
|
|
"refId": ref_id,
|
|
}
|
|
)
|
|
targets.append(template)
|
|
return targets
|