Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions python/ray/dashboard/modules/metrics/dashboards/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
from typing import Any, Dict, List, Optional

from ray.util.annotations import DeveloperAPI

Expand Down Expand Up @@ -92,7 +92,7 @@ class Target:
"exemplars": {"color": "rgba(255,0,255,0.7)"},
"filterValues": {"le": 1e-9},
"legend": {"show": True},
"rowsFrame": {"layout": "auto", "value": "Request count"},
"rowsFrame": {"layout": "auto", "value": "Value"},
"tooltip": {"mode": "single", "showColorScale": False, "yHistogram": True},
"yAxis": {"axisPlacement": "left", "reverse": False, "unit": "none"},
},
Expand Down Expand Up @@ -461,6 +461,36 @@ class Target:
"yaxis": {"align": False, "alignLevel": None},
}

TABLE_PANEL_TEMPLATE = {
"datasource": r"${datasource}",
"description": "<Description>",
"fieldConfig": {
"defaults": {
"custom": {
"align": "auto",
"displayMode": "auto",
},
"mappings": [],
},
"overrides": [],
},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"id": 26,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The id for this TABLE_PANEL_TEMPLATE is 26. This ID is also used by GRAPH_PANEL_TEMPLATE, PIE_CHART_TEMPLATE, and BAR_CHART_PANEL_TEMPLATE. While the panel id is overridden during dashboard generation, using duplicate IDs in templates is confusing and could lead to subtle bugs if the generation logic changes. Please consider using a unique placeholder ID for each panel template to avoid potential conflicts.

"options": {
"showHeader": True,
"footer": {
"show": False,
"reducer": ["sum"],
"fields": "",
},
},
"pluginVersion": "7.5.17",
"targets": [],
"title": "<Title>",
"type": "table",
"transformations": [{"id": "organize", "options": {}}],
}


@DeveloperAPI
class PanelTemplate(Enum):
Expand All @@ -470,6 +500,7 @@ class PanelTemplate(Enum):
STAT = STAT_PANEL_TEMPLATE
GAUGE = GAUGE_PANEL_TEMPLATE
BAR_CHART = BAR_CHART_PANEL_TEMPLATE
TABLE = TABLE_PANEL_TEMPLATE


@DeveloperAPI
Expand All @@ -488,6 +519,26 @@ class Panel:
targets: List of query targets.
fill: Whether or not the graph will be filled by a color.
stack: Whether or not the lines in the graph will be stacked.
linewidth: Width of the lines in the graph.
grid_pos: Grid position of the panel.
template: The panel template to use.
hideXAxis: Whether to hide the x-axis.
thresholds: Custom threshold configuration for stat/gauge panels.
Example: [
{"color": "green", "value": None},
{"color": "yellow", "value": 70},
{"color": "red", "value": 90}
]
value_mappings: Value mappings for displaying text instead of numbers.
Used for status panels.
color_mode: Color mode for stat panels ("value", "background", "none").
legend_mode: Legend display mode ("list", "table", "hidden").
min_val: Minimum value for gauge/graph y-axis.
max_val: Maximum value for gauge/graph y-axis.
reduce_calc: Reduce calculation method for stat panels (default: "lastNotNull").
heatmap_color_scheme: Color scheme for heatmap panels (e.g., "Spectral", "RdYlGn").
heatmap_color_reverse: Whether to reverse the heatmap color scheme.
heatmap_yaxis_label: Y-axis label for heatmap panels.
"""

title: str
Expand All @@ -501,6 +552,16 @@ class Panel:
grid_pos: Optional[GridPos] = None
template: Optional[PanelTemplate] = PanelTemplate.GRAPH
hideXAxis: bool = False
thresholds: Optional[List[Dict[str, Any]]] = None
value_mappings: Optional[List[Dict[str, Any]]] = None
color_mode: Optional[str] = None
legend_mode: Optional[str] = None
min_val: Optional[float] = None
max_val: Optional[float] = None
reduce_calc: Optional[str] = None
heatmap_color_scheme: Optional[str] = None
heatmap_color_reverse: Optional[bool] = None
heatmap_yaxis_label: Optional[str] = None


@DeveloperAPI
Expand Down
12 changes: 12 additions & 0 deletions python/ray/dashboard/modules/metrics/default_impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from ray.dashboard.modules.metrics.dashboards.common import DashboardConfig


def get_serve_dashboard_config() -> DashboardConfig:
from ray.dashboard.modules.metrics.dashboards.serve_dashboard_panels import (
serve_dashboard_config,
)

return serve_dashboard_config


# Anyscale overrides
121 changes: 110 additions & 11 deletions python/ray/dashboard/modules/metrics/grafana_dashboard_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@
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.default_dashboard_panels import (
default_dashboard_config,
)
from ray.dashboard.modules.metrics.dashboards.serve_dashboard_panels import (
serve_dashboard_config,
)
from ray.dashboard.modules.metrics.dashboards.serve_deployment_dashboard_panels import (
serve_deployment_dashboard_config,
)
Expand All @@ -28,6 +26,7 @@
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 = (
Expand Down Expand Up @@ -96,7 +95,7 @@ def generate_serve_grafana_dashboard() -> Tuple[str, str]:
Returns:
Tuple with format content, uid
"""
return _generate_grafana_dashboard(serve_dashboard_config)
return _generate_grafana_dashboard(get_serve_dashboard_config())


def generate_serve_deployment_grafana_dashboard() -> Tuple[str, str]:
Expand Down Expand Up @@ -224,17 +223,117 @@ def _generate_panel_template(
"y": base_y_position + (row_number * PANEL_HEIGHT),
}

template["yaxes"][0]["format"] = panel.unit
template["fill"] = panel.fill
template["stack"] = panel.stack
template["linewidth"] = panel.linewidth
# 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"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BAR_CHART panels ignore stack and linewidth settings

High Severity

The stack, linewidth, and nullPointMode settings are only applied when panel.template == PanelTemplate.GRAPH, but BAR_CHART panels also have these fields and need them applied. The BAR_CHART_PANEL_TEMPLATE has "stack": True and "linewidth": 1 as defaults, but several existing BAR_CHART panels in data_dashboard_panels.py explicitly set stack=False. Since the condition excludes BAR_CHART, these panels will incorrectly render as stacked when they should be grouped (unstacked). The condition should include PanelTemplate.BAR_CHART for stack, linewidth, and nullPointMode.

🔬 Verification Test

Test code:

# test_bar_chart_stack_bug.py
import copy
from ray.dashboard.modules.metrics.dashboards.common import (
    Panel, Target, PanelTemplate, BAR_CHART_PANEL_TEMPLATE
)
from ray.dashboard.modules.metrics.grafana_dashboard_factory import _generate_panel_template

# Create a BAR_CHART panel with stack=False (like the data dashboard panels)
panel = Panel(
    title="Test",
    description="Test",
    id=1,
    unit="short",
    targets=[Target(expr="test", legend="test")],
    stack=False,  # Explicitly setting stack to False
    template=PanelTemplate.BAR_CHART,
)

# Generate the template
result = _generate_panel_template(panel, [], 0, 0)

print(f"BAR_CHART_PANEL_TEMPLATE stack default: {BAR_CHART_PANEL_TEMPLATE['stack']}")
print(f"Panel.stack setting: {panel.stack}")
print(f"Generated template stack value: {result['stack']}")
print(f"BUG: stack=False was NOT applied: {result['stack'] == True}")

Command run:

cd /workspace && python test_bar_chart_stack_bug.py

Output:

BAR_CHART_PANEL_TEMPLATE stack default: True
Panel.stack setting: False
Generated template stack value: True
BUG: stack=False was NOT applied: True

Why this proves the bug: The panel was created with stack=False, but the generated template has stack: True (the default from BAR_CHART_PANEL_TEMPLATE). This proves the stack setting is being ignored for BAR_CHART panels, which will cause existing data dashboard histogram panels to render incorrectly as stacked instead of grouped.

Fix in Cursor Fix in Web


if panel.hideXAxis:
template.setdefault("xaxis", {})["show"] = False

# Handle stacking visualization
if panel.stack is True:
template["nullPointMode"] = "connected"
# 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

return template
Comment on lines +226 to 338
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This large block of conditional logic has many repeated checks like panel.template in (...). This reduces readability and makes it harder to maintain.

To improve this, you could define constant sets for groups of panel templates that share configuration options at the top of the file.

For example:

PANELS_WITH_YAXES = {
    PanelTemplate.GRAPH,
    PanelTemplate.HEATMAP,
    PanelTemplate.STAT,
    PanelTemplate.GAUGE,
    PanelTemplate.PIE_CHART,
    PanelTemplate.BAR_CHART,
}

Then, you can simplify the conditions:

if panel.template in PANELS_WITH_YAXES:
    template["yaxes"][0]["format"] = panel.unit

Applying this pattern throughout this function will make the code more declarative and easier to manage.


Expand Down
10 changes: 8 additions & 2 deletions python/ray/tests/test_metrics_head.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,14 @@ def test_metrics_folder_with_dashboard_override(
contents = json.loads(f.read())
assert contents["uid"] == serve_uid
for panel in contents["panels"]:
for target in panel["targets"]:
assert serve_global_filters in target["expr"]
if panel["type"] == "row":
# Row panels contain nested panels, not targets directly
for nested_panel in panel.get("panels", []):
for target in nested_panel["targets"]:
assert serve_global_filters in target["expr"]
else:
for target in panel["targets"]:
assert serve_global_filters in target["expr"]
Comment on lines 183 to +191
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic to check targets is duplicated for row panels and other panels. You can refactor this to avoid repetition and improve readability.

                panels_to_check = []
                if panel["type"] == "row":
                    # Row panels contain nested panels, not targets directly
                    panels_to_check.extend(panel.get("panels", []))
                else:
                    panels_to_check.append(panel)

                for p in panels_to_check:
                    for target in p.get("targets", []):
                        assert serve_global_filters in target["expr"]

for variable in contents["templating"]["list"]:
if variable["name"] == "datasource":
continue
Expand Down